Unit testing with Jest

Unit testing with Jest

ยท

9 min read

Unit testing is a critical practice in software development that involves writing test code to ensure that individual units of code, such as functions, classes, or modules, behave as expected. One of the most popular testing frameworks for JavaScript and TypeScript is Jest, which makes it easy to write and run unit tests.

To ensure that tests are comprehensive, reliable, and maintainable, it is important to follow a testing approach that breaks down the test into four distinct phases: setup, execution, assertion, and teardown. This approach, known as the Four-Phase Test Process, provides a clear and structured way of testing code.

  1. Arrange-> During the Arrange or setup phase, the test environment is set up by arranging any necessary dependencies or resources that the code being tested relies on. This could include creating objects, initializing variables, or configuring any other necessary resources.

  2. // Act-> In Act or execution phase, the code being tested is executed with specific inputs or parameters for that test. The aim is to perform the specific action or behavior that the test is intended to verify.

  3. Assert-> The assertion phase involves checking that the results of the execution are as expected. For example, you might check that a function returns the expected output for a given input or that a specific method was called with the correct parameters.

  4. Teardown -> Finally, during the teardown phase, the test environment is reset to its original state after the test has been executed. The goal is to release any resources that were acquired during the setup phase, reset any state that was modified during the test, or perform any other necessary cleanup actions.

Example:

describe("goodbyePerson", () => {
    test("should return a goodbye + name ", () => {
      //Arrange
      const expected = "goodbye juan"
      const sut = sayBye()
      //Act
      const actual = sut.goodbyePerson("juan");

      //Assert
      expect(actual).toBe(expected)
    })
  })

The anatomy of a test in jest.

import { greeter } from "./greeter"

/* The describe() function is used in Jest to group related test cases together. It helps organize tests into logical groups, making it easier to understand and run them. */
describe("greeter", () => {

  /* Name of the function , describe the function, output function */
  test("helloWorld given default should return Hello World", () => {
    const sut = greeter()
    const expected = "hello world" // Expected is the result what we expect output to be
    /* Sut is the acronimus of system under test  */

    const actual = sut.helloWorld();
    /* Actual is the output of the function */


    expect(actual).toBe(expected)
   //The expect function is used to assert the actual output with the expected output. If the actual output matches the expected output, the test case passes.
  })
})

F.I.R.S.T Principles

The F.I.R.S.T principles are an essential set of guidelines for writing unit tests that are effective, reliable, and maintainable.

F -> Fast

Unit tests are checks that developers write to make sure their code works as expected. These tests should be run frequently to catch any issues early on. If tests take a long time to run, it slows down the development process and makes it harder to identify and fix problems. By writing fast tests, developers can run them more frequently and catch issues earlier, which saves time and resources. Fast tests also help keep developers motivated and prevent frustration.

cheatsheet to get fast tests

  1. Most tests should be small: By keeping tests small and focused on specific units of code, it's easier to run them quickly and catch issues early on.

  2. Monitor test speed: Keeping track of how long tests take to run can help identify slow tests and potential bottlenecks in the testing process.t

  3. Try to improve test speed: Continuously looking for ways to improve test speed, such as optimiz fing code or using parallelization techniques, can help speed up the testing process and improve developer productivity.

  4. Breakdown slow tests: If a test is taking too long to run, it can be helpful to break it down into smaller tests or focus on optimizing the code being tested to improve its performance.

I -> Insolate

When we write unit tests, each test case should be designed to test a specific piece of code in isolation. This means that it should not depend on any other tests or external factors like a database, network, or file system.

By keeping our tests independent, we ensure that each test provides accurate and reliable results without any interference from external factors. This makes it easier to identify and fix issues quickly, as we can isolate the source of the problem more easily.

Overall, following the Independent principle helps us write better and more effective unit tests that can be run in isolation or as part of a larger test suite.

cheatsheet to get insolate

  1. Use the setup, execution, assert pattern that we see below

  2. Using a transient fixture, but whats means this?

Example:

Let's say we have a function calculateSum that takes two numbers as input and returns their sum:

function calculateSum(a: number, b: number): number {
  return a + b;
}

To test this function, we could write a Jest test like this:

test("calculateSum should return the correct sum", () => {
  const a = 2;
  const b = 3;
  const expectedSum = 5;

  const actualSum = calculateSum(a, b);

  expect(actualSum).toBe(expectedSum);
});

In this test, we're avoiding shared state by declaring the a, b, and expectedSum variables inside the test function. These variables are only used in this one test and are not shared with any other tests.

We're also using a transient fixture by calling the calculateSum function inside the test function and passing in the a and b variables as arguments. The calculateSum function returns a value that is only used inside this test and is not shared with any other tests.

By avoiding shared state and using transient fixtures like this, we can achieve test isolation and ensure that our tests are reliable and maintainable.

  1. no sequence dependencies-> means that the tests should be able to run in any order without affecting their results. Tests should not rely on other tests to run before or after them, and each test should be able to run independently. This helps ensure that our tests are reliable, maintainable, and can be easily scaled.

Example:

Suppose we have a function that generates a random number between 1 and 10 called generateRandomNumber(). In our test, we want to ensure that this function always returns a number between 1 and 10, but we also want to make sure that our test doesn't fail just because of bad luck (e.g. the random number generator happened to generate a 1 or 10).

test("generateRandomNumber should return a number between 1 and 10", () => {
  // Arrange
  const min = 1;
  const max = 10;

  // Act
  const randomNumber = generateRandomNumber();

  // Assert
  expect(randomNumber).toBeGreaterThanOrEqual(min);
  expect(randomNumber).toBeLessThanOrEqual(max);
});

Notice that we're not trying to control the sequence in which the random number generator generates numbers. We're simply checking that the output falls within the expected range. This ensures that our test is reliable regardless of the sequence in which the random numbers are generated.

R-> Repeatable

it means that we can run the same test multiple times and get the same result every time.

This is important because we want to ensure that the behavior of our code is consistent and predictable, even when we run the tests multiple times.

If a test is not repeatable, it can lead to false positives or false negatives, which can be misleading and make it difficult to trust the test results.

Cheatsheet to get repeteable tests

  1. Mock or stub any external dependencies: If your code relies on external services or resources, you can use mocking or stubbing to simulate their behavior in your tests. This can help ensure that your tests are consistent and repeatable, even if the external dependencies change.

  2. Use consistent test data: Make sure that your tests use the same input data every time they're run. This can help ensure that your tests produce the same results every time, and can also make it easier to troubleshoot failures.

  3. Reset any state changes: If your tests modify the state of any objects or resources, make sure to reset them to their initial state before each test is run. This can help ensure that each test starts with a clean slate and produces consistent results.

  4. Run your tests in isolation: Make sure that your tests are not affected by the results of other tests. This can be achieved by running each test in its own process or by using a tool like Jest that automatically isolates tests.

S -> self-validating

The "S" in the "FIRST" principles stands for "Self-Validating". It means that the test should have a boolean output, where true means the test passed and false means the test failed. This ensures that the test is fully automated and can be run without human intervention.

T-> Thorough

The T in F.I.R.S.T. stands for "Thorough". Thorough tests should cover all relevant scenarios and edge cases to ensure that the system being tested is functioning correctly. This means testing not only the "happy path" or expected outcomes, but also potential errors or unexpected situations that may arise. By writing thorough tests, we can increase the confidence we have in our code and reduce the likelihood of bugs or issues in production.

RULE -> TEST RUNNER

The test runner is a tool that executes the automated tests that you have written for your software application. It is responsible for finding, executing, and reporting on the results of your tests. Test runners provide a framework that automates the process of running tests, making it easier for developers to create and maintain their test suites. Popular test runners for JavaScript include Jest, Mocha, and Jasmine.

THE TEST RUNNER

The test runner is a tool that executes the automated tests that you have written for your software application. It is responsible for finding, executing, and reporting on the results of your tests. Test runners provide a framework that automates the process of running tests, making it easier for developers to create and maintain their test suites. Popular test runners for JavaScript include Jest, Mocha, and Jasmine

RULE -> ALWAYS USE A TEST RUNNER.

Without a test runner, developers would need to manually execute each test case and keep track of the results, which can be time-consuming and error-prone. Test runners can help ensure that all tests are executed consistently and efficiently, which is important in large and complex codebases.

cheatsheet

The most easy way to get avocate for archieving this principle is using TDD(TEST-DRIVEN-DEVELOPMENT)

CONCLUSION

In summary, unit testing is an important practice in software development, and the F.I.R.S.T principles provide a useful framework for writing effective and maintainable unit tests. By following these principles, developers can ensure that their tests are reliable, efficient, and able to catch issues early on in the development process.

In less words, a better quality tests.

ย