Unit tests

As briefly mentioned at the beginning, unit tests are used for testing single units that make up the code architecture. In practice, this means testing individual classes, especially the methods they contain and what they should be doing. Since the testing happens at such a low level, they are by far the fastest tests that can be run.

The logic behind unit tests is quite simple: after providing input, the test asserts that the method output is correct. Typically, the more input -> output scenarios it covers, the more stable the tested code is. For example, tests should also cover unexpected scenarios, as well as exercise all the code contained in the tested methods (such as forks created by if/else statements).

The programming pattern of dependency injection—objects should receive as dependencies other objects they might need—becomes critical when it comes to unit testing. The reason is that if class methods work with the global scope or instantiate other objects, we can no longer test them cleanly. Instead, if they require dependencies, we can mock them and pass these within the context of the executed tests. We will see some examples shortly. But before we do that, let's create a simple class that can be easily tested using a unit test.

A typical example is a simple calculator class. It will take two numbers as arguments to its constructor and have four methods for performing basic arithmetic on those numbers. We'll put this into our Hello World module:

namespace Drupalhello_world; 
 
/** 
 * Class used to demonstrate a simple Unit test. 
 */ 
class Calculator { 
 
  private $a; 
  private $b; 
 
  public function __construct($a, $b) { 
    $this->a = $a; 
    $this->b = $b; 
  } 
 
  public function add() { 
    return $this->a + $this->b; 
  } 
 
  public function subtract() { 
    return $this->a - $this->b; 
  } 
 
  public function multiply() { 
    return $this->a * $this->b; 
  } 
 
  public function divide() { 
    return $this->a / $this->b; 
  } 
} 

Nothing so complicated here. You could argue that a calculator class should not get any dependencies but instead pass the numbers to the actual arithmetic methods. However, this will work just as fine for our example and is a bit less repetitive.

Now, let's create the first unit test to make sure that this class behaves as we expect it. In the previous section, we saw which directory these need to go in. So, in our case, it will be /tests/src/Unit. And the test class looks like this:

namespace DrupalTestshello_worldUnit; 
 
use Drupalhello_worldCalculator; 
use DrupalTestsUnitTestCase; 
 
/** 
 * Tests the Calculator class methods. 
 * 
 * @group hello_world 
 */ 
class CalculatorTest extends UnitTestCase { 
 
  /** 
   * Tests the Calculator::add() method. 
   */ 
  public function testAdd() { 
    $calculator = new Calculator(10, 5); 
    $this->assertEquals(15, $calculator->add()); 
  } 
 
  /** 
   * Tests the Calculator::subtract() method. 
   */ 
  public function testSubtract() { 
    $calculator = new Calculator(10, 5); 
    $this->assertEquals(5, $calculator->subtract()); 
  } 
 
  /** 
   * Tests the Calculator::multiply() method. 
   */ 
  public function testMultiply() { 
    $calculator = new Calculator(10, 5); 
    $this->assertEquals(50, $calculator->multiply()); 
  } 
 
  /** 
   * Tests the Calculator::divide() method. 
   */ 
  public function testDivide() { 
    $calculator = new Calculator(10, 5); 
    $this->assertEquals(2, $calculator->divide()); 
  } 
 
}  

First of all, you notice the namespace corresponds to the pattern what we saw in the previous chapter. Second of all, the PHPDoc contains the required information: a summary and the @group tag. Third of all, the class name ends with the word Test. Finally, the class extends UnitTestCase, which is the base class we need to extend for all unit tests.

All types of test class names in Drupal 8 need to end with the word Test and extend the relevant base class that provides specific code for that type of test.

Then, we have the actual methods that test various aspects of the Calculator class and which always have to start with the word test. This is what tells PHPUnit that they need to be run. These methods are the actual standalone tests themselves, meaning that the CalculatorTest class has four tests. Moreover, each of these tests runs independently of the other.

Since the Calculator arithmetic is very simple, it's not difficult to understand what we are doing to test it. For each method, we are instantiating a new instance with some numbers, and then we assert that the result from the arithmetic operation equals to what we expect. The base class provides a multitude of different assertion methods that we can use in our tests. Since there are so many of them, we are not going to cover them all here. We will see more as we write more tests, but I strongly recommend you check the base classes of the various types of test suites for methods that start with the word assert. A great way is also to use an IDE that autocompletes as you type the method name. It can be very handy.

With this, we can already run the test and see whether it passes. Normally, it should because we can do math in our heads and we know it's correct:

../vendor/bin/phpunit ../modules/custom/hello_world/tests/src/Unit/CalculatorTest.php  

The result should be green:

OK (4 tests, 4 assertions)  

However, earlier I mentioned that a good test also accounts for unexpected situations and negative responses. However, we have not done so very well in our example. If we look at testAdd(), we can see that the assertion is correct with those two numbers. But what if we later go to the Calculator::add() method and change it to this by accident:

return 15;  

The test will still pass but will it actually be a true positive? Not really, because if we pass different numbers, the calculation won't match anymore. So we should test these methods with more than just one set of numbers to actually prove that the math behind the Calculator class is valid.

So instead, we can do something like this:

$calculator = new Calculator(10, 5); 
$this->assertEquals(15, $calculator->add()); 
$calculator = new Calculator(10, 6); 
$this->assertEquals(16, $calculator->add());  

This way, we are sure that the addition operation works correctly. One trade-off in this is that we have a bit of repetitive code, especially if we have to do this for all the other operations as well.

Generally, when writing tests, repetition is much more accepted than when writing the actual code. Many times, there is nothing you can do about it as the code will seem very repetitive. However, in our case, we can actually do something by using the setUp() method which is called by PHPUnit before each test method runs. Its purpose is to perform various preparation tasks that are common for all the tests in the class. However, don't take this to mean that it runs only once and then is used by all. In fact, it runs before each individual test method.

So, what we can do is something like this:

/** 
 * @var Drupalhello_worldCalculator 
 */ 
protected $calculatorOne; 
 
/** 
 * @var Drupalhello_worldCalculator 
 */ 
protected $calculatorTwo; 
 
/** 
 * {@inheritdoc} 
 */ 
public function setUp() { 
  parent::setUp(); 
  $this->calculatorOne = new Calculator(10, 5); 
  $this->calculatorTwo = new Calculator(10, 2); 
}  

We create two class properties and inside the setUp() method we assign them to our calculator objects. A very important thing to keep in mind is to always call the parent call of this method because it does very important things for the environment setup. Especially as we move to Kernel and Functional tests.

Now, the testAdd() method can look like this:

public function testAdd() { 
  $this->assertEquals(15, $this->calculatorOne->add()); 
  $this->assertEquals(12, $this->calculatorTwo->add()); 
}  

Much cleaner and less repetitive. Based on this, you can extrapolate and apply the same changes to the other methods yourself.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset