Test-Driven Development, often abbreviated as TDD, is a software development process. In short, with TDD you're writing the tests before writing the code, not the other way around. Let's take a look at the process in more detail.

  1. Before you write a single line of actual code you write a test. This is like thinking a new feature through and describing it on the paper. Kind of like "I want to make a GET request to the '/blog/posts' route and get a paginated list of blog posts from the server", but more specifically.

  2. Alright, you've got your test, but of course it fails, since there's no code yet. You then gradually start to implement the code just to make the test pass. This is like creating a prototype - it works as expected, but the implementation might not be the best.

  3. You may now start refactoring your code. Run the test after each change and make sure it still passes. Stop when you're satisfied with the code quality. Move to the next feature.

If you don't see the advantages of TDD yet - here they are.

You might get lazy to write tests for the existing code that seems to already work fine. Why would you? With TDD you're writing the tests first. As the result, most of your code is covered with tests, and it probably is covered better, since you implement the functionality according to the tests, not the other way around.

And having your code covered with tests eliminates this situation (which occurs more often than we wish it did), when you change a line of code in one place, and then something breaks in a totally unexpected and seemingly unrelated place, and you only find out about it a few hours/days/weeks/months later. With tests all you have to do is run your whole test suite after a change in the code and any broken pieces will pop up immediately.

Getting Started

We'll be learning hands-on, so go ahead and create a new Laravel project (at the time of writing this the latest Laravel version is 5.6). Laravel includes PHPUnit out of the box, although you might want to install it globally on your machine by running:

composer global require phpunit/phpunit

Laravel also includes a PHPUnit config file phpunit.xml and a folder structure to store the tests with two example test classes:

- tests
    - Feature
        - ExampleTest.php
    - Unit
        - ExampleTest.php
    - CreatesApplication.php
    - TestCase.php

The CreatesApplication.php trait bootstraps a Laravel application for our tests and is used in the TestCase.php class.

The TestCase.php class is the base class all your Laravel tests extend from. This class extends from another Laravel class, which in turn extends from the PHPUnit's TestCase class.

The two folders are used to store feature and unit tests respectively. There is no real difference between the two. You should use unit tests to test individual units (e.g a single class), and feature tests to test more complex things.

Before we move on make sure PHPUnit works and the two example tests pass. Run vendor/bin/phpunit (or just phpunit if you've installed it globally) from the root folder of your Laravel project. You should see something like this:

PHPUnit 7.0.2 by Sebastian Bergmann and contributors.

..                                                   2 / 2 (100%)

Time: 79 ms, Memory: 10.00MB

OK (2 tests, 2 assertions)

Note, that over time the amount of tests in your project will increase greatly and running the whole test suite each time will be pretty slow an unnecessary. Instead, you could run a single test class or a test method. Just execute phpunit --filter followed by a test class or method name, for example phpunit --filter ExampleTest or phpunit --filter testExample.

Alright, now go ahead and delete both ExampleTest.php files, we won't need them anymore.

Preparing the Database

We we'll be developing a simple news module with CRUD to demonstrate the TDD workflow. But before that let's prepare our database. Create a new model with a corresponding migration by running:

php artisan make:model News -m

Our news will have a title, a body and a main image displayed at the top of the page. This is what the migration looks like:

Schema::create('news', function (Blueprint $table) {
    $table->increments('id');
    $table->string('title');
    $table->string('body');
    $table->string('image_path')->nullable();
    $table->timestamps();
});

Now create your first test to test the creating of new news:

php artisan make:test CreateNewsTest

This will create a new file tests/Feature/CreateNewsTest.php. It contains an example test inside and if you run phpunit it will pass.

Each test file and class name should end with "Test". Each test method name should start with "test". If you want to call your method differently, write a /** @test */ annotation before the method declaration. This is what we'll be doing to give methods more descriptive names.

Open the test file. You'll notice it imports the use Illuminate\Foundation\Testing\RefreshDatabase; trait at the top. Go ahead and use it in our test class by adding use RefreshDatabase; at the beginning of the class.

class CreateNewsTest extends TestCase
{
    use RefreshDatabase;
    ...
}

This trait un-does any changes to the database that has been made during a test method execution, so that by the time the next test method is run the database is at its initial state. This assures that the data from one test is not interfering with the results of other tests and the order in which you perform tests doesn't matter.

If you run your tests now it will fail since we haven't setup a database connection in the .env file yet. Instead, we're going to set it up in the phpunit.xml file. Open the file and scroll down until you see the <php> section at the bottom. This is where the env variables for the testing environment are set and these will only take effect when you're executing PHPUnit tests. Here you can override any values from your .env file.

We are going to use the in-memory SQLite database for our tests. The in-memory database is stored in RAM, hence it works incredibly fast (as compared to, say, MySQL). All the data is destroyed after the tests are done, and since it's a separate database there's going to be no interference with your main database and data whatsoever.

The only downside here is the SQLite syntax and behavior are slightly different from the syntax and behavior of the real database you're going to use, be it MySQL, PostgreSQL or some other SQL database. Most of the times everything will work fine, but sometimes you'll run into situations when your tests fail, even though the actual code is working perfectly fine. This is something to keep in mind and there are workarounds for certain cases.

Alright, now add these lines inside the php block of the phpunit.xml file:

<php>
    ...
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
</php>

If you run phpunit now the test will pass, unless you don't have SQLite installed on your system, which you'll have to take care of on your own - just google it. Now we're ready to start the actual development, move on to the next part.