You Know How to Test. But What About When and What to Test?

You Know How to Test. But What About When and What to Test?

·

5 min read

Table of contents

No heading

No headings in the article.

This piece was inspired by Episode 121 of the Working Code podcast. There are a lot of arguments made for and against testing, but the gist is the the Protagonist, Ben, is frustrated when he encounters some poorly-written unit tests and then asks, essentially, "what parts of code deserve unit tests and what parts don't?"

Don't Test Other People's Libraries or Products

Let's start with an easy one -- anything that's not part of your code doesn't need testing. Importing libraries? Using a database someone else wrote an API for? Don't test the library; the people who wrote that library should be testing it. Same goes for the database (though you may want to test queries) or the API for that database.

Do Test Mappings and Mutations

The core of what you're doing in most programs is making a change to some state, and then displaying the data. So start testing the most basic functionality in your code:

  • Whenever you transform any data: given some expected input, do you receive the output you expected?

  • Whenever you map over any data: is all your data displayed and formatted properly? Did you lose any, or is all of it present?

Side Effects

There's a lot of discussion about side effects and whether or not they're appropriate. While some languages and situations are neatly predisposed to pure functions, in others side effects are unavoidable. Nevertheless, I will make two points:

  1. Pure functions are easier to test, and

  2. If you DO have side effects, you need to test them

This is a great time to use a Spy. If the side effect is a call to another function, verify that the other function is in fact getting called. You can also verify what arguments it's being called with.

Here's an example. Given the following function....

function isLongerThan5(input: string) {
    call anotherFunction(input);
    if (input.length > 5) {
        return true;   
    } else {
        return false;
    }
};

There are 3 things you would want to test:

  1. Does anotherFunction always get called with the function's input?
it("should always call anotherFunction with the same input", => {
    const sampleInput = "doesn't matter"
    // initialize a spy
    const spy = anotherFunction.spy()

    // call the function
    isLongerThan5(sampleInput)

    // verify that it has been called
    expect(spy).toHavebeenCalledWith(sampleInput)
})
  1. Given an input string longer than 5 characters, will the function return true?
it("should return true if the input is longer than 5", () => {
    const sampleInput = "longer than 5"            
    expect(isLongerThan5(sampleInput)).toBe(true)
})

3. Given an input string shorter than 5 characters, will the function return false?

it("should return true if the input is longer than 5", () => { 
   const sampleInput = "sh" 
   expect(isLongerThan5(sampleInput)).toBe(false) 
})

Do test negative behavior

What happens if your code fails? Will it crash, or be handled gracefully? Write tests to verify that the correct errors are returned if, say, the client loses internet access or if a user’s login credentials fail.

Test your tests

One of the most valuable things tests can do for you is tell you when you accidentally change the behavior of your application. Run your tests regularly to verify that core functionality is still intact while you’re rewriting part of your code base.

However, In order to gain the benefit of knowing when you’ve broken functionality, your tests need to fail when that functionality is broken. So after writing your tests to make them pass, try making them fail: either invert the assertion (expect “true” instead of expect “false”), or change the data to something that shouldn’t work.

Once it’s failed, change it back and know that if it falls in the future, it’s because you broke something you shouldn’t have. and if you change it on purpose in the future, update those tests to match the new expected behavior.

Don’t test your test data

It’s very easy to fall into the trap of wanting to verify everything, and it’s also easy to end up with tests that inadvertently assert that data should equal itself. Sound like circular logic? It is. You should learn to recognize circular assertions so you can avoid them. Something like

// do NOT do this
it("should test sample data", () => {
    const sampleInput = "some string"
    expect(sampleInput).toBe(“some string”)
    expect(typeof sampleInput).toBe(“string”)
})

Is useless because it will always be true, and doesn’t actually test any functionality of your application.

How much test coverage is enough?

Chelsea Troy wrote a great article about the pitfalls of blindly following metrics like required test coverage. I won’t repeat those arguments here, but I will add one suggestion: test every expected behavior. Maybe not every line of code, but definitely use a code coverage tool to see which lines of code aren’t ever being hit by your tests. Then decide whether they contain additional logic (say, a conditional check) that needs to be verified and the functionality safeguarded against unexpected changes.

Learn from tests

When you first start coding, it's often hard to think about what needs testing. It means you need to think clearly about every expected behavior, and what can go wrong along the way. Embrace this challenge: use your tests as a roadmap for your features.

I'm not a big proponent of test-driven development (mainly because requirements often aren't fleshed out enough to understand what the expected behavior of an application is until it's been written), but I would recommend writing out what you want your tests to do as soon as you finish writing a feature. That will help you think through what's missing.

You can also use tests as an easy way to learn about the expected behavior of someone else's code: what is this feature? What does it do when it works? How does it fail? Are there any pieces of the puzzle I'm missing here? Ask yourself these questions, and reference the test suite as you work your way through their code.