Why bother testing tho?
Code, like everything else in the universe, is subject to entropy. Code has a unique proclivity to decay literally overnight. Keeping up a project that does more than one trivial thing bug free is difficult, time consuming, and stressful, especially when requirements change and engineers switch hands. This is because all code is bad. Peter Welch wrote a phenomenal article on this topic here. Thankfully, writing tests will make this less painful.
With tests, we can save time for ourselves and a lot of pain. Tired of getting calls in the middle of the night when code fails somewhere? Run your test suite and see what happened. Wrote something months ago and forgot how it works? Run your test suite to see what your code is doing. Added a new feature but not sure if it broke anything else in your app? Run your test suite to see if it did. Good testing is a lot like putting money in savings. It kinda sucks right now because you can’t use it, but you’ll be so glad you did later.
Testing can be anything from a nice to have to critically important. If your code is written for an Reddit bot, people probably won’t notice if it goes down for a few hours. If your code is for a pacemaker, it’s vital that it’s well tested.
Unfortunately, writing tests is extra work. Sometimes, testing feels like you’re writing code twice. It requires time, and a lot of thinking about edge cases, before you’ve even written anything! Yikes. Before you start coding, check out these tips to make writing tests less stressful, no matter what language you’re testing in.
1) Use a paper and pencil
This one is first because it’s honestly the most important. Before you write a single line of code, it’s most important to familiarize yourself with your problem. Break the problem down as simply as possible. Ideally, each section should be broken down into the functions you’ll actually be writing.
Example: We need a web crawler to alert us every time our favorite shoes drop below $50 on Amazon:
- We’ll need to connect to Amazon
- We need to connect to our shoes page
- And receive a successful response
- We parse the response for the price
- We check that the price is a valid number
- We check the price < $50
- Alert the user
- Our app can send notifications
- Our app sends a notification on a successful response only
- We handle errors gracefully
This helps us in two ways. First, we can write all of these as test stubs before we actually write any code. Second, it helps us see what we need to write in the first place.
2) Set Up, Call It, and Get Out (SUCIGO)
Another important principle of TDD is that you don’t wan’t to be writing code twice. Unfortunately, test code suffers from entropy as fast as regular code does. The easiest way to prevent this is to make your tests dumb simple. Set up the state you want, call the method with that state, and get out ASAP. If our state is large and hard to follow, consider changing your function signatures to only accept slices of state.
These two functions do the exact same thing. The main difference is the input. We can easily manipulate the state of
easy_state_add_5() but manipulating
hard_state_add_5() will be harder to do.
3) Arrange, Act, Assert
Similar to tip #2, we want to prevent having to go back and rewrite tests as our codebase evolves. By using the Arrange, Act, Assert method, we can keep our tests simple. This is hard to follow in reality and many times your object under test will be complex. Due to this, multiple assertions can make sense. But the tradeoff is the test will become less clear as to why it failed.
Generally, single assertions are easier to track in the same way that small functions are easier to read.
4) Be lenient with your test cases
Tony Hoare went on record saying that inventing the null pointer was his biggest mistake.
“I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.”
We want our tests to be explicit and straightforward, but we don’t want them to be bottlenecks for development. Make sure to reach all edge cases, but don’t attempt to test code you didn’t write. This includes third party responses from other APIs, databases, and more. Most testing frameworks include ways to spy on and mock calls, so focus on harnessing those skills first. Then you’ll be able to focus just on the computation you wrote, and when errors come up you’re able to quickly deduce if it was because of your code or some extraneous error.
5) Mindset, Mindset, Mindset
The final tip is more meta than the rest, and can’t really be acted upon directly, either. If coding requires critical thinking about the process, testing requires critical thinking about the result. The next time you go off to war with your requirements in hand, different problems and solutions battling for your attention, take the extra time to think about what your goals are, and what tools you’ll need. This is analogous to walking into a tool shed and looking at your choices before you just dive into it with a drill and a prayer.
Testing is about what our programs should do, not about how they should do them. By familiarizing ourselves with different questions, we find different answers. This mindset makes our future engineering efforts more efficient as well.