Every developer who has worked on long-term projects has come across common problems at some point while fixing broken code and asked:

  • “How and where in the codebase do I fix this?”
  • “What do these lines of code do?”
  • “Is this code still needed or used at all?”

And most importantly:

  • “How can I make sure that if I change this, nothing else breaks?”

And other similar hair-pulling questions that make you reconsider your choice of career.
Sometimes developers end up simply wasting hours trying to make sense of code that: has no documentation, makes no sense due to a lack of clarity in its purpose, has functions with a thousand lines of code, is completely unreadable and incomprehensible, etc. These are perks that come from cutting corners, or in this case, from “cutting” costs ( you will see ahead how this translates into practical irony).

I qualify these problems as common above, but they are by no means normal nor should they be taken lightly or, in my opinion, even be given the loose margin for taking place whatsoever.

Simply put, code that is automatically unit-tested will effectively translate in practical terms into:

  • more reusability;
  • refactoring made easy/clean code;
  • agile code;
  • robust code;
  • readable code;
  • Simplified debugging.

What is a unit test?
Unit tests are automated tests written (in code) by developers, each testing a particular standalone unit of code, separately from the rest of the application. One unit test is given control data for executing a particular scenario (the input of whatever code it tests) and expects a given output. If that output is met, the test passes.
A test scenario usually (but not necessarily) tests a single function/method (the unit).

Let me show you a practical example:

Here I have a class with mathematical utility functions, and in it, a function that computes the sum of a given amount of numbers.

class MathUtil
{
public static function sum ($values)
{
$total = 0;
foreach ($values as $value)
{
$total += $value;
}
return total;
}
}

Here I have a test case that tests said function. If the output criteria are met (the assertEquals instruction), given an arbitrary set of input numbers (the $values variable), then the test passes.

use pathtoMathUtil;
use PHPUnitFrameworkTestCase;

class MathUtilTest extends TestCase
{
public function testSum ()
{
$expectedTotal = 40;
$values = [1, 10, 20, 5, 3, 2];
$total = MathUtil::sum(values);

%this->assertEquals(expectedTotal, total);
}
}

You can do more than one verification per scenario as you are not limited to one assertion to validate your test. All verifications need to comply for the test case to pass.

Code that is unit-tested can be reused with more ease and confidence.
You’re using previously used code, so, optimally, it should be already working and duly tested.
Using already tested code will also naturally result in more modular code, which in itself is a core basis for reusability.

E.g.:
I have a retail sales platform. Emails are sent in various sales processes. I have already developed a function that sends an email when someone purchases an item, but I also need to send an email when a seller contacts me by message through the platform. I should reuse my existing email sending function and prepare it to function for every scenario where sending an email is required.

Code that is unit-tested can be refactored without fear of breaking other stuff. You will be rapidly given updates that the code works as intended or not, which means your latest changes must have broken something. If configured correctly in an integration environment, the tests can be run automatically and will inform you of any updates on the fly when you try to push your code. This prevents failing code to advance onto further stages of the software development cycle. With that in place, problems will be more easily identified earlier in the code development process.
Unit-testing code will guarantee that the already tested code will not stop working suddenly and without anyone’s knowledge (which is often identified only after it’s too late, making it hard to identify and fix the bug). When that happens, it is often left as-is if the impact of the broken code is minimal for fear of breaking other parts that work. It is pretty much like playing Jenga, which will result in stacked up zombie code. So keeping the code tested on the fly will also indirectly help to keep the code cleaner.

E.g.:
I bought an item from a sales platform. An email is sent from the platform to my account’s email address to register the purchase. My sendEmail function sends an email using, for instance, PHP’s native mail() function directly, and I have a test scenario called testSendEmail that verifies the email is sent.
I now instead want to use Symfony’s send() function from its Mailer library, so I refactor my function to implement it with send(). I run the test and the test still works. Nothing else should have been affected by this change. If for some reason it was, it’s traceable to this change.

Code that is unit-tested will make it easier to add, remove, change features and refactor code, as testing often results in more contained (modular) and less coupled (fewer interdependencies) code.

E.g.:
Product ratings are editable, but now I want them to be final (no longer editable). Removing the feature of editing a previously submitted rating for an article online should be linear, and there should be a function solely responsible for altering the value of a given rating. Removing that function and its function call on the codebase should also be linear. This results in test scenario(s) that are no longer needed to test updating a rating value, thus it can be removed too.

Code that is unit-tested allows for earlier identification and prevention of possible bugs or unforeseen scenarios from shipping onto production. It makes identification of edge-case scenarios happen earlier as well, and it is more resilient to new bugs by catching uncommon scenarios in advance.

E.g.:
Items are bought for a specific value, and by logic, several items are sold by the sum of their prices. Discounts can be applicable to items.
Item discounts are non-cumulative for each item, as it commonly is in most retail businesses.

If an item has a discount ‘buy two for the price of one’ and there is another transversal discount applicable for many items at once which encompasses that one as well, with an overall 30% discount, which discount will be in effect? One implies a per-unit value discount but you take two items in total, the other other is one in total but discounted to another value. Also, if you buy more than one, which discount applies in that case? There must be extra logic specified to address these sorts of cases.

Code that is unit-tested is usually self-explanatory and self-documenting of the features it tests, as there should be one testing function scenario for each function scenario we want to be tested, which often leads to better design and high cohesion code.

E.g.:
I have a function that computes the average ratings of the sale of an item. I have a test function that tests that function given an arbitrary set of ratings. Regardless of the implementation of the function being tested, the function that tests it is explanatory of what the tested function does, and will, under good practices, be reflective in its name of its purpose and what it tests, such as testGetAverage.

However, this getAverage function may have branched code execution in it, leading to different paths of execution and thus resulting in different scenarios. We might want to test each scenario differently as the algorithm has sub-variations, for instance, one that computes statistical averages excluding outlier values. For this, we might also have another test scenario for the getAverage function tested through a function possibly called testGetAverageMinusOutliers.

The function:

class MathUtilTest extends TestCase
{
public function static getAverage($values)
{
if (self::hasOutliers($values));
{
self::trimOutliers(&$values);
}

return array_sum($values) / count ($values);
}
}

The test scenarios:

public function testGetAverage()
{
$expectedAverage = 2;
$values = [1, 4, 3, 2, 1, 1]
$atualAverage = MathUtil ::getAverage(values);

$this->assertEquals(expectedAverage, actualAverage);
}

public function testGetAverageMinusOutliers()
{
$expectedAverage = 3;
$values = [1, 5, 3, 2, 60, 4]; // 60 is outlier
$atualAverage = MathUtil ::getAverage(values);

$this->assertEquals(expectedAverage, actualAverage);
}

Code that is unit-tested narrows down the debugging process optimally to just the latest changes. This prevents debugging through old, unmaintained, and leftover code to figure out why something broke. You would find yourself unnecessarily struggling to try and figure out:

  • When did it stop working?
  • Why did it stop working/what broke it?
  • Is the code I’m going through even still in use?
  • Who broke it?

The amount of time debugging code that traces back to the stone age should be minimized by having unit tests in place and properly configured on your Continuous Integration environment to run when pushing new code.

Here’s an overview of what it looks like:

This is the output of the execution of the tests with an indication of success. Each dot is a successfully passed test scenario. The green light is a good omen.

 

The build has been generated, which means it passed the unit-tests. Again, the green light is a good omen.

 

In the end, ensuring all of those advantages from writing test cases will translate into:

  • more ease to maintain the code;
  • better teamwork,
  • and mid to long-term cost reduction.

From my experience having worked in two completely opposite project environments, documented, well-tested and covered, correctly set up, clean and structured code always trumps undocumented, untested, chaotic and unstructured code often with zombie code in it.

These are the considerations I’ve come to adopt as guidelines for when I consider whether I should write test scenarios:

  • It is a core business logic method/function:
    If your application is a content manager, maybe the simple act of creating a content instance should be unit-tested.
  • It is a value computation (algorithm):
    If your application generates statistics, for instance, you should unit-test statistical calculations
  • It is an auxiliary or utility method/function:
    A function that validates an email address against a regex, for instance, should be unit-tested.
  • Core database queries (not too complex, which might require testing on an integration environment):
    If your application has a function that queries the database to get all users that were registered within a certain date interval, have it tested.
  • It’s a new, self-contained, well-designed feature with well-defined input, output and behavior definition:
    Your application has products for sale. You can now submit a feedback review message with a completed sale, which should be tested.
  • It has edge cases identified:
    If you can order items from a sales platform, then after an order is issued, you should define and test what happens if you decide to cancel/delete your account while the order is in progress.
  • It is a feature broadly used or critical to my application:
    If the success of your application’s processes rely on a simple task, such as dealing with scheduled actions, you should test that your scheduler module works correctly.
  • If something seems too complex to write a test scenario for, the code might benefit from refactoring into potentially more modular code, so you can identify what does what and test each particle individually. If it is not yet modular enough and not specific to one action only, testing it as is will yield incorrect, unpredictable, and untrustworthy results.

But writing tests takes time, time is money, and someone has to write the tests, which, more often than not, no one wants to, so how cost-effective is it?

How can it affect development and the product roadmap?

It is still a matter of severe controversy in the coding community and within revenue-driven projects, whether one should invest or not in writing test scenarios for the application code. It is my own opinion from my experience that at least it should always be heavily considered to do so in each particular project you partake in, and not adopted as a generic rule to do one or the other.

My experience tells me the one thing that should perhaps be discussed at all times at least is how much of the code coverage should the tests address. Project scope and size may hint into how effective and useful it will be towards project success.

It has been proven over and over by experience that the tradeoff of neglecting the “less motivating” work like writing tests is your project being doomed to die from a lack of maintainability, of extensibility and team motivation.

You get what you paid for as the saying goes—trying to save money by not “wasting” resources on something that is not in itself a part of the product’s core service and doesn’t bring in revenue, may prove rather expensive later when trying to fix issues on the project. And it will happen, because finding the cause and origin of problems will take place much later in time, take considerable effort, and that bug-fixing period will take a long time due to accumulated problems identified late. These are sometimes done by developers that are no longer the ones who developed the original code, which brings unfamiliarity into the mix of downsides as well.
More time spent on these tasks sets back goals for product delivery deadlines, which in place leads to the necessity of more manpower to keep up and manpower comes with costs.

Saving up first to face the costs later is, despite working for some, prone to cause problems much later in a project lifetime and will make it harder to predict:

  • when it will happen;
  • for how long it will last;
  • how long it is maintainable;
  • how heavy its toll on developing features versus fixing broken components (including assessing how developers will cope with stopping developing features to focus on fixing bugs).

Preparing for this and performing project estimates, taking into account early on the costs of the constant and continuous integration of unit tests, makes it less likely to derail your estimates, control, maintainability, extensibility, team motivation, and continuity. It also provides a sense of ongoing accomplishment, as it is more likely for new features to see the light of day, instead of customer complaints (and developers’ complaints as well, unless fixing broken stuff is your jam instead of creating new code).

If you ask me what I suggest when balancing the scales to try and figure whether you should invest in unit testing your code, I’ll say:

You either invest early or later on—either by having well-written test coverage to ensure a stable, maintainable, and extensible codebase of your application or when you’re forced to fix high-maintenance issues and perform expensive duct-taping after the fact.

Both can be costly, but I strongly believe one method is more stable than the other in the long term. The bottom line is that you should always consider testing, even if you do not cover everything, even if you’re only focused on the core features of your application. Clean, tested code makes for happier, highly-motivated developers.