“We’ll get the interns to write our tests later.” That quote was from a manager explaining his team’s testing strategy during a product udpate. No one else in the room or on the call blinked.

I was shocked. I had a feeling that my company wasn’t great at testing. It was obvious from the number of outages across our products at any given time that testing wasn’t a priority. To hear a manager, however, publicly say that the team’s current engineers, the ones who were writing code that was being deployed into production, would not be the ones writing tests–I mean, major WTF?!

While I love tests, I don’t always enjoy writing them. Test code is not sexy. In fact, it’s usually pretty repetitive. Write a bit of code to perform setup tasks; write a bit of code to perform the test; write a bit of code to tear down the test.

That said, I cannot imagine life without tests. Why?

Tests breed confidence.

A project doesn’t start out with perfect test coverage–“perfect” meaning 100% coverage. (In fact, 100% test coverage shouldn’t actually be the goal.) But, from the beginning of a project, it should start with some tests, covering perhaps 30-40-50 percent of the codebase. Overtime, as the project grows, new features are added, and bugs are uncovered (in production undoubtedly), you add more tests. Maybe, test coverage hits 100%, but if it gets to 80%, 90%, that may be good enough.

What the tests provide isn’t a badge for you to display to the world. “Look, my codebase has 97% test coverage!” Rather, tests provide you with confidence to make changes–sometimes radical changes. Lets look at an example to demonstrate.

We can use the example to calculate a factorial from here, which I found by doing a search for “iteration vs recursion”.

# recursion
def factorial(n): 
    if (n == 0): 
        return 1; 
  
    return n * factorial(n - 1);

To write a test for the above function is straightforward.

import pytest

class TestFactorial:
    def test_factorial(self):
        f = factorial(5)
    assert f == 120

Then run the tests:

pytest test_factorial.py
===================================================== test session starts =====================================================
platform linux -- Python 3.8.2, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /home/tjb/personal/personal-blog/examples
plugins: cov-2.10.1, timeout-1.4.2
collected 1 item                                                                                                              

test_factorial.py .                                                                                                     [100%]

Now, lets say a month goes by, and I learn that instead of using recursion, I can do the same thing with iteration. I change the implementation of the factorial function:

# iteration
def factorial(n): 
    res = 1; 
  
    for i in range(2, n + 1): 
        res *= i; 
    
    return res; 

And rerun the tests:

pytest test_factorial.py    
===================================================== test session starts =====================================================
platform linux -- Python 3.8.2, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: /home/tjb/personal/personal-blog/examples
plugins: cov-2.10.1, timeout-1.4.2
collected 1 item                                                                                                              

test_factorial.py .                                                                                                     [100%]

While this example may seem trivial, it still shows the power of testing. The test focuses on the input and output of the function being tested. Once I have the test and working code, I can change the implementation as I see fit, knowing that the test will tell me if I do something foolish. Now, the only way I can introduce a bug into production, is if I either don’t run the test before committing my change, or worse, run the test but ignore any failures.