I’ve been doing Test Driven Development for nearly 10 years and I’ve spent most of that time doing TDD with C++. There’s a bunch of things I learned early on that I still use heavily today. And some patterns I’ve come to value over time. I’m using C++ examples but I’ve applied the same thinking in Python, PHP, Javascript and Lua.
I will also include some warning smells along the way. Things that will give hint your design is not testable.
1. Dependency Injection
This is one of the first things you’ll learn in doing TDD. It doesn’t matter if you’re doing London Style or Chicago Style you will eventually hit cases where you need to swap out the real item. It may be because that real object would touch the file system, a database or a network. Or it may be because it forms a convenient seam between your code and something more complex.
Smell: Singletons and classes that new their dependencies.
2. Wrap and Mock
Those seams between your code and the broader system become a place you need to carefully manage and control. It typically is where the barrier between tested and untested will form. Sometimes that means you’re dealing with a framework or library that you don’t have a way of pulling into your tests without making them slow or complicated – tests should execute in milliseconds. So you will create a thin wrapper for that external system so that you can replace it with a mock version in the tests.
With this example you can see I’m able to also use that seam to push out any implementation specific details out of the rest of the code. It doesn’t need to know SQL or how to establish a connection. It just knows this is a way to get a list of names. That allows my Mock to also remain simple.
Smell: Complex mocks
3. Builder Functions
All these dependencies and wrappers will lead to complex construction. So I create builder functions that hide that complexity from the rest of the code as well as minimise the impact when it changes. I also create builder functions for my unit tests where I find that construction process is being repeated a lot. I don’t write these up front but allow them to be discovered with code duplication and created as part of refactoring.
Smell: Preemptive refactoring. Let the code issues reveal themselves first.
4. Assert Outcomes not Interactions
In the above example I’m asking the mock what the saved data is. Those used to mocking libraries would often test that a save name function got called with the correct data passed in. That can cause issues as those interactions change over time and that happens a lot with relational databases as different parts may need to be saved in specific orders. It also means that your test, which is testing a name validator, needs to no details about how the saving works. But it shouldn’t care, it should only care that it uses it and gets the right result.
Even when you’re dealing with complex mocks they are usually trying to represent a system that has some sort of state. Try and make it simulate that state in a simple way rather than be an empty shell that only records what functions were called. I tend to call these Fakes instead of Mocks. A Fake database would keep a representation of the data it is storing. A Mock database only remembers interactions and needs to be specifically programmed for data it can return.
Smell: No Fakes. Only Mocks.
5. Testing Behaviours not Functions
You should be writing the tests first. So because of that you should be thinking about the new behaviour you want first. A behaviour is not the same as a function. A function may do things like open a file, connect to a database, close a socket. But these are things computers need to do that people do not care about. So writing tests focused on that behaviour absent anything else of value doesn’t make sense.
It’s a strange thing that I can look at code and make a reasonable guess if someone was using TDD when writing it. It’s not the code coverage. It’s not the test quality. It’s the function implementation. I see weird little side effects and complex interactions that make sense only if you care about the small low level details that the test doesn’t see.
Smell: Functions that serve no value to the code that calls it.
6. Use the Real Classes as Much as Possible
Mocking and fakes is an important part of TDD and software testing in general. But I avoid it. This comes down to how I learned TDD which is now sometimes described as the Chicago style. Why that style isn’t more common is beyond me since that’s where Extreme Programming comes from so all the XP founders practice Chicago style. London style is still useful. London style is where you mock everything that is not the system under test. Typically in London style TDD the system under test is a single class. But single classes often don’t define a coherent behaviour. Classes are often broken up as part of refactoring and it doesn’t always make sense to break up the tests too.
So you should learn both Chicago and London style. But you should write enough mocks to keep your tests fast, simple and free from outside systems. But no more. That still is a very fuzzy line to draw because one person’s simple is another’s complex interaction. For me it comes down to the construction. If I can use the real class with a builder function – for tests or for production – then I use it. If it looks like a lot of work managing all the dependencies that class will pull in then I’ll mock it out. I’m okay with either but I much prefer the first.
Smell: Every interaction is mocked. Do your tests actually test anything of value? Are you highly dependent on higher level integration tests? Do those higher level tests even exist?
7. Test all Those Errors
The above is a dangerous place for code to be at. What should that else clause do? Right now it does nothing and the rest of the code will try and move on. For some people they’ll leave it at this point and risk creating huge problems later on. Others will add the logging or error conditions but not write the test first. I do TDD as much as I can. So I do TDD for the failure cases too. It’s not hard to build a logging system you can test or mock out. It’s even easier to catch exceptions and test that they are the right type and contain useful data.
The above code might be tricky to test if your logging methods are global static functions. Much better if it’s an object that is dependency injected.
Exceptions are one of the easiest things to test. Sometimes in TDD I will write the error case first because it’s so simple to test and even easier to implement. I can just write one line to throw the exception.
Smell: No tests on exceptions or logging.
8. Delete Tests that no Longer add Value
When doing TDD it’s pretty normal for me to end up with tests that look like this
At this point I’m not finished. I’m only three tests in. But it’s at this point I’ll go ahead and delete that first test. Why did I create a test like that? It’s a simple test that fails. I fails with the compiler but that’s still a failure. I want to show that I can construct a Calculator. Why delete the test if it had value? It’s being tested already in the other tests. The first line in each does the same behaviour. I don’t need that duplication. Why not delete it as soon as the second test was working? I could do that. But there usually isn’t enough duplication at that point for it to bother me. I’d probably go a step further now and the third test to create a fixture that constructs the Calculator for me.
Smell: Tests kept because you value having a test count more than what it tests.
Keep on testing and keep on practising! I’m still learning TDD.
Geoff.