TDD

Test Driven Development in Laravel

When creating an application or new functionality, tests can be written in parallel during its development or only at the very end. In the case of TDD, the opposite is true. First, we write a test for non-existent functionality, and then we write a code that will make our test pass.

Here's what the TDD cycle looks like at a glance:

  1. We write a test for non-existent functionality.
  2. We run the test, which of course ends with an error. Based on the generated error, we create the minimum code that allows you to pass the test.
  3. After passing the test, we develop our code and run the tests again.

We can also call this cycle red, green, refactor. The first test run generates errors (red), after writing the code responsible for the new functionality, restarting the test ends with a positive result (green). Then we develop our code (refactor).

For the purposes of this article, let's assume that we will develop an application with the ability to edit and delete posts.

Preparation

In the case of Laravel, many things have already been prepared and creating a new test is extremely easy. To get started, we need to start with the database configuration. In the phpunit.xml file, which is located in the main project directory between the tags, we need to add (or in the case of Laravel 8 uncomment) the following code:

<server name="DB_CONNECTION" value="sqlite"/>

<server name="DB_DATABASE" value=":memory:"/> 

Thanks to this, when running the test, we do not have to use the database that we use every day when building the application. Then we need to create the appropriate file in which we will write tests. As always with Laravel, artisan comes to the rescue:

php artisan make:test PostTest

Create a new resource

In the newly created file tests/Feature/PostTest.php create a new test inorder to setting up new resources.

  /** @test */

    public function a_post_can_be_created()
    {
        $response = $this->post('/posts', [
            'title' => 'This is a test post.',
            'body' => 'Some lorem ipsum text.'
        ]);

        $response->assertOk();
        $this->assertCount(1, Post::all());
    }

Note of a few key points:

  1. A comment containing @test. This is necessary to run our test.
  2. The name of the method is better if it is long, but it should correctly describe the activities performed in the test, rather than short and meaningless.

Let's go over what our test includes. After sending a POST request to / posts with title and body data, we expect a server response with a status of 200 and that after downloading all posts from the database, they will be equal to one.

Unfortunately, when we rerun the test, the number of posts in the database will be 2 (post from the previous test and the current one). So we need to clear the database every time we finish the test. Note that we have already imported the appropriate trait at the top of the file:

use Illuminate\Foundation\Testing\RefreshDatabase;

We just need to use it in our class:

use RefreshDatabase;

So let's try to run our first test, we do it with the command:

./vendor/bin/phpunit --filter a_post_can_be_created

Obviously our test will fail because we don't have routing, controller or model ready. So let's prepare all the necessary things and try to run our test again, creating the controller, model and routing.

Error handling

Sometimes an error will be returned when running the tests. Its content will not be very helpful and at first glance, it will be hard to understand what is the cause:

1) Tests\Feature\PostTest::a_post_can_be_created
Response status code [500] does not match expected 200 status code.

To get more detailed information on the bug, we need to disable Laravel's error handling. So let's add this code:

$this->withoutExceptionHandling();

Now, after restarting the test, we will receive information that will be much more useful for us:

1) Tests\Feature\PostTest::a_post_can_be_created
Illuminate\Database\Eloquent\MassAssignmentException: Add [title] to fillable property to allow mass assignment on [App\Models\Post]

Changing an existing resource

Now let's create a test to check the possibility of changing an already created post. We add a new method:

    /** @test */

    public function a_post_can_be_updated()
    {

    }

When running each test, we start with a clean database that does not contain any data. So the test must begin by creating a new resource and then trying to change it.

/** @test */

public function a_post_can_be_updated()
{
    $this->post('/posts', [
        'title' => 'Post title',
        'body' => 'Post body',
    ]);

    $this->assertCount(1, Post::all());
    $post = Post::first();

    $this->patch('/posts/' . $post->id, [
        'title' => 'New title',
        'body' => 'New body',
    ]);

}

We need to check that the title and content have changed. To do this, we just need to download the first (and only) post and compare its content:

$this->assertEquals('New title', Post::first()->title);
$this->assertEquals('New body', Post::first()->body);

It's time to run the test and see what the result will be:

./vendor/bin/phpunit --filter a_post_can_be_updated

Again, the answer that gets returned isn't very helpful:

1) Tests\Feature\PostTest::a_post_can_be_updated
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'New title'
+'Post title’

However, now we know what to do in this case. So let's turn off error handling and run the test again. This time, we should receive information about the lack of proper routing. According to the TDD methodology, we should create a routing and a method in the controller to save the changes to the post. If we have done everything correctly, the test should be successful.

Removal of a resource

We will start the test very similar to modifying the resource. Due to the fact that there are no records in our database, we must start the test by adding a new post in order to be able to test its removal. So let's create a new method.

 /** @test */

public function a_post_can_be_deleted()
{
    $this->withoutExceptionHandling();
    $this->post('/posts', [
        'title' => 'Post title',
        'body' => 'Post body',
    ]);

    $this->assertCount(1, Post::all());
}

Then we send the appropriate request and check again if the posts table does not contain any records this time.

$post = Post::first();
$this->delete('/posts/' . $post->id);
$this->assertCount(0, Post::all());

Of course, the test will fail this time as well. This is what TDD is all about. Now is the time to create the appropriate routing and a method in the controller that will handle this request.

Test grouping

When there are more tests, it is worth grouping them. In the future, you won't have to run them all at once or manually start each one separately. To do this, add @gorup in the comment above each method. For this article, the tests are called: post_test_group. To run tests from the group, enter in the terminal:

./vendor/bin/phpunit --group post_test_group

Summary

TDD detects possible errors in the application much faster than in the normal mode of operation when the tests are written at the end. We save a lot of time on possibly improving the code, and fewer people are involved in the whole process. However, it should be remembered that TDD will not work for small applications, and requires additional skills from the development team, which are handled by the QA group.

At Droptica, we have many years of experience in providing PHP development services. Please, contact us if you want to learn more about Test Driven Development. Our Laravel developers will be happy to assist you.

And if you are interested in Laravel, I encourage you to read my article, in which I discuss the subject of intuitiveness and quickness of writing code using Laravel.

3. Best practices for software development teams