Writing unit tests

Let's start digging into all the features that PHPUnit offers us in order to write tests. We will divide these features in different subsections: setting up a test, assertions, exceptions, and data providers. Of course, you do not need to use all of these tools each time you write a test.

The start and end of a test

PHPUnit gives you the opportunity to set up a common scenario for each test in a class. For this, you need to use the setUp method, which, if present, is executed each time that a test of this class is executed. The instance of the class that invokes the setUp and test methods is the same, so you can use the properties of the class to save the context. One common use would be to create the object that we will use for our tests in case this is always the same. For an example, write the following code in tests/Domain/Customer/BasicTest.php:

<?php

namespace BookstoreTestsDomainCustomer;

use BookstoreDomainCustomerBasic;
use PHPUnit_Framework_TestCase;

class BasicTest extends PHPUnit_Framework_TestCase {
    private $customer;

    public function setUp() {
        $this->customer = new Basic(
            1, 'han', 'solo', '[email protected]'
        );
    }

    public function testAmountToBorrow() {
        $this->assertSame(
            3,
            $this->customer->getAmountToBorrow(),
            'Basic customer should borrow up to 3 books.'
        );
    }
}

When testAmountToBorrow is invoked, the $customer property is already initialized through the execution of the setUp method. If the class had more than one test, the setUp method would be executed each time.

Even though it is less common to use, there is another method used to clean up the scenario after the test is executed: tearDown. This works in the same way, but it is executed after each test of this class is executed. Possible uses would be to clean up database data, close connections, delete files, and so on.

Assertions

You have already been introduced to the concept of assertions, so let's just list the most common ones in this section. For the full list, we recommend you to visit the official documentation at https://phpunit.de/manual/current/en/appendixes.assertions.html as it is quite extensive; however, to be honest, you will probably not use many of them.

The first type of assertion that we will see is the Boolean assertion, that is, the one that checks whether a value is true or false. The methods are as simple as assertTrue and assertFalse, and they expect one parameter, which is the value to assert, and optionally, a text to display in case of failure. In the same BasicTest class, add the following test:

public function testIsExemptOfTaxes() {
    $this->assertFalse(
        $this->customer->isExemptOfTaxes(),
        'Basic customer should be exempt of taxes.'
    );
}

This test makes sure that a basic customer is never exempt of taxes. Note that we could do the same assertion by writing the following:

$this->assertSame(
    $this->customer->isExemptOfTaxes(),
    false,
    'Basic customer should be exempt of taxes.'
);

A second group of assertions would be the comparison assertions. The most famous ones are assertSame and assertEquals. You have already used the first one, but are you sure of its meaning? Let's add another test and run it:

public function testGetMonthlyFee() {
    $this->assertSame(
        5,
        $this->customer->getMonthlyFee(),
        'Basic customer should pay 5 a month.'
    );
}

The result of the test is shown in the following screenshot:

Assertions

The test failed! The reason is that assertSame is the equivalent to comparing using identity, that is, without using type juggling. The result of the getMonthlyFee method is always a float, and we will compare it with an integer, so it will never be the same, as the error message tells us. Change the assertion to assertEquals, which compares using equality, and the test will pass now.

When working with objects, we can use an assertion to check whether a given object is an instance of the expected class or not. When doing so, remember to send the full name of the class as this is a quite common mistake. Even better, you could get the class name using ::class, for example, Basic::class. Add the following test in tests/Domain/Customer/CustomerFactoryTest.php:

<?php

namespace BookstoreTestsDomainCustomer;

use BookstoreDomainCustomerCustomerFactory;
use PHPUnit_Framework_TestCase;

class CustomerFactoryTest extends PHPUnit_Framework_TestCase {
    public function testFactoryBasic() {
        $customer = CustomerFactory::factory(
            'basic', 1, 'han', 'solo', '[email protected]'
        );

        $this->assertInstanceOf(
Basic::class,
            $customer,
            'basic should create a CustomerBasic object.'
        );
    }
}

This test creates a customer using the customer factory. As the type of customer was basic, the result should be an instance of Basic, which is what we are testing with assertInstanceOf. The first argument is the expected class, the second is the object that we are testing, and the third is the error message. This test also helps us to note the behavior of comparison assertions with objects. Let's create a basic customer object as expected and compare it with the result of the factory. Then, run the test, as follows:

$expectedBasicCustomer = new Basic(1, 'han', 'solo', '[email protected]');

$this->assertSame(
    $customer,
    $expectedBasicCustomer,
    'Customer object is not as expected.'
);

The result of this test is shown in the following screenshot:

Assertions

The test failed because when you compare two objects with identity comparison, you comparing the object reference, and it will only be the same if the two objects are exactly the same instance. If you create two objects with the same properties, they will be equal but never identical. To fix the test, change the assertion as follows:

$expectedBasicCustomer = new Basic(1, 'han', 'solo', '[email protected]');

$this->assertEquals(
    $customer,
    $expectedBasicCustomer,
    'Customer object is not as expected.'
);

Let's now write the tests for the sale domain object at tests/Domain/SaleTest.php. This class is very easy to test and allows us to use some new assertions, as follows:

<?php

namespace BookstoreTestsDomainCustomer;

use BookstoreDomainSale;
use PHPUnit_Framework_TestCase;

class SaleTest extends PHPUnit_Framework_TestCase {
    public function testNewSaleHasNoBooks() {
        $sale = new Sale();

        $this->assertEmpty(
            $sale->getBooks(),
            'When new, sale should have no books.'
        );
    }

    public function testAddNewBook() {
        $sale = new Sale();
        $sale->addBook(123);

        $this->assertCount(
            1,
            $sale->getBooks(),
            'Number of books not valid.'
        );
        $this->assertArrayHasKey(
            123,
            $sale->getBooks(),
            'Book id could not be found in array.'
        );
        $this->assertSame(
            $sale->getBooks()[123],
            1,
            'When not specified, amount of books is 1.'
        );
    }
}

We added two tests here: one makes sure that for a new sale instance, the list of books associated with it is empty. For this, we used the assertEmpty method, which takes an array as an argument and will assert that it is empty. The second test is adding a book to the sale and then making sure that the list of books has the correct content. For this, we will use the assertCount method, which verifies that the array, that is, the second argument, has as many elements as the first argument provided. In this case, we expect that the list of books has only one entry. The second assertion of this test is verifying that the array of books contains a specific key, which is the ID of the book, with the assertArrayHasKey method, in which the first argument is the key, and the second one is the array. Finally, we will check with the already known assertSame method that the amount of books inserted is 1.

Even though these two new assertion methods are useful sometimes, all the three assertions of the last test can be replaced by just an assertSame method, comparing the whole array of books with the expected one, as follows:

$this->assertSame(
    [123 => 1],
    $sale->getBooks(),
    'Books array does not match.'
);

The suite of tests for the sale domain object would not be enough if we were not testing how the class behaves when adding multiple books. In this case, using assertCount and assertArrayHasKey would make the test unnecessarily long, so let's just compare the array with an expected one via the following code:

public function testAddMultipleBooks() {
    $sale = new Sale();
    $sale->addBook(123, 4);
    $sale->addBook(456, 2);
    $sale->addBook(456, 8);

    $this->assertSame(
        [123 => 4, 456 => 10],
        $sale->getBooks(),
        'Books are not as expected.'
    );
}

Expecting exceptions

Sometimes, a method is expected to throw an exception for certain unexpected use cases. When this happens, you could try to capture this exception inside the test or take advantage of another tool that PHPUnit offers: expecting exceptions. To mark a test to expect a given exception, just add the @expectedException annotation followed by the exception's class full name. Optionally, you can use @expectedExceptionMessage to assert the message of the exception. Let's add the following tests to our CustomerFactoryTest class:

/**
 * @expectedException InvalidArgumentException
 * @expectedExceptionMessage Wrong type.
 */
public function testCreatingWrongTypeOfCustomer() {
    $customer = CustomerFactory::factory(
        'deluxe', 1, 'han', 'solo', '[email protected]'

   );
}

In this test we will try to create a deluxe customer with our factory, but as this type of customer does not exist, we will get an exception. The type of the expected exception is InvalidArgumentException, and the error message is "Wrong type". If you run the tests, you will see that they pass.

If we defined an expected exception and the exception is never thrown, the test will fail; expecting exceptions is just another type of assertion. To see this happen, add the following to your test and run it; you will get a failure, and PHPUnit will complain saying that it expected the exception, but it was never thrown:

/**
 * @expectedException InvalidArgumentException
 */
public function testCreatingCorrectCustomer() {
    $customer = CustomerFactory::factory(
        'basic', 1, 'han', 'solo', '[email protected]'
    );
}

Data providers

If you think about the flow of a test, most of the time, we invoke a method with an input and expect an output. In order to cover all the edge cases, it is natural that we will repeat the same action with a set of inputs and expected outputs. PHPUnit gives us the ability to do so, thus removing a lot of duplicated code. This feature is called data providing.

A data provider is a public method defined in the test class that returns an array with a specific schema. Each entry of the array represents a test in which the key is the name of the test—optionally, you could use numeric keys—and the value is the parameter that the test needs. A test will declare that it needs a data provider with the @dataProvider annotation, and when executing tests, the data provider injects the arguments that the test method needs. Let's consider an example to make it easier. Write the following two methods in your CustomerFactoryTest class:

public function providerFactoryValidCustomerTypes() {
    return [
        'Basic customer, lowercase' => [
            'type' => 'basic',
            'expectedType' => 'BookstoreDomainCustomerBasic'
        ],
        'Basic customer, uppercase' => [
            'type' => 'BASIC',
            'expectedType' => 'BookstoreDomainCustomerBasic'
        ],
        'Premium customer, lowercase' => [
            'type' => 'premium',
            'expectedType' => 'BookstoreDomainCustomerPremium'
        ],
        'Premium customer, uppercase' => [
            'type' => 'PREMIUM',
            'expectedType' => 'BookstoreDomainCustomerPremium'
        ]
    ];
}

/**
 * @dataProvider providerFactoryValidCustomerTypes
 * @param string $type
 * @param string $expectedType
 */
public function testFactoryValidCustomerTypes(
    string $type,
    string $expectedType
) {
    $customer = CustomerFactory::factory(
        $type, 1, 'han', 'solo', '[email protected]'
    );
    $this->assertInstanceOf(
        $expectedType,
        $customer,
        'Factory created the wrong type of customer.'
    );
}

The test here is testFactoryValidCustomerTypes, which expects two arguments: $type and $expectedType. The test uses them to create a customer with the factory and verify the type of the result, which we already did by hardcoding the types. The test also declares that it needs the providerFactoryValidCustomerTypes data provider. This data provider returns an array of four entries, which means that the test will be executed four times with four different sets of arguments. The name of each test is the key of each entry—for example, "Basic customer, lowercase". This is very useful in case a test fails because it will be displayed as part of the error messages. Each entry is a map with two values, type and expectedType, which are the names of the arguments of the test method. The values of these entries are the values that the test method will get.

The bottom line is that the code we wrote would be the same as if we wrote testFactoryValidCustomerTypes four times, hardcoding $type and $expectedType each time. Imagine now that the test method contains tens of lines of code or we want to repeat the same test with tens of datasets; do you see how powerful it is?

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

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