Improve Your Testing Suites Readability

Dev Ops
"Whenever you are tempted to type something into a print statement or a debugger expression, write it as a test instead."
Martin Fowler - Developer & Author

Why It's Important To Have A Clean And Easy To Understand Testing Suite

The readability of tests are often overlooked, after all, they are simply there to tell us if something is working as expected right? Well, sort of.  Our tests serve multiple purposes...

  1. Prove correctness (tell us our code is working as expected)
  2. Allow for confident refactoring
  3. Serve as documentation

Take a look at your testing suite, think to yourself... If I come back to this project in a months time and decide to refactor this function, which then causes my tests to fail, could I read the tests that are failing and still be able to deduce what I broke? A well written test suite would certainly be able to do this. 

Although these guidelines will seem arbitrary, you will almost certainly feel the value of them in the future when you need to rely on your tests for refactoring and you no longer have the context in your mind when coming back to a piece of code in your application.

Remove Unnecessary Detail from Tests

What do I mean by unnecessary detail? Well, take a look at this test...

public function test_users_name_is_saved()
{
    $user = User::factory()->create();

    $this->patch(route('users.update', $user), [
        'name' => 'This is the new users name'
    ]);

    $this->assertDatabaseHas('users', ['name' => 'This is the new users name']);
}

This test simply creates a user with the user factory, and then attempts to update the users name using the users update route. So what are we actually testing here? We are testing that one string is equal to another... does the content of the string really matter? The answer is no.

In this example all we need to do is ensure two strings match, so we reduce 'This is the new users name' to literally '::name::' - this tells the reader that we don't care about the content of the string, all we care about is if the two strings match in the end.

public function test_users_name_is_saved()
{
    $user = User::factory()->create();

    $this->patch(route('users.update', $user), [
        'name' => '::name::'
    ]);

    $this->assertDatabaseHas('users', ['name' => '::name::']);
}

Caveats

  • Doesn't work with all types (i.e. integer, boolean, object...)
  • Emails can't follow this logic.

Doing this throughout your whole testing suite will enable you to clearly point out what is relevant in a test and what isn't. Lets compare this with a test where the content of a string is relevant and we want to show the to our future self.

In this example we have a class Action, which allows us to add a description as a property and then use the renderDescription method to return the Blade rendered string. We are testing that adding a variable that doesn't exist to the string would throw an exception.

public function test_users_name_is_saved()
{
    $user = User::factory()->create();

    $this->patch(route('users.update', $user), [
        'name' => '::name::'
    ]);

    $this->assertDatabaseHas('users', ['name' => '::name::']);
}

public function test_invalid_description_throws_view_exception()
{
    $action = new Action();
    $action->description = '{{ $variable_that_doesnt_exist }}';
    
    $this->expectException(ViewException::class);

    $action->renderDescription();
}

Here the the content of the string passed to the description property actually matters and we have shown this my the content of the string being something you would stop and read if the test fails in the future.

Skip Test That Will Fail

Some tests need to assume that some other test didn't fail to continue, for example, if we are testing an event is fired when a user registered and the user registration fails to create the user, our test in turn then fails due to no user being created, thus no event being fired. The question is, months down the line when the context is no longer in your head, would you know which failed test was the one that actually needed fixing? We know the registration event isn't firing but that has nothing to do with the event which is what we are testing.

public function test_users_can_be_created()
{
    $user = User::factory()->raw();
    $this->post(route('users.store'), $user);
    $this->assertDatabaseHas('users', [
        'email' => $user['email']
    ]);
}

public function test_user_creation_event_is_dispatched()
{
    Event::fake();
    $user = User::factory()->raw();
    $this->post(route('users.store'), $user);

    if ( ! User::where('email', $user['email'])->first()) {
        $this->markTestIncomplete('Failed to create user, skipping.');
    }

    Event::assertDispatched(UserCreatedEvent::class);
}

This allows us to better determine the reason our feature is failing, rather than having fifteen failed tests, we have fourteen marked as incomplete and one failing.

Split Test Suites

As your application grows, so do your tests, eventually you can to a point where you need to test one particular module of your application. Having multiple suites allows you to quickly test that module after some refactoring or extension of your application.

With PHPUnit we achieve this by adding new suites to the phpunit.xml config file.

<testsuites>
    <testsuite name="Unit">
        <directory suffix="Test.php">./tests/Unit</directory>
    </testsuite>
    <testsuite name="Unit-Users-Events">
        <directory suffix="Test.php">./tests/Unit/Users/Events</directory>
    </testsuite>
    <testsuite name="Feature">
        <directory suffix="Test.php">./tests/Feature</directory>
    </testsuite>
    <testsuite name="Feature-Users">
        <directory suffix="Test.php">./tests/Feature/Users</directory>
    </testsuite>
</testsuites>

This config setup would create four testing suites, by running our test command all four of these suites will be run back to back, however we can execute each individually by specifying the suite we wish to execute. We would test the User Events unit tests by executing test --testsuite=Unit-Users-Events

Copyright © 2024 | bonnick.dev