What code of yours isn’t tested? Is it code that deals with complicated situations that you don’t control? Threads, running commands, git, networking, or UI?
Our apps are most interesting when they’re complicated. They’re also most dangerous. And that’s why code that’s hard to test is exactly the kind of code that needs to be tested well. That doesn’t always happen.
Instead, every time you touch that code, you touch lightly. You tread carefully. Maybe you do some manual testing. And when you send the pull request, you hope your teammates don’t realize those tests don’t exist.
But that won’t make things better. You’ll run into the same problems, the same bugs, the same stress next time – and every time after that. How can you finally make those challenging tests something you can rely on?
Shift your mindset
The most frustrating thing about these tests? It’s going to take ten times as long to write it as it feels like it should. If you estimate the time the test saves you against the time you spend writing the test, it just doesn’t seem worth it.
But it’s not just about this test. It’s about all your future tests.
Most of the best-tested code I’ve seen has a lot of support. It’s not just the code in
test/models. Extremely well-tested code has fakes, it has mocks, it has a good set of test fixtures, it has configuration options specifically for the tests.
All that takes time to write and put together.
But once you have it, it feels so good. You can come up with test after test, feeling comfortable about your code, and confident in quickly you can move after the investment you’ve made.
You can rely on the work you’ve already done.
So it’s not just about preventing bugs in complicated code. It’s also about making future code easier to test, piece by piece.
Make it an integration test (for now)
Sometimes, though, it’s not about understanding the value – I get it. Instead, I just get stuck because I can’t figure out how to write a small, fast, unit test.
How do you know you’re running the right git commands in your deployment tool, without actually running
git? How do you make sure you’re sending a remote server the right headers?
With enough time, you can build a quality fake for your tests to rely on.
But when that seems like too much to think about, there’s something else you can try. Break testing apart into two separate steps: “test the code” and “write the mock.”
Just call that server. Just run that command. Why?
- It’s much easier to get started. You’re probably testing those commands manually, right? Running it in a console, or trying it in a browser? Just copy it into a test.
- When you eventually write your mock or fake, you can use these tests to make sure your mock works. If you see the same behavior in the real world as you see from your fake, your fake is probably good!
You probably don’t want to keep these tests around forever, though:
- They have all the problems integration tests have. They can be slow. They might need a live internet connection. They might be brittle, because they’re depending on behavior that your app doesn’t actually care about.
- You might not be able to test some things in the real world. For example, how do you force specific error codes when you don’t control the server on the other end?
- You might get blocked by a server you depend on, and that can break your tests (and your app!). This actually happened to me, and it was a big problem.
So, writing your test as a real-world integration test isn’t a permanent solution, or even a long-term one. But even with all those drawbacks, it’s still helpful. And after you replace it, you can still keep the integration test around, in a separate suite. That way, you can always check my code against reality, not just your assumptions.
Some code is just hard to test. It takes a while to build up the infrastructure you need to write reliable tests quickly. And a lot of the time, it doesn’t seem worth it.
But when you stop thinking about that single test, and think about the value of making all your future tests easier, testing complicated code becomes a lot more motivating. And once the first test goes down, the rest of them seem to magically become so much easier to write.
Sometimes, though, that’s not enough. What if you know how to make sure your code works in the real world, but just can’t figure out how to test it?
When that happens, stop looking at the test as something you need to keep pure and isolated. Instead, see it as a way to automatically do what you’re already doing manually.
It’s not perfect, and you should replace it as soon as you can. But those tests can give you the confidence you need to write and change complicated code quickly.