Question

I'm having troubles with using the right workflow with TDD. Some people say we should design before writing any code, some say we should make a test, make it pass, then refactor the code and that should help our thinking to find the right design.

But everyone say you should have 10 times more unit tests than integration tests.

I'm comfortable starting by doing an integration test that'd do the whole user story, and then refactoring. But in the end I have almost only integration tests, and almost no unit tests.

Here's an example of the workflow I use :

Feature : A user should be able to register through the API at /register/, passing the email and a password.

First I'd do an integration test :

<?php

class RegisterUserTest extends TestCase
{
    /** @test */
    public function user_is_registered_when_email_is_valid()
    {
        $email = 'test@soft.com';

        // Given the email was not registered before
        $this->notSeeInDatabase('user', ['email' => $email]);

        // When we post a request to /register/
        $this->json('POST', '/register/', ['email' => $email]);

        // Then we see the user is now registered
        $this->seeInDatabase('user', ['email' => $email]);
    }
}

Then I'd write the code to make this test pass (I'll only show the controller code) :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;

class RegisterController extends Controller
{
    public function registerUser(Request $request)
    {
        (new User)->create(['email' => $request->input('email')]);
    }
}

Now that's enough to make the first test pass, so I'll stop there and make another test to prove that when the email is not valid it should not be inserted in database, and it should return a 422 response code :

<?php

class RegisterUserTest extends TestCase
{
    //...

    /** @test */
    public function invalid_email_returns_a_422_and_user_is_not_registered()
    {
        // Given the email is invalid
        $email = 'invalid';

        // When we post a request to /register/
        $this->json('POST', '/register/', ['email' => $email]);

        // Then we see the response code is 422
        $this->assertResponseStatus(422);
        $this->notSeeInDatabase('user', ['email' => $email]);
    }
}

Now I'd write the code to make it pass :

public function registerUser(Request $request)
    {
        $email = $request->input('email');

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            return response('Invalid Email', 422);
        }

        (new User)->create(['email' => $email]);
    }

Now to me that's already too much logic in the controller so I'd refactor obviously :

public function registerUser(Request $request)
{
    try {
        $user = new User;
        $user->setEmail($request->input('email'));

        $user->save();
    } catch (InvalidArgumentException $invalidArgumentException) {
        return response($invalidArgumentException->getMessage(), 422);
    }
}

You get the idea. I didn't have to do any unit testing there, my integration testing work fine and are enough there. I could go on and on without any needed unit testing.

So how/when do you start unit testing ?

Was it helpful?

Solution

It can be a bit overwhelming trying to come up with the right balance for your project. I'd like to help you with a framework of how to put it all in place.

It starts with the customer

Your product owner, or customer has a new feature they want in place. They do their best to describe what it means and you believe you have a good idea. At this point you start thinking of how to incorporate that into the project.

  • If you do BDD, your team would write up the specifications in the Given/When/Then statements. Once your client is happy with those statements, your test team would start getting busy making these executable.
  • Do some high level design to decide what pieces have which responsibilities in your application. You don't need to go into minutia, but you do need a good idea of what needs to be done.

A little bit of both Bottom Up and Top Down

Go ahead and write one integration test. Start with the "happy path" (i.e. what is supposed to happen when there are no errors). It's going to fail because nothing is written yet. That's OK.

As you work on specific units of code that will in turn be needed to make your integration test pass, write unit tests. Don't change production code without a unit tests that tests the implementation. Only write enough to satisfy your integration test.

Now you can write your next integration test, and work on unit tests as you go. With a good design, you'll need fewer new unit tests to handle each new integration test.

An analogy

When I was learning about how katanas were made and maintained, the way that the person who polished the blade worked fit this scenario very well. The polisher always examines the whole blade when he is working on one small part of it. The goal is to make sure the geometry of the blade and the level of polish is consistent from the base to the tip.

Your integration tests are like picking up the blade to examine the length of it to make sure what you are doing is consistent with the design of the blade.

Your unit tests are like working locally to fix the imperfections in one small area.

They work together.

OTHER TIPS

I think your question is more about when do a integration test instead of a unit test.

Now that's enough to make the first test pass, so I'll stop there and make another test to prove that when the email is not valid it should not be inserted in database, and it should return a 422 response code

I think you could stop here with your Integration Test.

A good practice that I used is do the "happy path" with a integration test, just to be sure that all my code (Services, Controllers, Repositories, Entities, database, etc) are working well together for the most basic case. This guarantee that the dependency injections are OK, that any unexpected data are being save on the database, etc.

About the e-mail that is not valid, you could create a unit test for this case, testing different e-mails and seeing if the expected e-mails are OK and the wrong ones are returning false or a exception (depends of your code).

If your concern is that the invalid e-mail cannot be save on database, you can't verify this with an unit test too. Per example: you can mock the return of the e-mail validation and verify if the save method was called or not.

To do TDD in this scenario, you start creating some class what will be responsible for this two (or more) things: call validation and save.

Licensed under: CC-BY-SA with attribution
scroll top