Using mocks

Well, we've had enough of the helpers; let's start with the tests. The difficult part here is how to play with mocks. When you create one, you can add some expectations and return values. The methods are:

  • : This specifies the amount of times the mock's method is invoked. You can send , , or as an argument to specify 0, 1, or any invocations.
  • : This is used to specify the method we are talking about. The argument that it expects is just the name of the method.
  • : This is a method used to set the expectations of the arguments that the mock will receive when it is invoked. For example, if the mocked method is expected to get as the first argument and as the second, the method will be invoked as . This method is optional, but if we set it, PHPUnit will throw an error in case the mocked method does not get the expected arguments, so it works as an assertion.
  • : This is used to define what the mock will return. The two most common usages are or . This method is also optional, and if not invoked, the mock will always return null.

Let's add the first test to see how it would work. Add the following code to the file:

<?php

namespace Bookstore\Tests\Controllers;

use Bookstore\Controllers\BookController;
use Bookstore\Core\Request;
use Bookstore\Exceptions\NotFoundException;
use Bookstore\Models\BookModel;
use Bookstore\Tests\ControllerTestCase;
use Twig_Template;

class BookControllerTest extends ControllerTestCase {
    private function getController(
        Request $request = null
    ): BookController {
        if ($request === null) {
            $request = $this->mock('Core\Request');
        }
        return new BookController($this->di, $request);
    }

    public function testBookNotFound() {
        $bookModel = $this->mock(BookModel::class);
        $bookModel
            ->expects($this->once())
            ->method('get')
            ->with(123)
            ->will(
                $this->throwException(
                    new NotFoundException()
                )
            );
        $this->di->set('BookModel', $bookModel);

        $response = "Rendered template";
        $template = $this->mock(Twig_Template::class);
        $template
            ->expects($this->once())
            ->method('render')
            ->with(['errorMessage' => 'Book not found.'])
            ->will($this->returnValue($response));
        $this->di->get('Twig_Environment')
            ->expects($this->once())
            ->method('loadTemplate')
            ->with('error.twig')
            ->will($this->returnValue($template));

        $result = $this->getController()->borrow(123);

        $this->assertSame(
            $result,
            $response,
            'Response object is not the expected one.'
        );
    }
}

The first thing the test does is to create a mock of the class. Then, it adds an expectation that goes like this: the method will be called once with one argument, , and it will throw . This makes sense as the test tries to emulate a scenario in which we cannot find the book in the database.

The second part of the test consists of adding the expectations of the template engine. This is a bit more complex as there are two mocks involved. The method of is expected to be called once with the argument as the template name. This mock should return , which is another mock. The method of this second mock is expected to be called once with the correct error message, returning the response, which is a hardcoded string. After all the dependencies are defined, we just need to invoke the method of the controller and expect a response.

Remember that this test does not have only one assertion, but four: the method and the three mock expectations. If any of them are not accomplished, the test will fail, so we can say that this method is quite robust.

With our first test, we verified that the scenario in which the book is not found works. There are two more scenarios that fail as well: when there are not enough copies of the book to borrow and when there is a database error when trying to save the borrowed book. However, you can see now that all of them share a piece of code that mocks the template. Let's extract this code to a method that generates the mocks when it is given the template name, the parameters are sent to the template, and the expected response is received. Run the following:

protected function mockTemplate(
    string $templateName,
    array $params,
    $response
) {
    $template = $this->mock(Twig_Template::class);
    $template
        ->expects($this->once())
        ->method('render')
        ->with($params)
        ->will($this->returnValue($response));
    $this->di->get('Twig_Environment')
        ->expects($this->once())
        ->method('loadTemplate')
        ->with($templateName)
        ->will($this->returnValue($template));
}

public function testNotEnoughCopies() {
    $bookModel = $this->mock(BookModel::class);
    $bookModel
        ->expects($this->once())
        ->method('get')
        ->with(123)
        ->will($this->returnValue(new Book()));
    $bookModel
        ->expects($this->never())
        ->method('borrow');
    $this->di->set('BookModel', $bookModel);

    $response = "Rendered template";
    $this->mockTemplate(
        'error.twig',
        ['errorMessage' => 'There are no copies left.'],
        $response
    );

    $result = $this->getController()->borrow(123);

    $this->assertSame(
        $result,
        $response,
        'Response object is not the expected one.'
    );
}

public function testErrorSaving() {
    $controller = $this->getController();
    $controller->setCustomerId(9);

    $book = new Book();
    $book->addCopy();
    $bookModel = $this->mock(BookModel::class);
    $bookModel
        ->expects($this->once())
        ->method('get')
        ->with(123)
        ->will($this->returnValue($book));
    $bookModel
        ->expects($this->once())
        ->method('borrow')
        ->with(new Book(), 9)
        ->will($this->throwException(new DbException()));
    $this->di->set('BookModel', $bookModel);

    $response = "Rendered template";
    $this->mockTemplate(
        'error.twig',
        ['errorMessage' => 'Error borrowing book.'],
        $response
    );

    $result = $controller->borrow(123);

    $this->assertSame(
        $result,
        $response,
        'Response object is not the expected one.'
    );
}

The only novelty here is when we expect that the method is never invoked. As we do not expect it to be invoked, there is no reason to use the nor method. If the code actually invokes this method, PHPUnit will mark the test as failed.

We already tested and found that all the scenarios that can fail have failed. Let's add a test now where a user can successfully borrow a book, which means that we will return valid books and customers from the database, the method will be invoked correctly, and the template will get all the correct parameters. The test looks as follows:

public function testBorrowingBook() {
    $controller = $this->getController();
    $controller->setCustomerId(9);

    $book = new Book();
    $book->addCopy();
    $bookModel = $this->mock(BookModel::class);
    $bookModel
        ->expects($this->once())
        ->method('get')
        ->with(123)
        ->will($this->returnValue($book));
    $bookModel
        ->expects($this->once())
        ->method('borrow')
        ->with(new Book(), 9);
    $bookModel
        ->expects($this->once())
        ->method('getByUser')
        ->with(9)
        ->will($this->returnValue(['book1', 'book2']));
    $this->di->set('BookModel', $bookModel);

    $response = "Rendered template";
    $this->mockTemplate(
        'books.twig',
        [
            'books' => ['book1', 'book2'],
            'currentPage' => 1,
            'lastPage' => true
        ],
        $response
    );

    $result = $controller->borrow(123);

    $this->assertSame(
        $result,
        $response,
        'Response object is not the expected one.'
    );
}

So this is it. You have written one of the most complex tests you will need to write during this book. What do you think of it? Well, as you do not have much experience with tests, you might be quite satisfied with the result, but let's try to analyze it a bit further.

Learning PHP 7
titlepage.xhtml
part0000.html
part0001.html
part0002.html
part0003.html
part0004.html
part0005.html
part0006.html
part0007_split_000.html
part0007_split_001.html
part0007_split_002.html
part0008_split_000.html
part0008_split_001.html
part0009.html
part0010.html
part0011.html
part0012.html
part0013_split_000.html
part0013_split_001.html
part0013_split_002.html
part0013_split_003.html
part0013_split_004.html
part0014_split_000.html
part0014_split_001.html
part0014_split_002.html
part0014_split_003.html
part0014_split_004.html
part0015_split_000.html
part0015_split_001.html
part0015_split_002.html
part0015_split_003.html
part0015_split_004.html
part0016_split_000.html
part0016_split_001.html
part0016_split_002.html
part0016_split_003.html
part0016_split_004.html
part0017_split_000.html
part0017_split_001.html
part0017_split_002.html
part0017_split_003.html
part0018.html
part0019_split_000.html
part0019_split_001.html
part0019_split_002.html
part0019_split_003.html
part0019_split_004.html
part0020_split_000.html
part0020_split_001.html
part0021_split_000.html
part0021_split_001.html
part0021_split_002.html
part0021_split_003.html
part0022.html
part0023_split_000.html
part0023_split_001.html
part0024_split_000.html
part0024_split_001.html
part0025_split_000.html
part0025_split_001.html
part0025_split_002.html
part0025_split_003.html
part0025_split_004.html
part0025_split_005.html
part0025_split_006.html
part0026.html
part0027_split_000.html
part0027_split_001.html
part0027_split_002.html
part0027_split_003.html
part0027_split_004.html
part0027_split_005.html
part0027_split_006.html
part0027_split_007.html
part0028_split_000.html
part0028_split_001.html
part0028_split_002.html
part0028_split_003.html
part0028_split_004.html
part0029_split_000.html
part0029_split_001.html
part0029_split_002.html
part0029_split_003.html
part0030_split_000.html
part0030_split_001.html
part0030_split_002.html
part0030_split_003.html
part0030_split_004.html
part0031_split_000.html
part0031_split_001.html
part0031_split_002.html
part0031_split_003.html
part0032.html
part0033_split_000.html
part0033_split_001.html
part0033_split_002.html
part0033_split_003.html
part0033_split_004.html
part0033_split_005.html
part0034_split_000.html
part0034_split_001.html
part0035.html
part0036.html
part0037_split_000.html
part0037_split_001.html
part0037_split_002.html
part0038_split_000.html
part0038_split_001.html
part0038_split_002.html
part0038_split_003.html
part0039_split_000.html
part0039_split_001.html
part0040.html
part0041_split_000.html
part0041_split_001.html
part0041_split_002.html
part0041_split_003.html
part0042_split_000.html
part0042_split_001.html
part0042_split_002.html
part0043.html
part0044.html
part0045_split_000.html
part0045_split_001.html
part0045_split_002.html
part0046_split_000.html
part0046_split_001.html
part0046_split_002.html
part0046_split_003.html
part0047_split_000.html
part0047_split_001.html
part0047_split_002.html
part0047_split_003.html
part0047_split_004.html
part0048.html
part0049.html
part0050_split_000.html
part0050_split_001.html
part0050_split_002.html
part0050_split_003.html
part0051.html
part0052.html
part0053_split_000.html
part0053_split_001.html
part0053_split_002.html
part0053_split_003.html
part0054.html
part0055.html
part0056_split_000.html
part0056_split_001.html
part0057_split_000.html
part0057_split_001.html
part0057_split_002.html
part0057_split_003.html
part0057_split_004.html
part0058_split_000.html
part0058_split_001.html
part0058_split_002.html
part0058_split_003.html
part0058_split_004.html
part0059_split_000.html
part0059_split_001.html
part0059_split_002.html
part0059_split_003.html
part0060_split_000.html
part0060_split_001.html
part0060_split_002.html
part0060_split_003.html
part0060_split_004.html
part0060_split_005.html
part0060_split_006.html
part0060_split_007.html
part0061_split_000.html
part0061_split_001.html
part0061_split_002.html
part0061_split_003.html
part0061_split_004.html
part0061_split_005.html
part0062_split_000.html
part0062_split_001.html
part0062_split_002.html
part0063.html
part0064_split_000.html
part0064_split_001.html
part0064_split_002.html
part0064_split_003.html
part0065_split_000.html
part0065_split_001.html
part0065_split_002.html
part0065_split_003.html
part0066_split_000.html
part0066_split_001.html
part0066_split_002.html
part0066_split_003.html
part0066_split_004.html
part0067_split_000.html
part0067_split_001.html
part0067_split_002.html
part0067_split_003.html
part0068.html
part0069_split_000.html
part0069_split_001.html
part0070.html
part0071_split_000.html
part0071_split_001.html
part0071_split_002.html
part0071_split_003.html
part0072_split_000.html
part0072_split_001.html
part0072_split_002.html
part0072_split_003.html
part0072_split_004.html
part0073_split_000.html
part0073_split_001.html
part0073_split_002.html
part0074_split_000.html
part0074_split_001.html
part0074_split_002.html
part0074_split_003.html
part0075_split_000.html
part0075_split_001.html
part0075_split_002.html
part0075_split_003.html
part0075_split_004.html
part0075_split_005.html
part0075_split_006.html
part0075_split_007.html
part0076_split_000.html
part0076_split_001.html
part0076_split_002.html
part0076_split_003.html
part0076_split_004.html
part0077.html
part0078.html
part0079_split_000.html
part0079_split_001.html
part0080.html
part0081_split_000.html
part0081_split_001.html
part0081_split_002.html
part0081_split_003.html
part0082_split_000.html
part0082_split_001.html
part0082_split_002.html
part0082_split_003.html
part0082_split_004.html
part0083_split_000.html
part0083_split_001.html
part0083_split_002.html
part0084_split_000.html
part0084_split_001.html
part0084_split_002.html
part0084_split_003.html
part0084_split_004.html
part0084_split_005.html
part0085_split_000.html
part0085_split_001.html
part0085_split_002.html
part0085_split_003.html
part0085_split_004.html
part0085_split_005.html
part0086.html
part0087.html
part0088_split_000.html
part0088_split_001.html
part0088_split_002.html
part0088_split_003.html
part0088_split_004.html
part0088_split_005.html
part0089_split_000.html
part0089_split_001.html
part0089_split_002.html
part0089_split_003.html
part0089_split_004.html
part0090_split_000.html
part0090_split_001.html
part0090_split_002.html
part0090_split_003.html
part0091.html
part0092_split_000.html
part0092_split_001.html
part0092_split_002.html
part0092_split_003.html
part0092_split_004.html
part0092_split_005.html
part0092_split_006.html
part0092_split_007.html
part0092_split_008.html
part0092_split_009.html
part0092_split_010.html
part0092_split_011.html
part0092_split_012.html
part0092_split_013.html
part0092_split_014.html
part0092_split_015.html
part0092_split_016.html
part0092_split_017.html
part0092_split_018.html
part0092_split_019.html
part0092_split_020.html
part0092_split_021.html
part0092_split_022.html
part0092_split_023.html
part0092_split_024.html