Writing Unit Testable Code

What Does 'Unit Testable' Mean?

Code that is 'unit testable' means that it has been written with testing its features in mind. Code that is not 'unit testable' is often called 'happy path' code which makes an assumption that all values and objects passed to it are valid and not null.

In school we all heard the old adage:

Q: "what's the first thing you do when writing a software program that peels apples?"
A: "check to make sure you're holding an apple"

That means checking:

  • Is the object null?

  • Does it implement the interface you require?

  • Is it an instance of the object you are expecting?

Taking it further, we need to utilize Laravel's 'implicit loading' feature as often as possible.

What Is Implicit Loading?

Implicit Loading is automatically loading an object based on the ID passed in the URI. eg:

/lotteries/{lottery_id}/stages/{stage_id} //this will not fire implicit loading

This will dictate that the controller's method looks like:

public function doSomething(int $lottery_id, int $stage_id) {
    //code here
}

The problem with this is we still need to verify that the lottery exists for the lottery_id passed in. We also need to verify that the stage exists for the stage_id passed in. that means we need to add this inside our controller:

Implicit loading will take care of the loading of objects BEFORE the execution of code reaches the controller and throw an exception if they are not found.

What developers often do is:

This is the happy path - assuming that everything is ready to save, not considering an exception that could occur saving to the database, and sending a success only response. This is not code that we can write unit tests for, apart from a basic unit test of: public function testSave_success()

We need to write software that puts the onus on the requester to prove to us that all conditions are pristine before we will consider the request. Following the example of 'save a stage to a lottery':

This will dictate that the controller's method looks like:

This has already proven to the code that the values passed back in the URI are valid rows in the database to work with. This is only the start though.

Other aspects to consider:

  • Does that stage map to the lottery or does it map to a different lottery?

We've only validated their existence in the database.

EVEN IF YOU AREN'T USING THE OBJECT IN THE CODE YOU SHOULD RELY ON IMPLICIT LOADING FOR YOUR VALIDATION CHECKS

We aren't needing the Lottery object simply to update the values of a stage. But we do need to ensure they map to each other:

Stage also maps to:

  • Jobs (submissions)

  • Status

These need to be verified before we attempt to insert into the database. The limited validation being utilized by Laravel only verifies that they are in fact valid integers and whether or not they are required.

That means our pseduocode might look like this:

Often there is more code trying to prove that a request cannot be honored that the code required to honor the request. This is good - this is to prove that the conditions are pristine to ensure that an exception does not occur during execution of the request, where values may be generated or registered in the processing of a request and then an exception occurs mid way through the request - leaving orphaned values in data stores unless proper transactions and rollbacks are implemented - another aspect of the software that is all too often not taken into consideration.

By checking for pristine conditions before the execution of code we mitigate the need for rollbacks (not all rollbacks can be done within a basic DB transaction).

Writing a Unit Test for Testable Code

Now that we've implemented as many possible checks for pristine conditions we need to test EACH of the IF statements (not just check the exceptions thrown).

Checking All If Statements

With unit testing we have to check all paths within our methods we test.

There should be a provable test for each of these conditions - that means 3 separate tests. Every line of code should have test coverage which means:

  • every line of comparison (if/else)

  • every exception

  • every method call

  • every controller endpoint

  • every service method ~ public, protected, and private

Writing Code That Is Testable

Separation of Concerns Avoid writing an endpoint that puts all the code into 1 method inside a controller. The controller is simply meant to determine what needs to be done, and should not do the work itself.

Pass the work off to one or more associated services to do the work. Even refactoring an endpoint to utilize services takes an average of 15 minutes to refactor, including writing the service, the interface, registering both of them, and then refactoring the code. It is not a very challenging task.

Separating each portion of a request into a service makes it easily unit testable. If a method is doing more than 1 task it should be refactored into a method for each task.

Sample of proving all conditions are pristine before handling request:

Matching Integration unit tests would be:

Even inside the injected service, further checks are performed:

Matching Integration unit tests would be:

Last updated