Jaime Lopez

Should you write automated tests?

Should you write automated tests?

tl;dr

Yes, you should.

But why?

I don't come from a CS background, so when I first started developing, I would just modify the code directly on production.
This was bad.

My process evolved as I grew in the industry. I began using dev environments and stage servers, and a QA engineer would test features on stage and production. A great improvement, but there was still a hole in this process. It's not feasible to test every feature, every time. When your new feature breaks a seemingly unrelated feature, you aren't going to notice until it's too late.

Enter test-driven development (TDD)

There are some great resources and tutorials online explaining what test-driven development is, and how to approach it for the language/platform you're developing for. The idea is to write code that automatically black box-tests your features. In a typical test, you would expect to:

  • Define a given scenario
  • Provide inputs or interact with the feature
  • Expect a specific output from it.

This is awesome! Now, when you create a feature, you write a test for it. It is always available to you, and you know you're good if it passes. If it fails, you either broke something or you changed how the system works, and you should update your test.

Changing the paradigm

Since I've been writing tests, my process changed. Now, I write tests before I write the feature, and outline how I expect my feature to succeed or fail. I would expect to see responses and their status codes, applicable validation messages, and data persisting on the database.

Having a test-driven mindset helps when developing internal application processes, such as a queue or scheduled cron job. While manual testing has its place, other developers can also understand and review your tests, avoiding complicated testing instructions and cumbersome setups.

It can't be all good, right?

One caveat is that writing tests (like all code development) takes time. Incorporating TDD into your project could double or even triple development time--precious time that could be used to develop another feature.

Some managers don’t initially see the cost to time benefits of TDD, and neither do clients, who often understand even less about how the product works. Resistance is normal, but be ready to present your case for writing automated tests. In the long run, having less down time and frustrations from unexpected bugs is worth the time expense.

Let's see an example

Our Haystack application comprises multiple components, and each of these components are tested. For example, I’ll show you an API integration test. The API is written using the Lumen PHP framework.

Our API keeps track of multiple things, including users and teams. The API endpoint to get a specific team is /teams/{id}. The following controller responds to this call

public function show(Request $request, $id)
{
  $team = Team::with('users')->findOrFail($id);

  if($request->user()->cannot('view', $team)) {
    throw new AccessDeniedHttpException('Not authorized.');
  }

  return $this->response()->item($team, new TeamTransformer);
}

Even though the controller is short and efficient, there's a lot going on here. First, we call the findOrFail() method to see if the team really exists. If it doesn’t exist, an error is returned without the rest of the code running.

To create a test for a feature, I normally go through a mental Gherkin exercise, which describes a test with its setup (Given,) the action I perform (When,) and what I should get back (Then.)
In this specific case

Given I am a logged in user
When I make a get request for an non-existing team
Then I should expect a 'not found' answer

And in code

public function testFailTeamNotFound()
{
  // Given I am a logged in user
  $token = '?token=abc123'
  
  // When I make a get request for an non-existing team
  $endpoint = '/teams/0';
  $response = $this->json('GET', $endpoint . $token);

  // Then I should expect a 'not found' answer
  $this->seeStatusCode(404)
       ->seeJsonContains(['error' => 'Resource not found.']);
}

I give it a meaningful test name so it is easy to maintain and understand in case it fails. In the method I have a token for an authenticated user as my setup. I make the JSON request to the /teams/0 endpoint, knowing that team 0 doesn't exist, and expecting to see a 404 response with a specific message.

After I successfully run and pass my test, I move down through the controller code. I test each possible failure until I get to the return, where I test for successfully receiving the team I requested.

Keep in mind that you should never run tests on a production environment, as they could compromise your database.

Closing notes

  1. Tests are sweet!
  2. You get a strong layer proving that your code works.
  3. Writing tests will quietly improve the quality of your code.
  4. Tests will change the way you approach your code.
  5. Write tests before the feature--you already know how your feature should behave.
  6. When you’re considering packages or libraries, you will often see tests included. This reassures you that that package you're about to install has been proven to work.
  7. TDD knowledge is a great resume-building skill.