DEV Community

Cover image for How to test Angular components using Jest nice and easy
Wagner Caetano
Wagner Caetano

Posted on

How to test Angular components using Jest nice and easy

1. How angular tests are structured

In the context of Angular and Jest, "shallow testing" and "unit testing" refer to two different approaches to testing components and their behavior.

The difference between shallow testing and unit testing

Let's break down the differences between these two concepts:

  • Unit Testing:
    It involves testing individual units of code, often isolated from their dependencies, to ensure they work as intended. Mocks or stubs are often used to isolate dependencies and create controlled testing environments.

    For example, when unit testing an Angular component, you might:

    • Test the logic of a specific method within the component.
    • Verify that properties are set correctly after certain actions.
    • Ensure that event handlers or input/output bindings work as expected.
  • Shallow Testing:
    It involves testing a component while rendering itself as a whole and their immediate child components as "shallow" placeholders. Shallow testing is particularly useful when you want to test the html template.

    For example, in the context of an Angular component, shallow testing might involve:

    • Testing how the component renders certain data and responds to user interactions.
    • Verifying that the component's template structure and bindings are correct.
    • Ignoring the actual behavior of child components by replacing them with shallow representations.

Showing you why 99% of tests don’t need TestBed

If you create a test file or component folder using Ng or other generation tool for Angular files, you will notice that it creates the test with a huge setup structure.

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { MyComponent } from './my.component';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ MyComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(MyComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

This is the default setup for Angular tests. It is using TestBed to create the component and all its dependencies. This is a very heavy setup and it is not needed for most of the tests.
So a way to create the same test without using TestBed is:

import { MyComponent } from './my.component';

describe('MyComponent', () => {
  let component: MyComponent;

  beforeEach(() => {
    component = new MyComponent();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});
Enter fullscreen mode Exit fullscreen mode

Notice that this test has the same setup outcome as the before, and now we are just instantiating a new component and testing it.

The mindset behind a good test scenario

What is a good test scenario?

We can use the concept of Gherkin as a direction, basically it's split in 3 parts:
given, when, then.

  • Given: The initial context or state of the system. Here's where we setup everything that we need outside of the test scenario, like services mocks, data mocks, etc.
  • When: The action or event that occurs. Here's where we triggers that we want to test, it can be a function call, a click, a key press, etc.
  • Then: The expected outcome. Here's where we check if the expected outcome is the same as the actual outcome, or if the calls are called in a expected amount of times, etc.

This idea helps you setup your test initially so later you just need to write the code to do what it needs to do.
For example:

it('should validate getCustomer', () => {
    //given: mock the return of the moethod getCustomer

    //when: triggers the component method that calls the service

    //then: expect that any logic or treatment is done correctly

    //then: expect that the service is called only once

    //then: expect that the component now has the value returned and treated stored in a variable
  });
Enter fullscreen mode Exit fullscreen mode

It can be a little bit over do for some cases, but it helps you to think about the test before you write it, especially if you are starting now to write tests scenarios.

Beyond that we should also finish our scenarios with a cleanup

  • Cleanup: The cleanup of the test. Here's where we clean the calls of everything we setup in the given part, like services mocks, data mocks, etc.

Using jest, we can call two kinds of methods for that:

// Cleans the calls and the returns of all mocks
jest.clearAllMocks();

// Cleans the mock implementation itself
jest.resetAllMocks();
Enter fullscreen mode Exit fullscreen mode

How to mock all methods properly

The idea is to bring the mock the closest to the real scenario as possible, so we can have a better control of the test and the expected outcome is acurate in reality.

Using jest as a facilitator

Jest has a method called 'fn()' that creates a mock function, and it has a lot of methods that helps us to control the mock and the expected outcome.
For example:

// example of a http call
public getCustomers(): Observable<Customer[]> {
  return this.http.get<Customer[]>(`${this.url}/customer`);
}
Enter fullscreen mode Exit fullscreen mode

In that case a empty mock could be:

const customersService = {
  getCustomers: jest.fn().mockReturnValue(of([])),
}
Enter fullscreen mode Exit fullscreen mode

As you might see, we also used the method 'mockReturnValue' to return a value when the method is called, because the method 'getCustomers' returns an Observable, so we need to return a value to the mock.

Notice that the method fn() returns a series of auxiliar methods that we can use to control the mock, like:

  • mock.calls: Returns an array of the calls done to the mock, each call is an array of the arguments passed to the mock.
  • mock.results: Returns an array of the results of the calls done to the mock, each result is an object with the value and the status of the call.
  • mockImplementation: Allows you to set the implementation of the mock.
  • mockImplementationOnce: Allows you to set the implementation of the mock for a specific call.
  • mockReturnValue: Allows you to set the return value of the mock.
  • mockResolvedValue: Allows you to set the resolved value of the mock.
  • mockRejectedValue: Allows you to set the rejected value of the mock.

and many more, you can check the full list in the jest documentation.

How to validate something in chain

Sometimes we need to validate something in chain, like a method that returns a object that we need to call a method to get a value, and we need to validate that value. To create that mock we can:

const customersService = {
  getCustomers: jest.fn().mockReturnValue(of([{ name: 'John' }])),
}

const serviceChooser = {
  getService: jest.fn().mockReturnValue(customersService),
}
Enter fullscreen mode Exit fullscreen mode

How to create a mock for a dinamic scenario

Imagine the scenario, where depending on the input of a method you return a different object or it do a different logic, to create a mock like that we can:

const customersService = {
  getCustomers: jest.fn().mockImplementation((input) => {
    if (input === 'John') {
      return of([{ name: 'John' }]);
    } else {
      return of([{ name: 'Mary' }]);
    }
  }),
}
Enter fullscreen mode Exit fullscreen mode

As you can see we are using the auxiliar method 'mockImplementation' to set the implementation of the mock, and we are using the input of the method to return a different value.

We could also add 'Once' to it, that way it would work as a mock just once, and then you would need to mock again.

That's also useful for cache testing, like imagine a scenario where the first time it shouldn't return something, but then the next one it should return something, we could do:

const customersService = {
  getCustomers: jest.fn()
    .mockImplementationOnce(() => of([]))
    .mockImplementationOnce(() => of([{ name: 'John' }])),
}
Enter fullscreen mode Exit fullscreen mode

How to know for sure that your async test is running correctly

Using fakeAsync

The function fakeAsync is a function that allows you to run a test in a synchronous way, so you don't need to use the async/await syntax, and you can use the tick() function to simulate the time passing.

it('should validate getCustomer', fakeAsync(() => {
  //when: triggers the component method that calls the service
  tick(); // this will simulate the time passing
  //then: expect that any logic or treatment is done correctly
}));
Enter fullscreen mode Exit fullscreen mode

You can also use other methods like:

  • flush(): Simulates the passage of time until all pending asynchronous activities finish.
  • flushMicrotasks(): Simulates the passage of time until all pending microtasks finish.
  • tick(millis): Simulates the passage of time until all pending asynchronous activities finish. The microtasks queue is drained at the very start of this function and after any timer callback has been executed.
  • tickMacroTasks(millis): Simulates the passage of time until all pending asynchronous activities finish. The microtasks queue is drained at the very start of this function and after any timer callback has been executed.
  • discardPeriodicTasks(): Discard all remaining periodic tasks.
  • flushPeriodicTasks(): Flushes all pending periodic tasks.

Using async/await

The async/await syntax is a way to write asynchronous code in a synchronous way, so you don't need to use the fakeAsync function, and you can use the await keyword to wait for the promise to resolve.

it('should validate getCustomer', async () => {
  //when: triggers the component method that calls the service
  await fixture.whenStable(); // this will wait for the promise to resolve
  //then: expect that any logic or treatment is done correctly
});
Enter fullscreen mode Exit fullscreen mode

But can we use it always ?

No, we can't, because the async/await syntax is a way to write asynchronous code in a synchronous way, so it doesn't work with all asynchronous code, like setTimeout, setInterval, etc.

// won't work
public getWaitToShowSomething(): void {
  interval(1000).subscribe(() => {
    this.showSomething = true;
  });
}

it('should validate getWaitToShowSomething', async () => {
  const response = await component.getWaitToShowSomething();
  expect(response).toBe(true);
});

// will work

public getWaitToShowSomething(): Promise<boolean> {
  return new Promise((resolve) => {
    interval(1000).subscribe(() => {
      this.showSomething = true;
      resolve(true);
    });
  });
}

it('should validate getWaitToShowSomething', async () => {
  const response = await component.getWaitToShowSomething();
  expect(response).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Notice that when you use a observable, and you don't want to convert it into a promise,
async/await won't work, because it's not a promise, so you need to use the fakeAsync function.

Using done as a callback

The done callback is a function that you can use to tell the test that it's done, so it can continue to the next test, it's useful when you have a asynchronous code that you can't convert into a promise or observable.

it('should validate getCustomer', (done) => {
  customersService.getCustomers().subscribe(() => {
    //then: expect that any logic or treatment is done correctly
    done();
  });
});
Enter fullscreen mode Exit fullscreen mode

In that case we are using the done callback to tell the test that it's done, so it can continue to the next test.If the done callback is not called, the test will fail after 5000ms.

This scenario ensures that the classic false positive of the test is not happening, where the test passes because it is not waiting for the asynchronous code to finish.

Make your expectations as clear as possible

Its better to have a toHaveBeenCalledTimes(1) than a toHaveBeenCalled()

The idea is to make your expectations as clear as possible, so you can know exactly what is happening in your test, and you can know exactly what is failing.

The toHaveBeenCalled() method is a method that checks if the mock was called, but it doesn't check how many times it was called, so it's better to use the toHaveBeenCalledTimes(1) method, that way you know exactly how many times it was called.

it('should validate getCustomer', () => {
  //given: mock the return of the moethod getCustomer
  customersService.getCustomers = jest.fn().mockReturnValue(of([]));

  //when: triggers the component method that calls the service
  component.getCustomer();

  //then: expect that any logic or treatment is done correctly
  expect(customersService.getCustomers).toHaveBeenCalled();
});
Enter fullscreen mode Exit fullscreen mode

In that case the test will pass, but if we change the mock to return a value, the test will fail, because the mock was called more than once.

it('should validate getCustomer', () => {
  //given: mock the return of the moethod getCustomer
  customersService.getCustomers = jest.fn().mockReturnValue(of([{ name: 'John' }]));

  //when: triggers the component method that calls the service
  component.getCustomer();

  //then: expect that any logic or treatment is done correctly
  expect(customersService.getCustomers).toHaveBeenCalledTimes(1);
});
Enter fullscreen mode Exit fullscreen mode

We can also use it for when we don't want the mock to be called, we can use the toHaveBeenCalledTimes(0) method.

it('should validate getCustomer', () => {
  //given: mock the return of the moethod getCustomer
  customersService.getCustomers = jest.fn().mockReturnValue(of([{ name: 'John' }]));

  //when: triggers the component method that calls the service
  component.getCustomer();

  //then: expect that any logic or treatment is done correctly
  expect(customersService.getCustomers).toHaveBeenCalledTimes(0);
});
Enter fullscreen mode Exit fullscreen mode

Its better to have a toHaveBeenCalledWith('John') than a toHaveBeenCalled()

The toHaveBeenCalled() method is a method that checks if the mock was called, but it doesn't check with what arguments it was called, so it's better to use the toHaveBeenCalledWith('John') method, that way you know exactly with what arguments it was called.

it('should validate getCustomer', () => {
  //given: mock the return of the moethod getCustomer
  customersService.getCustomers = jest.fn().mockReturnValue(of([{ name: 'John' }]));

  //when: triggers the component method that calls the service
  component.getCustomer();

  //then: expect that any logic or treatment is done correctly
  expect(customersService.getCustomers).toHaveBeenCalledWith('John');
});
Enter fullscreen mode Exit fullscreen mode

That's it for now, I hope you enjoyed it, and I hope it helps you to write better tests.

Sources:

You're testing your angular code wrong

Why shallow rendering is important in angular unit tests

Angular unit testing isolate and shallow jasmine

Top comments (4)

Collapse
 
dmitryefimenko profile image
Dmitry A. Efimenko

I'm going to blow your mind:
The child component of a component under test is also a dependency of the component under test!

So the difference between shallow testing and unit testing section is not quite right.

Collapse
 
wagnercaetano profile image
Wagner Caetano

Suree !!
That makes sense, but I'm not sure if I undestand why the comparison isn't quite right..

Collapse
 
dmitryefimenko profile image
Dmitry A. Efimenko

under "Unit Testing" section you mention:

Mocks or stubs are often used to isolate dependencies and create controlled testing environments

under "Shallow Testing" section you mention:

Ignoring the actual behavior of child components by replacing them with shallow representations

These are the same things. The mechanism of mocking is different, but they achieve the same goal: mocking out the dependencies so that the component under test can be tested in isolation.

Thread Thread
 
wagnercaetano profile image
Wagner Caetano

Oh I see, yeah, the point there was to specify that their scope are different, even tho their purpose is the same.. like in the shallow test the dependency is not only the services or dependencies injected in the class.. but also the dependencies in the template.. maybe i didn't express myself clearly.. I appreciate the feedback!!