DEV Community

Cover image for 8 Tips for Writing Better Unit Tests
Fotis Adamakis
Fotis Adamakis

Posted on • Edited on • Originally published at fadamakis.com

8 Tips for Writing Better Unit Tests

Unit tests are an essential part of every successful software application. They help identify bugs early and ensure that the code is working as expected. Let me list some widely adopted key strategies that can help you write better code and improve the quality of your codebase.

1. Arrange, Act, and Assert

The Arrange, Act, and Assert (AAA) is a widely used technique in unit testing. It involves breaking down a test into three distinct phases:

  1. Arranging the test data.

  2. Performing the Action or method call.

  3. Asserting the results.

To apply the AAA pattern, you should first arrange the test by setting up the necessary mocks and component instance. Then, you should act by performing the action or method call that you want to test. Finally, you should assert the results by checking that the output of the method call matches the expected output.

For example, given the following simple counter component written in Vue Options API:

    // Counter.vue
    export default {
      data() {
        return {
          counter: 0
        }
      },
      methods: {
        increment() {
          this.counter++
        }
      }
    }
Enter fullscreen mode Exit fullscreen mode

Testing it with the AAA approach is pretty straightforward:

    // Counter.spec.js
    import { shallowMount } from '@vue/test-utils'
    import Counter from '@/components/Counter.vue'

    describe('Counter.vue', () => {
      it('increments the counter when the increment method is called', () => {
        // Arrange
        const wrapper = shallowMount(Counter)

        // Act
        wrapper.vm.increment()

        // Assert
        expect(wrapper.vm.counter).toBe(1)
      })
    })
Enter fullscreen mode Exit fullscreen mode

2. Descriptive Names and Descriptions

Clear and concise test names and descriptions are essential for effectively communicating the purpose and scope of the test and making it easier to understand and maintain them.

To write good test names and descriptions, you should use descriptive and meaningful names that accurately reflect the purpose of the test. You should also include relevant details in the description, such as the input data and expected output.

For example, a lazily written title gives little information about the test case:

    describe('Counter.vue', () => {
      it('increment', () => {
        ...
      })
    })
Enter fullscreen mode Exit fullscreen mode

In contrast, when one of the following fails, the problem will be apparent.

    describe('Counter.vue', () => {
      it('increments the counter when the increase button is pressed', () => {
        ...
      })
      it('prevents the counter value of being negative when the counter is zero and the decrement button is pressed', () => {
        ...
      })
    })
Enter fullscreen mode Exit fullscreen mode

3. Fast

Unit tests should be fast to ensure a seamless development workflow. Quick test execution allows for prompt feedback, making it easier to identify and fix issues. To create speedy tests, avoid external dependencies and minimize I/O operations. Utilize mocks and stubs to simulate dependencies and focus on testing small, isolated pieces of code.

4. Deterministic

Deterministic tests are tests that produce the same output every time they are run. They are important because they help ensure that the tests are reliable and repeatable. To write deterministic tests, avoid using random data and external sources that can change. Use repeatable inputs and keep test environments consistent. Also, avoid using shared global resources that other tests can modify.

5. Comprehensive

A well-rounded unit test assesses both successful execution and potential failures (happy & unhappy paths). It’s vital to test functions using valid inputs to confirm expected results and invalid inputs to identify potential issues. Furthermore, validate error handling by examining proper responses to thrown exceptions during path execution.

6. Test-driven Development

Test-driven development (TDD) is a technique of writing tests before or during the development process. It ensures that the code is testable and that the tests cover all the necessary scenarios.

To implement TDD, start by writing a failing test that covers a specific scenario or requirement. Then, write the minimum amount of code necessary to pass the test. Finally, refactor the code to improve its design and maintainability.

7. Using Mocks

Mocks simulate the behaviour of external dependencies or objects. They are important because they help isolate the code being tested and make it easier to write deterministic tests.

To use mocks efficiently, start by identifying external dependencies that must be mocked. Then, generate a mock/stub object that mimics the behaviour of the dependency. This provides full control of returned values and the ability to track invocations and their associated parameters.

8. Run on CI/CD

By integrating unit tests into your development pipeline and ensuring their successful execution, you can prevent potential issues from slipping into production environments.

Conclusion

Treating unit tests as first-class citizens means giving them the same level of importance as production code. This mindset ensures that tests are consistently written, well-maintained, and updated as necessary.

Taking the time to invest in proper unit testing techniques will save you time and effort in the long run and result in more robust and efficient applications.

Top comments (0)