Yeison Daza
5 min read

What Are Tests and How to Write Them in JavaScript

Something that probably all programmers do every day is make mistakes (break things), and this happens more frequently as the complexity of our applications grows. The way we prevent this from happening and affecting the business is by writing tests. Today we’re going to look at what tests really are and how tools like Jest work.

What Is a Test, Really?

A test, in a nutshell, is a small piece of code that helps us verify that the programs we write work as they should. Technically, a test is code that throws an error during execution if the result doesn’t match what we expect.

For example, if we wanted to test a function that adds two numbers, we could write:

// tests.js

const result = sum(3,4)
const expected = 7

if(result !== expected) {
  throw new Error(`${result} is not equal to ${expected} 😕`)
} else {
  console.log("Todo bien 🌈")
}

If the sum function returns a value different from what we expect, the program would throw an error.

You can save the code above in a file called test.js and run it with node test.js. Try changing the expected value and see that it actually throws an error.

If you wrote code like this, you’d already be creating tests for your code. But writing conditions and errors this way can get boring or complicated, and that’s where testing tools come in.

A test lets us detect when a part of our code doesn’t return what we expect, using a condition to evaluate it. This is called an assertion.

What Is a Testing Tool?

A testing tool mainly gives us two things:

  1. A way to group and organize tests
  2. A way to create these conditions to evaluate our code, called assertions

There are hundreds of libraries that solve one or both of these needs. Personally, I use Jest — a library that handles both really well. But I feel that to start using it, it’s important to understand how it works, so let’s write a simple version of how Jest works (almost all testing tools work the same way).

Assertion Library

First, let’s write a function that makes assertions much easier.

function expect(result) {
  return {
    toBe(expected) {
      if(result !== expected) {
        throw new Error(`${result} is not equal a ${expected} 😕`)
      }
    }
  }
}

This expect function receives a value and, thanks to closures, we can return an object with one or more methods that can access this value. In this case, just one method that receives another value and checks if they’re actually equal.

With this function, if we wanted to test the sum function again, we can simply write:

const result = sum(3,4)
const expected = 7

expect(result).toBe(expected)

And we’d be creating tests for our function just like we did above. The important thing is that we now have a pretty useful abstraction that lets us write assertions much faster. These abstractions are called matchers, and tools like Jest come with many useful ones, such as:

  • toEqual
  • toBeGreaterThan
  • toBeLessThan
  • toBeLessThanOrEqual
  • toStrictEqual
  • toThrow
  • toThrowError

But remember, at the end of the day, these are just conditions like the ones we learned in our first programming class.

How to Organize Our Tests

Now we can write assertions much faster, but what happens if one fails? Right now it would be hard to tell which one failed.

An important thing about error messages is that they help us find where the error happens and give us hints about what part we should look into. This is really useful if we have hundreds of tests. Let’s write a function for this.

function test(title, callback) {
  try {
    callback()
    console.log(`😉 ${title}`)
  } catch (error) {
    console.error(`🙁 ${title}`)
    console.error(error)
  }
}

The test function simply receives a title for the test and uses it to show a more organized log of the tests you run.

Now we can rewrite our test, giving it a title:

test('sum works', () => {
  const result = sum(3,4)
  const expected = 7
  expect(result).toBe(expected)
})

If this test fails, we’d see something like 🙁 sum works in the console, which helps us find which test failed. Jest takes it a step further and adds more information.

But what if you have several related tests or tests that evaluate the same code? You might want to group them. Jest covers this case and gives us a describe function that lets us group related tests.

All the tests we’ve written so far are valid Jest tests. A file ends up looking something like this:

// sum.test.js

describe('sum function', () => {
  test('should return a correct value', () => {
    const result = sum(3,4)
    const expected = 7
    expect(result).toBe(expected)
  });

  test('should convert value strings to number and result value', () => {
    const result = sum(3, "4")
    const expected = 7
    expect(result).toBe(expected)
  });

  test("should throw a error if pass a invalid string", () => {
    expect(() => {
      sum(3,"a")
    }).toThrow();
  })
});

Inside describe you can add one or many tests, and Jest will help you by showing great logs when any of them fail. But remember, it’s important that the descriptions of your blocks (describe) and tests (test) are clear and descriptive.

Final Words

Throughout this post we’ve come to understand what a test really is and how to write them using a tool like Jest. But we still have a long road ahead. In upcoming posts we’ll cover things like:

  • Creating tests for asynchronous functions
  • Creating tests for functions that don’t always return the same thing
  • Using spies and mocks
  • Testing functions that depend on timers
  • And much more.