JavaScript unit testing 101

In this blog post I will explain how to write unit tests for JavaScript. For the examples, we will be using jasmine and ts-mockito, but the theory should be applicable to every testing framework. Why am I using two frameworks here instead of only jasmine? Well, in my opinion, jasmine relies too much on magic strings that might give problems during refactoring and makes autocompletion impossible in a lot of cases (depending on how good your IDE is). Ts-mockito is a mocking framework that allows you to create mocks based on a class, so you don’t have to tell it the method names to mock. There are other good mocking frameworks for typescript as well, but my experience is with ts-mockito, so I will be using it for the examples of this blog post. or the code examples I used the Angular CLI to generate a project for me, but I will not be using any of the Angular testing tools to keep these test cases completely framework independent. The Angular CLI was just an easy way to quickly get everything up and running.

The basics

The beginning of a test is naming it. I write every test in the same structure:

describe: ‘classname’

    ↪ constant values used in the test

    ↪ mock declarations

    ↪ classUnderTest declaration

    ↪ beforeEach to setup the test

    ↪ describe: ‘methodname’:

        ↪ it ‘should return this when input is so and so’

        ↪ it ‘should return this value if input is something else’
		
    ↪ describe: ‘other method name’:
	
        ↪ it‘should return this when input is so and so’
		
        ↪ it‘should return this value if input is something else’

This allows you to quickly see from your testing log, which method from which class has a bug. I personally always try to call the object you are testing something like ‘componentUnderTest’ or ‘serviceUnderTest’. This allows you to very quickly see in a unit test, which object is actually being tested.

When you want to mock certain dependencies of your class, you should always declare all the mocks at the top of the test, before your (first) beforeEach call and you should initialize them within the beforeEach. The reason we want to initialize our mocks in the beforeEach and not at declaration, is because you want fresh mocks for every test. If you initialize your mock only once at declaration, the method call count and mock return values will not be removed. This can cause tests to influence each others output, so they can complete successfully or unsuccessfully, depending on the order in which they are run, this can be a headache to debug if you don’t realize this from the beginning. I will explain here with an example:

describe('AppComponent', () => {

  const aBrownCar = {color: 'brown', brand: 'Ford'};
  const aRedFerrari = {color: 'red', brand: 'Ferrari'};
  const aRedPorsche = {color: 'red', brand: 'Porsche'};
  const aBlackCadillac = {color: 'black', brand: 'Cadillac'};

  const carService: CarService = mock(CarService);

  let componentUnderTest: AppComponent;

  beforeEach(() => {
    componentUnderTest = new AppComponent(
      instance(carService)
    );
  });

  describe('getAllRedCars', () => {
    it('should only return the cars where the color is red', () => {
      when(carService.getCars()).thenReturn([aBrownCar, aRedFerrari, aRedPorsche, aBlackCadillac]);

      const actual = componentUnderTest.getAllRedCars();

      expect(actual).toEqual([aRedFerrari, aRedPorsche]);
      verify(carService.getCars()).once();
    });
  });

  describe('getAllBlackCars', () => {
    it('should only return the cars where the color is black', () => {
      when(carService.getCars()).thenReturn([aBrownCar, aRedFerrari, aRedPorsche, aBlackCadillac]);

      const actual = componentUnderTest.getAllBlackCars();

      expect(actual).toEqual([aBlackCadillac]);
      verify(carService.getCars()).once();
    });
  });
});

As you can guess, this test will fail with:

Error: Expected "getCars()" to be called 1 time(s). But has been called 2 time(s).

This happens because the tests don’t start with a fresh mock. So the call to the getCars() from the first test, is still part of the callcount of the mock in the second test. If we reinitialize the mocks every time in the beforeEach, this problem is solved.

Writing a unit test

Every unit test has 3 parts. First part is the setup, where you set certain values and mock the necessary method calls. Second part is the actual call of the method you are testing. The last part is the verification, where you will check if the output is correct or the right methods have been called. For readability, leave a blank line between each part, so you can clearly see from a glance what your setup, call and verification part is.

A good way to write your unit test is the following:

You start with your empty test:

it('should only return the cars where the color is black', () => {
 });

Then you write the method call you want to test:

 it('should only return the cars where the color is black', () => {
     const actual = componentUnderTest.getAllBlackCars();
 });

Then you write your verification part:

it('should only return the cars where the color is black', () => {
      const actual = componentUnderTest.getAllBlackCars();

      expect(actual).toEqual([aBlackCadillac]);
      verify(carService.getCars()).once();
});

After this, you run your test once. It should fail. Then you write the setup you need to get to the expected output:

it('should only return the cars where the color is black', () => {
      when(carService.getCars()).thenReturn([aBrownCar, aRedFerrari, aRedPorsche, aBlackCadillac]);

      const actual = componentUnderTest.getAllBlackCars();

      expect(actual).toEqual([aBlackCadillac]);
      verify(carService.getCars()).once();
});

This is a good way to work when you write unit tests. Writing the verification part before your setup, allows you to see that your test is actually able to fail. This is especially handy when you are testing asynchronous code, where a missing done operator can make your test be successful every time, no matter what your expects check, but more about this in the next part.

Asynchronous testing

Thinking in async code, can be a bit of a challenge, especially when you start out with JavaScript as a new developer or coming from a language that works mostly with synchronous code.

In this part of the post, I will show you how to test asynchronous code. Both with promises and observables.

For both promises and observables, I will test methods that show the most common usage patterns. One scenario is where you simply get something asynchronously and set its value to a field, the other scenario is a little more complicated, where you get something asynchronously, have some side effects before it resolves, and have some more side effects after it resolves. If that seems complicated, don’t worry, it will become clear very soon.

For the promises, I will be using the async / await syntax. If you don’t know what that is yet, I suggest you read up on it, a good article is this one.

Promises

Scenario A

The method we will be testing will be the following:

async getAllRedCarsAsPromise(): Promise<Array<Car>> {
    const redCars = await this.carService.getCarsAsPromise();
    return redCars.filter((car: Car) => car.color === 'red');
}

This function just gets the cars from the carService asynchronously and filters out all the non-red cars before returning the array.

The first test, we will simply test the happy scenario, where the carService returns us a nice list of cars and the non-red cars are filtered out.

 describe('getAllRedCarsAsPromise', () => {

    it('should get the cars where the color is red', async done => {
      when(carService.getCarsAsPromise()).thenReturn(Promise.resolve([aRedPorsche, aBlackCadillac, aBrownCar]));

      try {
        const actual = await componentUnderTest.getAllRedCarsAsPromise();

        expect(actual).toEqual([aRedPorsche]);
      } catch (e) {
        fail(e);
      } finally {
        done();
      }
    });
});

So what exactly goes on here? First we mock the carService.getCarsAsPromise() method and tell it that it should always return a promise that resolves with a list of cars. Then we call the method that we are testing, we wait for it to resolve (by using the await keyword). Afterward we expect that the returned array only contains the red Porsche. Now, what is this whole try catch thingy? When the promise is rejected, the execution of the code in the try block will be stopped and the code in the catch block will be executed. In this case, if the getCarsAsPromise() should fail, it will go into the catch block and the test will fail. The message that will be shown by jasmine, is the error message that is given to the error that was thrown. For instance, if the getCarsAsPromise() promise return value were to be rejected with the error message ‘Something went wrong.’, the test will fail with the message Failed: Something went wrong.. In the finally block, we call the done function, to tell jasmine that our test is done, we call it in the finally block, because the code in the finally block will always be executed, regardless of the result of the promise.

Note: the done function should ALWAYS be called, regardless if your test fails or succeeds. The reason for this is that without the done function, the test will only fail after the timeout limit has been reached. When jasmine executes a test, it will wait for 5 seconds before timing out, if the done function has not been called within that time, it will fail.

To explain with an example, if we would remove the finally block and call the done function after the expect in the try block, the test would execute as expected in the scenario where the promise is resolved, but if the promise where to be rejected, it would call the fail(e), but since jasmine does not consider the fail call as the end of a test, it would still wait until the timeout for the done function. The test will eventually fail, but it will take 5 seconds to fail instead of 0.5 seconds.

In the second test, we will test what happens when the promise is rejected:

it('should set retrievalError to true if the promise is rejected', async done => {
      when(carService.getCarsAsPromise()).thenReturn(Promise.reject());

      try {
        await componentUnderTest.getAllRedCarsAsPromise();

        fail('Call should not have succeeded');
      } catch (e) {
        // nothing to expect here
      } finally {
        done();
      }
});

As you can see here, we call the fail function in the try block, because the method under test should throw an error. So after calling the method under test, it should go directly to the catch block. Since our method under test does not handle any errors, there is nothing to expect there. In the finally block, we tell jasmine the test is done.

Scenario B

The next method we will be testing is the following:

async getAllRedCarsAsPromiseWithStuffHappeningInBetween(): Promise<void> {
    try {
      this.loading = true;
      this.cars = await this.getAllRedCarsAsPromise();
    } catch (e) {
      this.carRetrievalError = true;
    } finally {
      this.loading = false;
    }
  }

So a few things are happening:

  • First we set loading to true
  • Then we get the red cars asynchronously and assign them to our cars field
  • If getting the red cars throws an error, we set carRetrievalError to true
  • Afterward we always set loading back to false

In this case, we don’t just want to test the end result of our test, but also the side effects in between, in this case, the setting of the loading field to true or false. For observables, these types of scenarios are a little easier to test, but with a little creativity, we can also do this for promises, without the use of special testing tools or libraries.

it('should set all the red cars to the cars field and set loading field correctly', async done => {
      let promiseResolve: (cars: Array<Car>) => void;
      when(carService.getCarsAsPromise()).thenReturn(new Promise(resolve => {
        promiseResolve = resolve.bind(this);
      }));

      componentUnderTest
        .getAllRedCarsAsPromiseWithStuffHappeningInBetween()
        .finally(
          () => {
            expect(componentUnderTest.cars).toEqual([aRedPorsche]);
            expect(componentUnderTest.loading).toBeFalsy();
            done();
          }
        );

      expect(componentUnderTest.loading).toBeTruthy();

      promiseResolve([aBlackCadillac, aRedPorsche]);
    });

So what’s happening here? In the promise callback, we assign the resolve function to a variable outside of that function, so we can use it later on. Then we call our method under test, but notice that we don’t use the await keyword here. Why is this? Well, if we were to use the await keyword, it would simply wait for the promise to resolve, but it would never happen because our resolve function is never called. Instead we just want to call it, let it execute and start expecting the side effects. As we saw in the method under test, the loading field is set to true before the red cars are retrieved, since those cars are not retrieved yet, we can check that the loading field is indeed set to true. Ok, now we have checked that, let’s resolve the carService call. We do this by calling our promiseResolve function. Now the cars are resolved, so we can check that the loading has now been set to false and the cars field has been correctly set to the right value. The finally callback of the returned promise, will execute after we call our promiseResolve function, and will check the final result of our method and subsequently call the done function to tell jasmine our test is over.

In the second test we will test the error scenario, where we want to see if the loading value is still set correctly and the carRetrievalError is also set to true.

it('should set the retrievalError to true and loading states correctly if the promise is rejected', async done => {
      let promiseReject: (cars: Array<Car>) => void;
      when(carService.getCarsAsPromise()).thenReturn(new Promise((resolve, reject) => {
        promiseReject = reject.bind(this);
      }));

      componentUnderTest
        .getAllRedCarsAsPromiseWithStuffHappeningInBetween()
        .finally(
          () => {
            expect(componentUnderTest.cars).toEqual([]);
            expect(componentUnderTest.loading).toBeFalsy();
            expect(componentUnderTest.carRetrievalError).toBeTruthy();
            done();
          }
        );

      expect(componentUnderTest.loading).toBeTruthy();

      promiseReject(undefined);
    });

Here we actually do the same thing, except instead of using the resolve function, we use the reject function.

Observables

For observables, I have created methods that do exactly the same as our previous promises scenarios, except we use observables here.

Scenario A

For scenario A we will test a simple observable that returns one value and then completes. This is a fairly common use case, especially with http calls. Here is our method under test:

getAllRedCarsAsObservable(): Observable<Array<Car>> {
    return this.carService
      .getCarsAsObservable()
      .pipe(
        map((cars: Array<Car>) => cars.filter((car: Car) => car.color === 'red'))
       );
}

We simply get all the cars as an observable and then we call the map operator to filter the list we get. Pretty simple scenario.

For a simple scenario like this, there are two ways to test this. The first one is this:

it('should get the cars where the color is red', done => {
      when(carService.getCarsAsObservable()).thenReturn(of([aRedPorsche, aBlackCadillac, aBrownCar]));

    componentUnderTest
      .getAllRedCarsAsObservable()
      .subscribe(
        (actual: Array<Car>) => {
          expect(actual).toEqual([aRedPorsche]);
          done();
        },
        e => {
          fail(e);
          done();
        }
      );
});

We simply call our method under test, subscribe to the resulting observable, and in the subscribe callback we do our expects and in the error callback we fail the test. This is the easiest way to test this scenario, although, it is a little verbose. To reduce the lines of code, a simple scenario like this, can also be tested by transforming the observable to a promise and test it like we did in the promise scenario:

it('should get the cars where the color is red (alternative way)', async done => {
      when(carService.getCarsAsObservable()).thenReturn(of([aRedPorsche, aBlackCadillac, aBrownCar]));

      try {
        const actual = await componentUnderTest.getAllRedCarsAsObservable().toPromise();
        expect(actual).toEqual([aRedPorsche]);
        done();
      } catch (e) {
        fail(e);
        done();
      }
});

I guess in this case it depends on what you find the most readable. The async await syntax makes the second version very readable, but comes down to personal preference.

Scenario B

In the next scenario we will be testing an observable call with side effects.

getAllRedCarsAsObservablewithStuffHappeningInBetween(): void {
    this.loading = true;

    this.getAllRedCarsAsObservable()
      .pipe(
        finalize(() => this.loading = false)
      )
      .subscribe(
        (cars: Array<Car>) => this.cars = cars,
        () => this.carRetrievalError = true
      );
  }

Same scenario as the promises, we set loading to true and subscribe to an observable, when it resolves, we set the value and when it completes, we set loading to false. If the observable throws an error, we set carRetrievalError to true.

So our first test is the happy path, the observable resolves with an array of cars and everything goes as expected:

it('should set all the red cars to the cars field and set loading field correctly', async done => {
    const carsSubject = new Subject<Array<Car>>();
    when(carService.getCarsAsObservable()).thenReturn(carsSubject);

    componentUnderTest.getAllRedCarsAsObservablewithStuffHappeningInBetween();

    expect(componentUnderTest.loading).toBeTruthy();

    carsSubject.next([aBlackCadillac, aRedPorsche]);
    carsSubject.complete();

    expect(componentUnderTest.cars).toEqual([aRedPorsche]);
    expect(componentUnderTest.loading).toBeFalsy();
    done();
});

This scenario is very easily tested with observables, because of Subjects. What is a subject? A subject can act as both an observable and an observer. This means we can subscribe to it and receive the emitted values, or we can tell it to emit a certain value.

In this case, instead of telling the carService to return an Observable that immediately resolves with a value, we tell it to return a subject.asObservable(). Now we are in control of when and what value is emitted in this observable. This allows us to check our side effects easily at the right time.

Note that I am calling the carsSubject.complete() after I emit the car array value. I do this because I set loading to false in the finalize operator. This finalize operator is only called when the observable is completed. So if I don’t call the carsSubject.complete(), loading will never be set to false and my test will fail. The error scenario is pretty much the same logic, except we call the error method instead of the next on the subject.

it('should set retrievalError to true if the observable throws an error', async done => {
    const carsSubject = new Subject<Array<Car>>();
    when(carService.getCarsAsObservable()).thenReturn(carsSubject.asObservable());

    componentUnderTest.getAllRedCarsAsObservablewithStuffHappeningInBetween();

    expect(componentUnderTest.loading).toBeTruthy();

    carsSubject.error(undefined);
    carsSubject.complete();

    expect(componentUnderTest.cars).toEqual([]);
    expect(componentUnderTest.carRetrievalError).toBeTruthy();
    expect(componentUnderTest.loading).toBeFalsy();
    done();
});

Conclusion

I hope these code examples help you to effectively unit test your (asynchronous) code. Readability is everything, so make sure you use blank lines to clearly differentiate between the different parts of a test. And don’t forget that done function, without it, your test might timeout, or even worse, it might succeed without actually having tested anything at all.

All the code examples can be checked on the following repository:

https://github.com/ivarvh/js-testing-101

Simply clone the repo, run npm install and run npm test to execute the tests.

Happy testing everyone!

Ivar is a backend Java developer who converted to frontend. When ES6 and TypeScript came along, he really started investing his time in that and now he writes Javascript / TypeScript almost exclusively.