facebook

Blog

Stay updated

A look at testing starting from file .spec.ts, and creation of our first tests
Angular Testing you don’t scare me
Wednesday, December 23, 2020

The first time I opened a test file, which we usually find after creating a new project using Angular CLI, I felt a bit lost.

As I was much more familiar with test frameworks such as XUnit or NUnit, I tried to match the parts of a test file in NUnit and those present in a Jasmine test.

We can see below the code of the app.component.spec.ts file, and we will try to analyze it so that we can start creating our test files.

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
 
describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));
 
  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });
 
  it(`should have as title 'AngularTesting'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('AngularTesting');
  });
 
  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to AngularTesting!');
  });
});

As a first step I tried to find something like [TestFixture] that identifies a test class.

In the code we found describe(‘AppComponent’ () => {}); which represents our [TestFixture].

Right afterwards, there is beforeEach(() => {}); that in NUnit is identified with the [SetUp] annotation. With this annotation, we are instructing NUnit to perform the decorated method before each test, very useful when the tests need a common setup phase. Similarly, the instructions contained in the beforeEach block will be executed before each test.

At this point, we miss the annotation [Test], we usually find in the methods, but in this case I would say that it is quite simple to identify it, due to the format of the file.

it(‘should create the app’, () => {});

Now that we have divided the test file, we can analyze more specifically the areas.

Angular TestBed

In beforeEach we find the following instruction:

TestBed.configureTestingModule({});

First of all, we must understand who TestBed is.

Angular Test Bed (ATB) is the highest level Angular testing framework that allows us to test everything that depends on the Angular Framework.

Going back to the instruction, a thing that allows us to do TestBed is the dynamical creation of a module for our test that simulates an Angular NgModule. The structure is the same, with imports, declarations and all that we can declare in NgModule.

Let’s analyze one of the tests:

it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

we encounter another instruction in which ATB is present,

TestBed.createComponent(AppComponent);

When I looked at this instruction, the first thing I asked myself was why we didn’t use a simple new.

A component is not only a class, with new i only get the instance of the class of the component. I can still test its operation, of course,but I cannot test if the elements of the DOM are properly created and if they interact with each other as expected.

We must use the createComponent of ATB; it creates an instance of AppComponent, to which the HTML template is associated and it returns a ComponentFixture object.

ComponentFixture

ComponentFixture is a tool for debugging and testing a component.

it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

It’s easy to imagine what this method is testing: it makes sure that the AppComponent instance has been properly created.

What debugElement is, on the contrary, is not so intuitive and therefore we must analyze it.

As we said before, a component is not only a class but also its template. ComponentFixture includes these two things in one single object and exposes some properties such as debugElement and nativeElement.

In nativeElement, Angular is not able to know at compile time what we will find, it depends on the testing environment we are using.

In case it is a browser environment like the one we will see in these tests, there will be an HTMLElement and the object will be the same that we will find in debugElement.nativeElement. Otherwise, there may be an object, which API would be reduced compared to HTMLElement, or it may have no API at all.

Angular then relies on debugElement abstraction to stay safe on any supported platform.

Starting from the HTMLElement we can use the querySelector to take the elements of the DOM that we are interested to test, as it happens in the following instructions of the last test:

const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to AngularTesting!');

Spy e TestBed.inject

At this point, we are almost ready to write our test class, but we still can’t test components or services that have dependencies.

There are several ways to provide the class we are testing with its dependencies: we can pass them with a new, we can create a Test Double class, or we can use a spy, and this isthe recommended choice.

Jasmine allows us to create a spy using the following instruction:

var fooSpy = jasmine.createSpyObj(foo, [bar]);

and, with the following instruction, we can make the setup of the bar method

fooSpy.bar.and.returnValue('stub value');

now we just have to create our class by passing the spy

service = new Service(fooSpy);

With this technique, however, we are not using the dependency injection of Angular: in our Angular applications, we will never make a new of a class to pass it to our service. Generally, such an approach would mean having dependencies that do not facilitate testing and would require the manual creation of the instance. To overcome the problem, we can register in the appropriate module all the classes that we need in the array Provider.

We would like to replicate the same behavior in our tests too. We can do that by combining spy and ATB:

var fooSpy = jasmine.createSpyObj(Foo, [bar]);
TestBed.configureTestingModule({
    …
        providers: [
     Service,
      { provide: Foo, useValue: fooSpy }
    ]
    ..
});

Finally, in the test methods we will use the following instructions to recover the instances we need

service = TestBed.inject(Service);
fooSpy = TestBed.inject(Foo) as jasmine.SpyObj<Foo>;

TestBed.inject can be used starting from the version 9 of Angular, for the previous ones we must use TestBed.get

Demo

We just have to write the code and try to add a dependency to the AppComponent created by the CLI, and then test it with a spy.

I want to create a service that exposes a method that returns a list of strings: if the call is successful, the list will be shown otherwise an error message.

Let’s see how we can configure the test. First of all, we create the service:

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class AppService {
  constructor() { }
  getList(): Observable<string[]> {
    return of(['item1', 'item2', 'item3']);
  }
}

Add it to app.module.ts by inserting it among the providers

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [AppService],
  bootstrap: [AppComponent]
})
export class AppModule { }

edit app.component.ts to be able to make the call to getList()

import { Component, OnInit } from '@angular/core';
import { AppService } from './app.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'AngularTesting';
  items = [];
  constructor(service: AppService) {
    service.getList().subscribe(
      data => this.items = data
    );
  }
}

and app.component.html to view the list.

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<ul>
  <li *ngFor="let item of items">{{item}}</li>
</ul>

Now we can add our tests, modifying the test class we analyzed before: app.component.spec.ts

If we run the tests now, they’ll all be red. In the AppComponent, we added a dependency with a method that returns an observable, so we must instruct the test class to handle the change.

We modify the beforeEach by adding the spy for the created service, and proceed with the setup of the getList method.

const items = ['item1', 'item2', 'item3'];
beforeEach(async(() => {
    const spy = jasmine.createSpyObj('AppService', ['getList']);
    spy.getList.and.returnValue(of(items));
 
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
      providers: [
        { provide: AppService, useValue: spy }
      ]
    }).compileComponents();
  }));

Now that tests are green again, we can insert a new one.

it('should render list when getList() work properly', () => {
  const fixture = TestBed.createComponent(AppComponent);
 
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  const elements = compiled.querySelector('ul')
  .querySelectorAll('li');
 
  items.forEach((item, index) => {
    expect(elements[index].textContent === item);
  });
});

With this test, we make sure that the list items shown to the user are exactly those we have provided in the setup, in case getList is successful.

There is no test where the call generates an error, and we want to show an error message to the user.

In the component, at the moment, we have nothing that handles such an error on the code side nor element able to view it; we will then have to modify it in order to manage this case study.

Let’s start with a TDD approach, starting from the test and then modify the component.

As the first step, we recover the AppService spy and change the behavior of the getList method, returning an error.

Now we can create our expect: using querySelector, we recover the HTML element that should contain the error message and verify that it contains exactly “Error!”.

The full test should be like this:

it('should show error message when getList() return an error', () => {
   const errorMessage = 'Fake error';
   const spy = TestBed.get(AppService) as jasmine.SpyObj<AppService>;
   spy.getList.and.callFake(() => {
     return throwError(new Error(errorMessage));
   });
   const fixture = TestBed.createComponent(AppComponent);
 
   fixture.detectChanges();
   const compiled = fixture.debugElement.nativeElement;
   const element = compiled.querySelector('h2.error');
 
   expect(element.textContent.trim()).toEqual(errorMessage);
 });

When we run the tests, we will have a red test, and the reason is that the textContent property of null object cannot be read. In other words, no h2 element with error class has been found in the component. I would have been surprised by the opposite.

Let’s change the component now to catch the error launched by getList;

app.component.ts
import { Component } from '@angular/core';
import { AppService } from './app.service';
 
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'AngularTesting';
  items = [];
  errorMessage = '';
 
  constructor(service: AppService) {
    this.errorMessage = '';
 
    service.getList().subscribe(
      data => this.items = data,
      error => this.errorMessage = error.message
    );
  }
}

and show it by h2 tags with error class as established in the test:

app.component.html
<h1>
    Welcome to {{ title }}!
  </h1>
</div>
<h2 class="error" *ngIf="errorMessage">
  {{errorMessage}}
</h2>
<ul>
  <li *ngFor="let item of items">{{item}}</li>
</ul>

In the end, we relaunch the tests:

It was impossible to analyze all the scenarios that arise in the tests in a single article. But this can undoubtedly be a good basis for starting to understand the existing tests in an Angular project and for beginning to write our own tests that cover the requirements of the tasks that have been assigned to us.

I hope you enjoyed the article.
See you in the next one!