Top 5 Mistakes You’re Probably Making While Unit Testing Your Own Code

Top 5 Mistakes You’re Probably Making While Unit Testing Your Own Code

There is a big chance you’re doing one of these mistakes and haven’t even noticed

All developers hate unit tests, right? That’s like one of those untold rules, if you’re working as a developer, your focus should be on your working code, let others who know less than you write the filthy tests.

I’ve seen a lot of developers with that mindset in my (almost) 20 years of experience. Heck, I know I was one of them at one point. But the truth is I learned from my mistakes, I eventually understood the role unit tests play within the context of a software project and I just want you to also reach that point.

I want to you enjoy writing tests, because yes, such a thing is possible. And to show you how little attention you’re paying to the task of unit testing, here are 5 mistakes you’re most likely making without even realizing it.

Testing other people’s code

This one is a classic, I probably say the phrase “you’re testing code that you didn’t write” twice a week nowadays. But it’s fine, it’s better to understand this one first since it’s the base mistake for bigger ones.

The key to unit testing is the word “unit”. First and foremost, your tests should be testing a unit of code. And what is a “unit” of code you ask? Well, that’s where the problem begins, it’s mean to be thought of as the smallest portion of your code that makes sense to test.

Essentially, you don’t need to be testing one line at a time, but considering a full function or method a unit of code, sometimes, can be too much. You need to walk that fine line and understand if there is a way for you to test less code and yet, accomplish the same goal. When the answer to that question is an honest “no”, then you got your unit test.

That being said, what do I mean by testing other people’s code? Simple, take the following fake function for instance:


function saveUserToDatabase(userObject, dbConnection){

    if(userObject.name == '') return new Error("The user's name is empty")
    if(!emailValidator.validate(userObject.email)) return new Error("The user's email is not valid")

    if(dbConnection.save(userObject)) {
        return {
            error: false,
            msg: "User saved to the database correctly"
        }
    } else {
        return new Error("There was a problem saving the user to the database")
    }
}

Of course, this is just some JavaScript thrown together, there is no real logic behind it, just focus on what it is supposed to be doing: validating data and saving into the database.

Now, if you were to test something like this:

describe("saveUserToDatabase function", () => {

    it("Return an error if the user's email is not valid", _ => {
        let user = {
            name: "Fernando",
            email: "lkadlkjalkjd"
        }
        let ret = saveUserToDatabase(user, dbConnection)
        assert(ret.message, "The user's email is not valid")
    })
})
`

You’re essentially trying to make sure your function returns the correct error message. And that’s completely fine, but if you pay attention to the program flow, you’ll notice you’re calling the validate method of a package you didn’t write. You’re essentially making sure that method correctly responds to an invalid email. That test was already written by the library’s creator. It’s not your job to validate that piece again.

You should be focusing on the error message returned and to do that, you need to overwrite the dependency you have on your emailValidator and inject a fake one. This injected fake validator, also known as a stub will return whatever you need it to return for your test, making sure your logic behaves as expected on every execution.

The same goes for the save method, but we’ll cover that particular one in the next mistake.

Testing the database connection

I see this problem time and time again with developers trying to test functions that deal with databases.

Look at the example from above, the same saveUsertoDatabase function. You can see how it’s calling the save method of the database connection. That’s not something strange, that’s quite normal. The problem with that is the test. If you’re hoping to test that an object got saved to the database by running a query against it and making sure the retrieved object is the same as the one saved, then you’re doing it wrong.

Even if you’re spinning up a test database that gets destroyed after the fact, you’re doing it wrong. Unit tests can’t depend on external services to function. Think about it: if for any reason the connection to the database fails during test execution, then your test will fail. Does that mean your code is wrong? No. It just means you meant to test your code but ended up testing the connection to the database. And that is wrong.

Integration tests, e2e tests, and other types of tests are great to correctly test that type of behavior. In our case, we just want to worry about our code. That means the only thing that needs to exist while your test is running, is the test. And for that, you need to mock every database object and inject a fake one into your function.

This comes with an added benefit: with mocks, you can control what the database returns every time, so it’s easier for you to write tests for the cases where the query fails, when the query succeeds and even strange cases where the object gets half said (I don’t know, maybe that’s something you want to do, don’t look at me like that).

And this can all be applied to any external service really, API calls, file system calls, and so on. Anytime your code needs to make an external call and deal with the result, that call needs to be faked.

Testing multiple things inside a test case

Which error message do you consider easier to understand:

Error: Test "validate and save user" failed

Or

Error: Test "Validate name field on user object" failed

I’m going to assume you’re answering “the second one”, otherwise, you need to rethink your answer.

Having functions that do several things is not uncommon, it’s actually quite normal and you should not stop doing that. But your tests aren’t meant to be written one for every function, they need to focus on the “unit” of code you’re testing (remember from above?).

So if you’re hoping to validate several things at once because it makes sense and otherwise you’d be repeating a lot of logic and code in your tests, then think again. A unit test should focus on one thing, and one thing alone. If that test fails, it should clearly state what unit of code failed, so you can fix it. But if instead, it covers 3 or 4 logic paths, pinpointing the actual error can get messy.

Writing your tests after the fact

Unit testing is a practice that should happen either during the development of the tested code or best case scenario: before.

This is because, in both of those cases, you’re thinking about everything your code can do, every edge case that could potentially happen, and every way it could fail. However, if you’re writing your test once the code is written and, potentially, already deployed, then you’re biased.

Your working code is dictating the logic of your test, which means you’re more likely to only test the “happy path” of your logic because that is what you’re seeing. Not having the code fully written and working, helps you in questioning its logic more. It’s really an unconscious thing, but you can compare it to having developers testing their own functionalities vs having a separate person, who’s had no involvement in creating it, testing them. The second person will most likely report a lot more problems than its creator, who’s already biased and testing it the way they understood it and how they coded it.

That is not how you test features and definitely not how you write your tests either.

Failing to update your tests after code refactors

Finally, consistency is also part of writing good unit tests. Just like with the previous point, if you’re not updating the logic of your tests after (or while) the logic of the tested code changes, then the tests lose all validity.

Revisiting your tests after a code update (or refactor) is important because you could’ve made changes and added new logic paths that were never covered by the original test. This means the new code could potentially be considered safe (after all, old tests are still green) but really, all there is that that you added new units of code that haven’t been tested.

This is why code coverage tools are important, they help you understand if all the units of code in your files have actually been tested. You can use that to understand the actual impact a refactor has over your tests, before calling it a day because everything is green after you touched 20 lines of code.

Imagine adding three more validations to the saveUserToDatabase function based on new attributes on the user object. Those are all new logic paths our tests would’ve missed, and they could potentially be all written incorrectly. Just revisit your tests after code changes, it’s not going to kill you and it’ll make sure the test suite keeps doing its job properly.

Unit testing is definitely a very important task, that should be part of every software project. No matter how small or big it is, they provide a level of certainty over the quality of the written code that very few other tools can. And they also add that extra level of security for the future, when updates and refactors come our way, we can rest assured that if we wrote our tests correctly, and we treat them properly, they’ll raise their hand the minute something looks off.

That is it, are you falling prey to any of the above mistakes? Answer truthfully, no one is watching! Leave a comment down below if you can think of any other common mistakes when writing unit tests, I’d love to compile a bigger list!


If you liked what you’ve read, consider subscribing to my Newsletter and I’ll send quick tips & insights about our industry directly to your inbox!

Did you find this article valuable?

Support The Rambling of an Old Developer by becoming a sponsor. Any amount is appreciated!