When TDD goes wrong

hamed sheykhlou
4 min readJul 1, 2021

--

The main recommendation of TDD is to write tests first. unfortunately, even in some coding books and lectures, tests will be written after the main code. this behavior becomes an instinctive later time when we write other codes. but why we should write tests first? is full code coverage the final destination of TDD (and writing tests)? are there any problems and pitfalls with writing tests?

there are some pitfalls in writing tests and solutions that we can use by TDD recommendation.

This document is written based on Dave Farley fantastic presentation: https://www.youtube.com/watch?v=-4Ybn0Cz2oU

Liar tests

there is a function for calculating the tax that returns zero if the shopping chart price is less than a threshold. we wrote a test for it and tests will be passed. consider a test like this:

A lier test

this test wants to verify that if our invoice price is lower than a threshold, then our tax becomes zero. as you see, the test name is OK, but our assertion seems odd. this test will be successful for every positive value. later time if a developer, changes the calculate_tax function and breaks the threshold check section, we cannot find it in the test section and the code will deploy. for this test, we should change the name to the proper name or we should change the assertion method.

Solution: use pair programming or doing a serious code review mechanism.

Many steps of preparations

every test needs some preparation, some things like mocks, initial values, create objects, initial subject, and similar things. this preparation should be small as possible. for any additional step in preparation, maintenance of the test will become harder. also onboarding new team members become tedious and time-consuming.

Unit tests make low cohesion visible through the costs of test setup. Low cohesion increases the number of setup tasks performed in a test. In a functionally cohesive module, it is usually only necessary to set up a few different sets of test conditions. The code to set up such a condition is called a test fixture. In a random or functionally cohesive module, many more fixtures are required by comparison. Each fixture is code that must be written, and time and effort must be expended. Jeff Younker (Foundations of Agile Python Development, Apress, 2008, pages 141–142).

one question remains: what is the meaning of Many when we say many steps (because Many have relative meaning). in this case, many means more than we need. Any function has a specific task and any preparation that not directly related to that task is redundant.

Solution: Write the test first, and if you see much preparation, break the functionality.

Giant Test

There is one principle in the writing tests: Each test, only one assertion.

consider the following test:

In this test, we test our function's behavior and get full coverage. as you know we need multiple assertions for this unit test. But compare it with the following tests:

we extract assertions to separate test functions. and sure we have some boilerplate codes, but we get a Valuable prize: very good documentation.

Mockery

We use mocks to simulate an external entity that our code used it. for example, we mock databases, web services, classes, and similar things. when we want to send an email using e email service, we can mock that service and emulate the functionality of that service. so mocks are essential for some tests, but mock overuse smells a problem in our code: GOD-object!

consider a function like this:

we need to mock at least 3 services to be able to test this function (email service, DB service, log service). it’s obvious that we don't use the Single Responsibility principle in our function design thus we should break our function into multiple smaller functions.

Solution: write the test first, if you saw a similar problem, change your design.

Patching

we have a function that should check input data based on multiple checker functions and if there was an error, log it and return True or False. something like this:

writing some tests to check the output value of this function is easy and we can write it without a problem. but what about logging functionality. we can mock the logger function of the base library and done our task. but patching the base library makes our test high couple to the library implementation. in this situation, any change to the base library may break our tests.

another solution is to pass logger as input arguments to our function as a dependency (dependency injection). with this solution, we can separate our implementation from base implementation.

Solution: write the test first, to detect potential DI arguments.

(I’m not a native English, if there were some problems with my sentences, please say them to me. thanks)

--

--