Basics of unit testing

Some basics on unit testing

Published on 07 Jan, 2024


Introduction

This article aims to explain what you should to consider when writing unit tests, It does not explain how to write unit tests in your programming language or in a certain testing framework.

With unit tests, you can detect bugs early in the development process, which reduces the cost and headache of fixing bugs when they are found in a production system. Unit tests are also the easiest and fastest tests to write as a developer, as you can write and execute them directly while writing your production code. One of the biggest advantages of unit tests is that they ensure the stability of your code. When you or someone on the team need to adjust or refactor the code, it is ensured that unwanted logic changes will be detected before the code goes into production.

Getting started

Your tests are code too, so treat them accordingly. Here are a few things to consider:

  • Avoid copying and pasting variables from one test case to another; make them global.
  • If your test cases that are used multiple times, define them globally.
  • Organize your tests per test case in own methods.
  • Apply the same coding rules to your tests as you do to your regular code.

Select a consistent naming convention for your unit tests and adhere to it. Here are some tips to follow:

  • Ensure that your test names are unambiguous and leave no room for interpretation.
  • Be specific about what is being tested, and include details that help identify the context or scenario.
  • Choose names that clearly describe the functionality being tested

You don’t necessarily need a unit testing framework. While it’s advisable for larger applications or those with plans for significant growth, for small scripts where you don’t intend to create a full-blown project, you can simply write code to test your functions. Just call them and check the output.

What not to test

Before we discuss what we want to test, let’s first draw the line on what not to test.

Avoid testing connections to other systems such as:

  • Your database
  • 3rd party systems connected via API, SDK…
  • Files from your file system

In general, we should avoid using mocking. Often, you can achieve the desired result by splitting the part you want to test into its own method and testing it as a single isolated unit.

Avoid testing workflows in unit tests; use integration tests for these.

Do not test nested methods; instead, focus on testing isolated methods.

Generally, avoid testing performance in your unit tests, as your client PC and CI pipelines may have different specifications than your staging or production environment. Additionally, you want to test your entire process with more load than a single execution.

What to test

General ideas

Now, let’s discuss what you do want to test. Here are a few general rules:

  • Test every method to ensure it behaves correctly when given null or an empty value.
  • For methods that change the state, check that the state is changed correctly and complies with the state lifecycle.
  • For methods that create or transform an object, test that the object looks correct, and verify that all variables are set correctly.
  • When validating minimum and maximum values, test the value under the minimum, at the minimum, at the maximum, and above the maximum. (a little unclear)

Security

If your code includes security-related logic, you definitely want to test it! Here are some examples:

  • If it is possible to upload files to your software, ensure that only the allowed MIME types are accepted and not all.
  • If you want to enforce HTTPS, test that HTTP is not allowed.
  • If your system has password strength policies, test both the positive cases and all the negative cases, to ensure compliance.

Error handling

Testing error handling is crucial for well-written software and providing understandable errors for users. Additionally, error handling may be one of the few exceptions where we want to use mocking.

  • Test that the text of the error message precisely describes the problem and suggests possible solutions.
  • Ensure that the correct exceptions are thrown and not generic ones.
  • Verify the correct reaction to HTTP errors; in this case, you have to use mocking when simulating an API call

Tips

  • If you encounter a scenario where you need to transform an API response (eg. JSON) to objects, you can save the API response on your filesystem and read it. This approach eliminates the need to make an actual API call and keeps the JSON out of your code.

Test Coverage

Don’t fall for the test coverage trap! While it’s important to cover your validations and business logic, testing every line of code may not be necessary.

If you choose to measure the coverage of your code, include only the files or methods that you want to test and ignore everything else. Alternatively, explicitly disable coverage analysis for certain files or methods. This approach ensures that you focus on testing what is essential and prevents overlooking areas you intended to test.

Last words

When you start writing unit tests, you will notice that you begin to structure your code differently. You will have smaller methods that contain your logic and validation. This is a perfect opportunity to start thinking about how to structure your code more effectively and where to place different parts of your code.