15

Unit Testing: Best Practices To Follow

 4 years ago
source link: https://blog.bitsrc.io/unit-testing-best-practices-to-follow-2ace94dfdabe
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Unit Testing: Best Practices To Follow

Here are 8 tips for you to turn the boring task of writing unit tests into an actual challenging programming problem.

Image for post
Image for post

Everybody and their brother should be writing unit tests if they’re writing production-ready code. This should be mandatory for all levels, not only Jr developers starting out on the project (which is something we sometimes tend to do).

And here is why: unit tests are a great way of ensuring your code is not only working at any given point in time, but most importantly, they ensure you it works after a change. That is massive! If you’ve never had to make a big change in a huge code base that you didn’t write, then you haven’t lived.

The sad truth is we tend to consider writing tests as a boring task and we try to minimize our time with them. When in reality, it should be as important (if not more) than writing your regular code.

So just to help you get started, I wanted to share with you some advise on what your tests should look like and what they should do. Because even though they might seem like a very basic task, they’re very easy to get wrong and write useless tests.

Here are my 8 recommendations.

Only test your code

This is a basic one, when writing tests we can get carried away and start testing code that we don’t have to.

Let me explain: if you’re only writing code that you wrote 100% of, then this is not going to happen, but considering that almost never happens, you normally end up interacting with 3rd party libraries. Either built by someone you don’t know or by someone on your team, the takeaway should be the same one: if it’s written by someone else and used as a black box (i.e you don’t have access or the need to access their code, only their public interface) then they should’ve tested their functionality, not you.

Let me explain with a bit of code:

This is a completely fake piece of JavaScript code, but as an example, consider the following points:

  • You probably didn’t write the code for SQLORM , and even if you did, it’s probably part of a library you’re using as black box here, so if you write a piece of test that calls this function without overwriting this object, you’re indirectly testing it. This can be fixed by a combination of dependency injection and the use of stubs or mocks (I’ll cover these points in a second).
  • The same goes for the validateUser , if it’s not part of your base code, then you should be overwriting it.
  • The logic of your test should be focusing on the different IF statements, and the returned values, that is all.

I can hear you asking: but how do I test that the user actually got saved? Saving the object into the database is the responsibility of the SQLORM object, and that code should’ve been tested by their creators. If you know that code doesn’t have tests, then you shouldn’t have used it in the first place. I know, it’s a hard separation to make, but over time and with experience, you’ll start seeing where your tests end and their code begins.

Tip: Share your reusable components between projects using Bit (Github). Bit makes it simple to share, document, and organize independent components from any project.

Use it to maximize code reuse, collaborate on independent components, and build apps that scale.

Bit supports Node, TypeScript, React, Vue, Angular, and more.

Image for post
Image for post
Example: exploring reusable React components shared on Bit.dev

Unit tests should be automated

Or rather, unit tests should be easy to automate. This is because unit tests aren’t really meant for you to manually run, they’re mean to be run as a trigger or a reaction to a change.

This is normally accomplished as part of a CI/CD process. Every time someone on your team makes a change, unit tests should be executed. Every time a merge is done (assuming you’re using GIT here or something similar), tests should be executed. Heck, every time a deployment is done tests should be executed, just to be safe.

The best part is that most unit tests frameworks already allow for this to happen, all you need to worry about is writing your tests. Deciding which files to execute, how to run their code and even how to capture their output is already solved by the test suite.

So remember: running the tests should be cheap, it should be easy and most importantly, it should be fast. They should not affect or delay a deployment, unless of course, they fail, in which case they should stop it or roll it back.

If executing your tests make you want to leave your computer, make a cup of coffee and drinking while you wait, then you’re doing something wrong. Period.

Test one thing at a time

Inline with only testing your code, make sure that your unit tests only test one thing at a time. I’ve seen (and written) my share of tests where at the end, I write like 5 assess statements checking for different results.

This is not correct, when a test fails, you need to quickly and easily identify which part of your code broke it, so if you see a message on your deployment logs stating:

Validate, save and return user - has failed

How can you be sure which of the three things you tested was actually failing?

More to the point, normally, tested code has a lot of moving parts, in the example above, I’m making up a function that validates if a user is correct, then proceeds to save it into some kind of database and finally, it returns it. There are three variables there, but when testing you only have to have one: the actual condition you’re testing. The rest should be static.

How can you turn these variables into static conditions so it doesn’t affect your logic? That’s what mocks and stubs are for, they will allow you to fake a part of your code to control its output, that way the test will only care about the variable part. I’ll give you some examples down below, but right now just remember: avoid testing for multiple things, even if the function is small, consider the type of test you’re writing: unit tests. If you’re able to assess multiple things, then what you’re testing is not a unit of code.

Write your tests during development

Writing the tests after the code’s been written, and sometimes deployed to production is just as effective as checking the gas meter after filling the tank of your car. Even if it gives you an empty reading, you’re going to trust the empiric evidence more than its readings.

When writing the tests for a piece of code that’s already finished and tested manually, you’ll be biased by its behavior, by how it works. Unless you’re writing that tests because a bug was fixed and you want to be sure it can’t happen again, you’ll just be testing for the behavior you’re seeing deployed. This is wrong.

When writing your tests, you’re not just testing the “happy path” of your logic, that one is easy, you should also be testing for edge cases, conditions that can only happen every once in a while or even never. Here is where unit tests show their true value, so take advantage of this.

Consider a function to add two values, nothing too crazy, your basic test would be to check that given two values, you get the expected result, right? That’s the happy path, that’s the normal and expected behavior when all conditions are met, but what happens when one of these values is null? Or both? What happens if these values are negative? What if you don’t have strong types, such as with JavaScript? Is your function capable of adding two strings? One string and one number? Do you have an expected error message for those cases or will you just crash?

These are all questions you’re more likely to ask yourself during the development of your function (or even before you even start writing your first line of code) than after the fact, when it’s done and integrated into a bigger code base.

Enter TDD

And that is a point for TDD or Test Driven Development, which essentially states that you should be writing your tests first, describing every expected behavior for your code, before actually writing your code. It definitely requires a change in your mental model, but trust me, it is worth it.

In theory, TDD would give you error free code, but we all know that in practice that doesn’t exist, there are always bugs. That being said, TDD is definitely a great tool to help you iron out stupid and basic errors, that you wouldn’t catch on your first go if you tried to solve your problem by first coding your function.

As developers we tend to see a problem and think of the code that would fix it, not on the tests that would describe it. That is just a fact, but that is also something you can train, and with time, you’ll see the benefits. Writing code will take you longer at first, but consider the amount of time you’ll safe bug fixing and debugging on the long run.

Give TDD a try.

Only test the public interface

Knowing when to stop testing is also a potential problem, because after all not all code is public. Or in other words, when writing a big enough system, you’re most likely writing modules, which export functions but also have private helper functions that don’t get exported.

If that is your case, writing test for these internal functions might be challenging, but that’s OK. Think about it like this: private functions are not accessible to the outside world directly, they’re just there to help public facing functions. You have two options here:

  1. You break encapsulation and turn all your private functions into public ones. Effectively allowing your tests to reach them but exporting a lot of functionality that should not be public.
  2. Or, simply test their behavior through their public siblings. In other words, let your tests conditions cover their functionality as well from the effect they have on the public function’s logic.

Going back to the adding function example, let’s see some code:

With this scenario, you can’t access the areValid function from outside the module, but you can definitely test its behavior through the output of add. So consider that when writing your tests, not every piece of logic tested needs to be public, it just needs to be reachable through a public pathway.

Dependency Injection is your friend, embrace it

Dependency injection (or sometimes known as dependency inversion) states that you should find a way to “inject” any external dependency into your code, instead of having your code importing it from inside. Why is that important? Because this practice allows you to write logic that you can affect without changing its code.

Or put another way, if your logic depends on external code, such as the save function of a database ORM, you can “inject” a different ORM providing the same save method but doing it differently, and you would be changing the way your logic works, without changing a single line of code.

This is a fantastic tool to have when writing tests, because we’re normally depending on external libraries. We don’t re-invent the wheel everytime, instead, we tend to re-use known and well tested libraries. But going back to the “Only test your code” and “Test one thing at a time” principles, we need to both, control what these external dependencies do, and how they behave, in order to test our code.

Consider changing the add module’s code to this:

The code now depends on an external (albeit fictional) dependency: add-validator . If you were to write tests for add they would be testing the module’s behavior and we don’t want that. So instead, you need to write your code allowing for dependency injection.

In JavaScript you can do that easily by allowing for a third, optional parameter to be passed, in other languages, there are options, you just need to understand what they are:

Now you can easily use this function as part of your bigger logic, without affecting it, but at the same time, you can write a test that overwrites the third parameter with a custom function.

By injecting that dependency, you can now control its behavior on each test.If you want to test what happens when the validator returns an error, you’ll pass in a fake version of it that always returns an error, and so on.

For real world testing, dependency injection is less of a nice-to-have and more of a must-have tool, so consider picking it up if you haven’t yet.

Use mocks, stubs, spies and dummies to control logic flow

Finally, after all the introductory topics from above, we’ve landed on the big kahuna. These are the tools you’ll be using the most during your testing time. And that is simply because they allow you to accomplish everything from “only testing your code” all the way to “only testing one thing” .

Stubs and mocks are probably the most common ones, but there are other variations of them called Dummies and Spies. In practice they’re all related and you can probably accomplish the same thing with all of them, but depending on the methodology (and sometimes even the framework) you’re using, you’ll be presented with some of them or all of them.

But the main gist of them is that they allow you to “fake” a portion of the tested code so you can make all those variables parts, static. Remember that part? Sometimes a moving part can be an external dependency, which you can fake with a Stub, which essentially is an object with a pre-defined behavior.

Sometimes, a moving part is a dataset, which you can fake with mocked data. Or sometimes you’re trying to understand if as part of your tested logic, you’re calling the right methods, spies are great for that.

The point I’m trying to make here is that all these tools need to be your bread and butter when writing tests. You can’t properly test your logic without them, so don’t avoid learning about them.

To give you an example, here is how you would provide a stub for the validation function we’ve been discussing:

Granted, the test and the tested code are too simple to make write any meaningful code, but you get the point: I’m “injecting” an external dependency that’s been stubbed in order to always return the same result. Essentially my test is saying: if the validator returns an error message, you should return the same thing, unchanged. That is the logic path of my code that I’m testing, instead of testing how the validator does its work.

It’s a fine line we’re walking here, so be careful not to cross it and end up testing external logic.

Don’t test external calls

The final tip I want to cover is a personal pet peeve. As I’ve mentioned several times already here, unit tests are meant to test your code, your logic, not the external libraries you’re using.

So when your code is saving data to the database, having a “dummy” database to save your data and then validate that test by querying it and checking for it is wrong. This is not a unit test, this is an integration test. You’re literally testing the integration between your code and your database (albeit a dummy one). I don’t care if you’re spinning up a database only for the test and then destroying it, that’s just wrong.

The same goes for any piece of code that calls an external service.

Every time your unit test calls an external service a puppy dies.

Think about that for a minute.

If the code you’re testing is performing an external call (i.e a database query, an API request, a socket message, even a file system call), then you need to stub it, mock it, fake it however you want, but you need to turn that ugly variable into a static beauty. Why? Because your unit tests can’t depend on an external service that you don’t control.

Think about it this way: if the API your tested code is calling is down for maintenance, suddenly your tests will start failing, potentially breaking your deployments, stopping your urgent bug fix to be deployed into production. And all of that simply because an external service you don’t control is undergoing a maintenance window. Does that sound right to you?

Let me answer that for you: no it doesn’t.

So please, for the love of all that is holy, use everything covered so far in this article to avoid calling external services from within your tests. You have the tools here, use them.

That’s it, I’ve shown you the light, I’ve given you the tools to write beautiful unit tests that provide security to your deployments, certainty during your refactors, and a good night’s sleep after a Friday deploy. It is now up to you to use them properly.

But on a more serious note, have I left anything out? Do you have any extra tips you’d like to share with others? Leave a comment down below, I’d love to know what your experience with unit tests has been like.

Learn More


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK