Angular unit testing 101 (with examples)

Mustapha Aouas - Aug 29 '19 - - Dev Community

The more we add features to our software the more it grows in complexity. And as it grows in complexity, more time is required to manually test it. In fact, as we add new features to our applications, the time required to manually test them increases exponentially!
To prevent that we could take advantage of automated testing as it is the best way to increase the effectiveness, efficiency, and coverage of our applications testing.

In this post, we'll be talking about Angular unit testing, using Karma and Jasmine. By the end of this post, you should feel comfortable writing specs to test your Angular components, directives, pipes, and services as well as learning techniques to test synchronous and asynchronous behaviors.

Before we start

First things first, let's talk about some of the basics and terminologies of testing. It'll help us establish a mental model of how things work so we will be able to understand the later parts a little bit better.

Terminologies

Automated testing

It's the practice of writing code to test our code, and then run those tests. There are 3 types of tests: unit tests, integration tests, and end-to-end (e2e) tests.

Unit test

A unit test or UT is the procedure to check the proper functioning of a specific part of a software or a portion of a program.

Karma

Karma is a test runner. It will automatically create a browser instance, run our tests, then gives us the results. The big advantage is that it allows us to test our code in different browsers without any manual change in our part.

The pattern that Karma uses to identify test files is <filename>.spec.ts. This is a general convention that other languages use. If for some reason you want to change it, you can do so in the test.ts file.

Jasmine

Jasmine is a popular Javascript testing framework. It comes with test doubles by using spies (we'll define what is a spy later), and assertions built into it out of the box.

Jasmine provides a lot of useful functions to write tests. The three main APIs are:

  1. Describe(): It's a suite of tests
  2. it(): Declaration of a single test
  3. expect(): Expect something to be true for example

Mock

Mock objects are fake (simulated) objects that mimic the behavior of real objects in controlled ways.

Fixture

A fixture is a wrapper around an instance of a component. With a fixture, we can have access to a component instance as well as its template.

Spy

Spies are useful for verifying the behavior of our components depending on outside inputs, without having to define those outside inputs. They're most useful when testing components that have services as a dependency.

Basics

The Angular CLI downloads and installs everything you need to test an Angular application with the Jasmine test framework. All you need to do to start testing is to run the following command:



ng test


Enter fullscreen mode Exit fullscreen mode

This command builds the app in watch mode and launches Karma.

Angular automated testing

Skeleton of a test

Using the three Jasmine APIs mentioned above, a skeleton of a unit test should look like this:



describe('TestSuitName', () => {
  // suite of tests here

  it('should do some stuff', () => {
    // this is the body of the test
  });
});


Enter fullscreen mode Exit fullscreen mode

When testing, there's a pattern that became almost a standard across the developer community, called AAA (Arrange-Act-Assert). AAA suggests that you should divide your test method into three sections: arrange, act and assert. Each one of them only responsible for the part in which they are named after.

So the arrange section you only have code required to set up that specific test. Here objects would be created, mocks setup (if you are using one) and potentially expectations would be set. Then there is the Act, which should be the invocation of the method being tested. And on Assert you would simply check whether the expectations were met.

Following this pattern does make the code quite well structured and easy to understand. In general lines, it would look like this:



  it('should truncate a string if its too long (>20)', () => {
    // Arrange
    const pipe = new TroncaturePipe();

    // Act
    const ret = pipe.transform('1234567890123456789012345');

    // Assert
    expect(ret.length).toBeLessThanOrEqual(20);
  });


Enter fullscreen mode Exit fullscreen mode

Configuration & instantiation

In order to access methods of the component we want to test, we first need to instantiate it.
Jasmine comes with an API called beforeAll() which is called once before all the tests.
The thing is if we instantiate our component inside this function our tests won't be isolated because the component properties could be changed by each test, and therefore, a first test could influence the behavior of a second test.
To solve that problem, Jasmine has another API called beforeEach(), which is very useful as it lets our tests to be run from the same starting point and thus to be run in isolation.
So, using this API, our test should look something like this:



describe('componentName', () => {
  // suite of tests here

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [myComponent],
    });

    fixture = TestBed.createComponent(myComponent);
    component = fixture.componentInstance;
  });

  it('should do some stuff', () => {
    // this is the body of the test

    // test stuff here
    expect(myComponent.methodOfMyComponent()).not.toBe(true);
  });
});


Enter fullscreen mode Exit fullscreen mode

All of a sudden we have a lot of new unknown APIs. Let's have a closer look at what we have here.
Angular comes with an API for testing testBed that has a method configureTestingModule() for configuring a test module where we can import other Angular modules, components, pipes, directives, or services.
Once our testing module configured we can then instantiate for example the component we want to test.

Components

An Angular component combines an HTML template and a TypeScript class.
So, to test a component we need to create the component's host element in the browser DOM.
To do that we use a TestBed method called createComponent().
This method will create a fixture containing our component instance and its HTML reference. With this fixture, we can access the raw component by calling its property componentInstance and its HTML reference by using nativeElement.

With that, an Angular component test should look like this:



describe('HeaderComponent', () => {
  let component: HeaderComponent;
  let element: HTMLElement;
  let fixture: ComponentFixture<HeaderComponent>;

  // * We use beforeEach so our tests are run in isolation
  beforeEach(() => {
    TestBed.configureTestingModule({
      // * here we configure our testing module with all the declarations,
      // * imports, and providers necessary to this component
      imports: [CommonModule],
      providers: [],
      declarations: [HeaderComponent],
    }).compileComponents();

    fixture = TestBed.createComponent(HeaderComponent);
    component = fixture.componentInstance; // The component instantiation 
    element = fixture.nativeElement; // The HTML reference
  });

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

  it('should create', () => {
    // * arrange
    const title = 'Hey there, i hope you are enjoying this article';
    const titleElement = element.querySelector('.header-title');
    // * act
    component.title = title;
    fixture.detectChanges(); 
    // * assert
    expect(titleElement.textContent).toContain(title);
  });
});


Enter fullscreen mode Exit fullscreen mode

After setting the title in our test, we need to call detectChanges() so the template is updated with the new title we just set (because binding happens when Angular performs change detection).

Pipes

Because a pipe is a class that has one method, transform, (that manipulates the input value into a transformed output value), it's easier to test without any Angular testing utilities.

Bellow an example of what a pipe test should look like:



describe('TroncaturePipe', () => {
  it('create an instance', () => {
    const pipe = new TroncaturePipe(); // * pipe instantiation
    expect(pipe).toBeTruthy();
  });

  it('truncate a string if its too long (>20)', () => {
    // * arrange
    const pipe = new TroncaturePipe();
    // * act
    const ret = pipe.transform('123456789123456789456666123');
    // * asser
    expect(ret.length).toBe(20);
  });
});



Enter fullscreen mode Exit fullscreen mode

Directives

An attribute directive modifies the behavior of an element. So you could unit test it like a pipe where you only test its methods, or you could test it with a host component where you can check if it correctly changed its behavior.

Here is an example of testing a directive with a host component:



// * Host component:
@Component({
  template: `<div [appPadding]="2">Test</div>`,
})
class HostComponent {}
@NgModule({
  declarations: [HostComponent, PaddingDirective],
  exports: [HostComponent],
})
class HostModule {}

// * Test suite:
describe('PaddingDirective', () => {
  let component: HostComponent;
  let element: HTMLElement;
  let fixture: ComponentFixture<HostComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [CommonModule, HostModule], // * we import the host module
    }).compileComponents();

    fixture = TestBed.createComponent(HostComponent);
    component = fixture.componentInstance;
    element = fixture.nativeElement;

    fixture.detectChanges(); // * so the directive gets appilied
  });

  it('should create a host instance', () => {
    expect(component).toBeTruthy();
  });

  it('should add padding', () => {
    // * arrange
    const el = element.querySelector('div');
    // * assert
    expect(el.style.padding).toBe('2rem'); // * we check if the directive worked correctly
  });
});


Enter fullscreen mode Exit fullscreen mode

Services

Like pipes, services are often easier to test. We could instantiate them with the new keyword. That's fine for basic services, but if your service has dependencies, it's better to use the TestBed.configureTestingModule API like this:



describe('LocalService', () => {
  let service: LocalService;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [LocalService],
    });

    service = TestBed.inject(LocalService); // * inject service instance
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should set the local', () => {
    // * act
    service.setLocal('fr');
    // * assert
    expect(service.getLocal()).toBe('fr');
  });
});


Enter fullscreen mode Exit fullscreen mode

To get the service instance, we could inject it inside a test by calling TestBed.get() (with the service class as the argument).

Well, with that you should be ready to write tests for your Angular applications. That being said, to tackle some of the common difficulties you might encounter while writing Angular tests, I added small cheatsheets you can find in the next section :)

Cheatsheets

Dealing with HTTP requests

To avoid making HTTP requests at each test, one method consists of providing a fake service that mocks the real one (the one that communicates via HTTP requests).
Once the fake service is implemented we provide it to the TestBed.configureTestingModule() like this:



class FakeApiService {
  // Implement the methods you want to overload here
  getData() {
    return of({ items: [] }); // * mocks the return of the real method
  }
}
//...
TestBed.configureTestingModule({
  imports: [],
  declarations: [myComponent],
  providers: [
    {
      provide: RealApiService,
      useClass: FakeApiService,
    },
  ],
});
//...


Enter fullscreen mode Exit fullscreen mode

Dealing with the Angular router

To deal with the Router you could either add the RouterTestingModule in the imports of your testing module or you could mock it using the technique we saw in the test above.

Using spies

Spies are an easy way to check if a function was called or to provide a custom return value.
Here is an example of how to use them:



it('should do something', () => {
  // arrange
  const service = TestBed.get(dataService);
  const spyOnMethod = spyOn(service, 'saveData').and.callThrough();
  // act
  component.onSave();
  // assert
  expect(spyOnMethod).toHaveBeenCalled();
});


Enter fullscreen mode Exit fullscreen mode

You could read more about Spies in this great article.

Dealing with asynchronous code

It's worth noting that since I wrote this post, there have been new and improved methods for testing asynchronous code. I'll return to this topic in a future article.

Dealing with promises


it('should do something async', async () => {
  //  * arrange
  const ob = { id: 1 };
  component.selected = ob;
  //  * act
  const selected = await component.getSelectedAsync(); // get the promise value
  //  * assert
  expect(selected.id).toBe(ob.id);
});


Enter fullscreen mode Exit fullscreen mode
Dealing with observables


it('should do something async', (done) => {
  //  * arrange
  const ob = { id: 1 };
  component.selected = ob;
  //  * act
  const selected$ = component.getSelectedObs(); // get an Observable
  //  * assert
  selected$.subscribe(selected => {
    expect(selected.id).toBe(ob.id);
    done(); // let Jasmine know that you are done testing
  });
});


Enter fullscreen mode Exit fullscreen mode
Dealing with timeouts


const TIMEOUT_DELAY = 250;
//...
it('should do something async', (done) => {
  //  * arrange
  const ob = { id: 1 };
  //  * act
  component.setSelectedAfterATimeout(ob);
  // * assert
  setTimeout(() => {
    expect(component.selected.id).toBe(ob.id);
    done(); // let Jasmine know that you are done testing
  }, TIMEOUT_DELAY);
});


Enter fullscreen mode Exit fullscreen mode

Wrap up

conclusion

So, in this article, we saw that the Angular CLI configures everything for us and we only have to run ng test to start our tests. Then we saw what is an automated test and how to write one using Jasmine and the Angular testing utilities (for components, pipes, directives, and services). Finally, we saw examples of some of the special cases you might encounter when writing tests.

With all this we just scraped the surface of Angular testing, there's so much more to learn about it. That's why this post is the first of an "Angular testing" series. Follow me on twitter @theAngularGuy to get updated when the next posts might come out.

In the meantime, Happy codding!


What to read next?

. . . . . . . . . . . . . . . . . . .