Test-driven development

You might realize already that there is no unique way to do things when talking about developing an application. It is out of the scope of this book to show you all of them—and by the time you are done reading these lines, more techniques will have been incorporated already—but there is one approach that is very useful when it comes to writing good, testable code: test-driven development (TDD).

This methodology consists of writing the unit tests before writing the code itself. The idea, though, is not to write all the tests at once and then write the class or method but rather to do it in a progressive way. Let's consider an example to make it easier. Imagine that your Sale class is yet to be implemented and the only thing we know is that we have to be able to add books. Rename your src/Domain/Sale.php file to src/Domain/Sale2.php or just delete it so that the application does not know about it.

Note

Is all this verbosity necessary?

You will note in this example that we will perform an excessive amount of steps to come up with a very simple piece of code. Indeed, they are too many for this example, but there will be times when this amount is just fine. Finding these moments comes with experience, so we recommend you to practice first with simple examples. Eventually, it will come naturally to you.

The mechanics of TDD consist of four steps, as follows:

  1. Write a test for some functionality that is not yet implemented.
  2. Run the unit tests, and they should fail. If they do not, either your test is wrong, or your code already implements this functionality.
  3. Write the minimum amount of code to make the tests pass.
  4. Run the unit tests again. This time, they should pass.

We do not have the sale domain object, so the first thing, as we should start from small things and then move on to bigger things, is to assure that we can instantiate the sale object. Write the following unit test in tests/Domain/SaleTest.php as we will write all the existing tests, but using TDD; you can remove the existing tests in this file.

<?php

namespace BookstoreTestsDomain;

use BookstoreDomainSale;
use PHPUnit_Framework_TestCase;

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

Run the tests to make sure that they are failing. In order to run one specific test, you can mention the file of the test when running PHPUnit, as shown in the following script:

Test-driven development

Good, they are failing. That means that PHP cannot find the object to instantiate it. Let's now write the minimum amount of code required to make this test pass. In this case, creating the class would be enough, and you can do this through the following lines of code:

<?php

namespace BookstoreDomain;

class Sale {
}

Now, run the tests to make sure that there are no errors.

Test-driven development

This is easy, right? So, what we need to do is repeat this process, adding more functionality each time. Let's focus on the books that a sale holds; when created, the book's list should be empty, as follows:

public function testWhenCreatedBookListIsEmpty() {
    $sale = new Sale();

    $this->assertEmpty($sale->getBooks());
}

Run the tests to make sure that they fail—they do. Now, write the following method in the class:

public function getBooks(): array {
return [];
}

Now, if you run... wait, what? We are forcing the getBooks method to return an empty array always? This is not the implementation that we need—nor the one we deserve—so why do we do it? The reason is the wording of step 3: "Write the minimum amount of code to make the tests pass.". Our test suite should be extensive enough to detect this kind of problem, and this is our way to make sure it does. This time, we will write bad code on purpose, but next time, we might introduce a bug unintentionally, and our unit tests should be able to detect it as soon as possible. Run the tests; they will pass.

Now, let's discuss the next functionality. When adding a book to the list, we should see this book with amount 1. The test should be as follows:

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

    $this->assertSame(
        $sale->getBooks(),
        [123 => 1]
    );
}

This test is very useful. Not only does it force us to implement the addBook method, but also it helps us fix the getBooks method—as it is hardcoded right now—to always return an empty array. As the getBooks method now expects two different results, we cannot trick the tests any more. The new code for the class should be as follows:

class Sale {
    private $books = [];

    public function getBooks(): array {
        return $this->books;
    }

    public function addBook(int $bookId) {
        $this->books[123] = 1;
    }
}

A new test we can write is the one that allows you to add more than one book at a time, sending the amount as the second argument. The test would look similar to the following:

public function testSpecifyAmountBooks() {
    $sale = new Sale();
    $sale->addBook(123, 5);

    $this->assertSame(
        $sale->getBooks(),
        [123 => 5]
    );
}

Now, the tests do not pass, so we need to fix them. Let's refactor addBook so that it can accept a second argument as the amount :

public function addBook(int $bookId, int $amount = 1) {
    $this->books[123] = $amount;
}

The next functionality we would like to add is the same book invoking the method several times, keeping track of the total amount of books added. The test could be as follows:

public function testAddMultipleTimesSameBook() {
    $sale = new Sale();
    $sale->addBook(123, 5);
    $sale->addBook(123);
    $sale->addBook(123, 5);

    $this->assertSame(
        $sale->getBooks(),
        [123 => 11]
    );
}

This test will fail as the current execution will not add all the amounts but will instead keep the last one. Let's fix it by executing the following code:

public function addBook(int $bookId, int $amount = 1) {
    if (!isset($this->books[123])) {
        $this->books[123] = 0;
    }
    $this->books[123] += $amount;
}

Well, we are almost there. There is one last test we should add, which is the ability to add more than one different book. The test is as follows:

public function testAddDifferentBooks() {
    $sale = new Sale();
    $sale->addBook(123, 5);
    $sale->addBook(456, 2);
    $sale->addBook(789, 5);

    $this->assertSame(
        $sale->getBooks(),
        [123 => 5, 456 => 2, 789 => 5]
    );
}

This test fails due to the hardcoded book ID in our implementation. If we did not do this, the test would have already passed. Let's fix it then; run the following:

public function addBook(int $bookId, int $amount = 1) {
    if (!isset($this->books[$bookId])) {
        $this->books[$bookId] = 0;
    }
    $this->books[$bookId] += $amount;
}

We are done! Does it look familiar? It is the same code we wrote on our first implementation except for the rest of the properties. You can now replace the sale domain object with the previous one, so you have all the functionalities needed.

Theory versus practice

As mentioned before, this is a quite long and verbose process that very few experienced developers follow from start to end but one that most of them encourage people to follow. Why is this so? When you write all your code first and leave the unit tests for the end, there are two problems:

  • Firstly, in too many cases developers are lazy enough to skip tests, telling themselves that the code already works, so there is no need to write the tests. You already know that one of the goals of tests is to make sure that future changes do not break the current features, so this is not a valid reason.
  • Secondly, the tests written after the code usually test the code rather than the functionality. Imagine that you have a method that was initially meant to perform an action. After writing the method, we will not perform the action perfectly due to a bug or bad design; instead, we will either do too much or leave some edge cases untreated. When we write the test after writing the code, we will test what we see in the method, not what the original functionality was!

If you instead force yourself to write the tests first and then the code, you make sure that you always have tests and that they test what the code is meant to do, leading to a code that performs as expected and is fully covered. Also, by doing it in small intervals, you get quick feedback and don't have to wait for hours to know whether all the tests and code you wrote make sense at all. Even though this idea is quite simple and makes a lot of sense, many novice developers find it hard to implement.

Experienced developers have written code for several years, so they have already internalized all of this. This is the reason why some of them prefer to either write several tests before starting with the code or the other way around, that is, writing code and then testing it as they are more productive this way. However, if there is something that all of them have in common it is that their applications will always be full of tests.

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

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