A Basic Introduction to Unit Testing in Angular Apps
analyzing the testing setup (as created by the CLI)
in app.component.spec.ts each it block is a test
beforeEach method runs first then each test
tests run independently of each other
TestBed is testing utility object
in declarations list components to be tested
need to create each component in the it block
each test block ends with expect method
    import { TestBed, async } from '@angular/core/testing';
    import { AppComponent } from './app.component';

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

Top

Index

running tests with the CLI
in project folder to run tests use
        ng test
            

Back to top

Index

Adding a Component and Some Fitting Tests
create a new component named user with command
    ng g c user
            
change the new component's markup to
    <div *ngIf='isLoggedIn'>
      <h1>User logged in</h1>
      <p>User is : {{user.name}}</p>
    </div>
    <div  *ngIf='!isLoggedIn'>
      <h1>User not logged in</h1>
      <p>Please log in first</p>
    </div>
            
in user.component add the properties shown below
    import { Component, OnInit } from '@angular/core';

    @Component({
      selector: 'app-user',
      templateUrl: './user.component.html',
      styleUrls: ['./user.component.css']
    })
    export class UserComponent implements OnInit {
      user: { name: string };
      isLoggedIn = false;

      constructor() { }

      ngOnInit() { }
    }
    
edit user.component.spec.ts as shown below
    import { async, ComponentFixture, TestBed } from '@angular/core/testing';

    import { UserComponent } from './user.component';

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

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

      it('should create the app', () => {
        fixture = TestBed.createComponent(UserComponent);
        let app = fixture.debugElement.componentInstance;
        expect(app).toBeTruthy();
      });
    });
            
run the tests using the command
    ng test
            

Top

Index

testing dependencies : components and services
add file user.service.ts to user folder
edit the file as shown below
    export class UserService {
        user = { name: 'Flip' };
    }            
in user.component inject the service adding the service as a provider
in ngOnInit set the user property to the user the service provides
    import { Component, OnInit } from '@angular/core';

    import { UserService } from './user.service';

    @Component({
      selector: 'app-user',
      templateUrl: './user.component.html',
      styleUrls: ['./user.component.css'],
      providers: [UserService]
    })
    export class UserComponent implements OnInit {
      user: { name: string };
      isLoggedIn = false;

      constructor(private userService: UserService) { }

      ngOnInit() {
        this.user = this.userService.user;
      }
    }
            
in user.component.spec.ts add a new test to check the if the service is injected
in the browser Karma should show no user is logged in
    import { async, ComponentFixture, TestBed } from '@angular/core/testing';

    import { UserComponent } from './user.component';
    import { UserService } from './user.service';

    describe('UserComponent', () => {
      ...
      it('should user the user name from the service', () => {
        fixture = TestBed.createComponent(UserComponent);
        let app = fixture.debugElement.componentInstance;
        // get an instance of the service
        let userService = fixture.debugElement.injector.get(UserService);
        // inject the service
        fixture.detectChanges();
        expect(userService.user.name).toEqual(app.user.name);
      });
      ...
    });
           
add a new test to check the user name is shown when the user is logged in
add a similar test to check the user name is not shwon when the user is not logged in
    import { async, ComponentFixture, TestBed } from '@angular/core/testing';

    import { UserComponent } from './user.component';
    import { UserService } from './user.service';

    describe('UserComponent', () => {
      ...
      it('should show the user name when logged in', () => {
        fixture = TestBed.createComponent(UserComponent);
        let app = fixture.debugElement.componentInstance;
        app.isLoggedIn = true;
        fixture.detectChanges();
        let compiled = fixture.debugElement.nativeElement;
        expect(compiled.querySelector('p').textContent).toContain(app.user.name);
      });

      it('should not show the user name when not logged in', () => {
        fixture = TestBed.createComponent(UserComponent);
        let app = fixture.debugElement.componentInstance;
        fixture.detectChanges();
        let compiled = fixture.debugElement.nativeElement;
        expect(compiled.querySelector('p').textContent).not.toContain(app.user.name);
      });
    });
            

Top

Index

simulating async tasks
add a service to the sample
service imitates an asynchronous task
    export class DataService {
        getDetails() {
            const resultPromise = new Promise((resolve, reject) => {
                setTimeout(() => {
                   resolve('Data');
                }, 1500);
            });
            return resultPromise;
        }
    }
            
inject the service into the component
add a data property and set the property in ngInit
    import { Component, OnInit } from '@angular/core';

    import { UserService } from './user.service';
    import { DataService } from '../shared/data.service';

    @Component({
      selector: 'app-user',
      templateUrl: './user.component.html',
      styleUrls: ['./user.component.css'],
      providers: [UserService, DataService]
    })
    export class UserComponent implements OnInit {
      user: { name: string };
      isLoggedIn = false;
      data: string;

      constructor(private userService: UserService, private dataService: DataService) { }

      ngOnInit() {
        this.user = this.userService.user;
        this.dataService.getDetails()
        .then((data: string) => this.data = data);
      }
    }
            
add two tests to user.component.spec.ts
each test uses the spyOn method of testing environment
the returnValue method supplies the result expected of the service
the first test calls the service synchronously
the call returns immediately with a null returned
failure or success depends upon the argument to the toBe method
when the arg is undefined the test succeeds
    import { async, ComponentFixture, TestBed } from '@angular/core/testing';

    import { UserComponent } from './user.component';
    import { UserService } from './user.service';
    import { DataService } from '../shared/data.service';

    describe('UserComponent', () => {
      const component: UserComponent = null;
      let fixture: ComponentFixture<UserComponent>;
      ...
      it('should not fetch data successfully if not called asynchronously', () => {
        fixture = TestBed.createComponent(UserComponent);
        const app = fixture.debugElement.componentInstance;
        const dataService = fixture.debugElement.injector.get(DataService);
        // method of testing environment
        const spy = spyOn(dataService, 'getDetails')
          // will execute async code but returns provided data
          // will run in async fashion
          .and.returnValue(Promise.resolve('Data'));
        fixture.detectChanges();
        // OK
        expect(app.data).toBe(undefined);
        // fails
        // expect(app.data).toBe('Data');
      });
      
the second test wraps the callback using the async method
fixture.whenStable waits for the service method to complete and its callback contains the expect().toBe() methods
because the call completes asynchronously the service method 'returns' the argument to the returnValue method
      // wrap callback with async function
      it('should fetch data successfully if called asynchronously', async(() => {
        fixture = TestBed.createComponent(UserComponent);
        const app = fixture.debugElement.componentInstance;
        const dataService = fixture.debugElement.injector.get(DataService);
        // method of testing environment
        const spy = spyOn(dataService, 'getDetails')
          .and.returnValue(Promise.resolve('Data'));
        fixture.detectChanges();
        // waits for all async tasks to finish
        fixture.whenStable().then(() => {
          expect(app.data).toBe('Data');
        });
      }));
    });
            

Top

Index

using "fakeAsync" and "tick"
add new test copying the previous test
change async call to fakeAsync
when using fakeAsync there is no need to call fixture.whenStable()
instead call tick method before expect method
same test as before just a different way to run it
    import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';

    import { UserComponent } from './user.component';
    import { UserService } from './user.service';
    import { DataService } from '../shared/data.service';

    describe('UserComponent', () => {
      const component: UserComponent = null;
      let fixture: ComponentFixture<UserComponent>;

      beforeEach(async(() => {
        TestBed.configureTestingModule({
          declarations: [UserComponent]
        })
          .compileComponents();
      }));
      ...
      // wrap callback with fakeAsync function
      it('should fetch data successfully if called asynchronously', fakeAsync(() => {
        fixture = TestBed.createComponent(UserComponent);
        const app = fixture.debugElement.componentInstance;
        const dataService = fixture.debugElement.injector.get(DataService);
        // method of testing environment
        const spy = spyOn(dataService, 'getDetails')
          .and.returnValue(Promise.resolve('Data'));
        fixture.detectChanges();
        // call tick before expect
        // in fakeAsync environment wait for all async tasks to complete
        tick();
          expect(app.data).toBe('Data');
      }));
    });
            

Top

Index

isolated vs. non-isolated tests
a pipe or simple method call can be tested independently from Angular
an isolated test can be run when the thing to be tested does not rely on Angular or other pieces of the application
if not isolated must use testing methods
a simple pipe
    import { Pipe, PipeTransform } from '@angular/core';

    @Pipe({
        name: 'reverse'
    })
    export class ReversePipe implements PipeTransform {
        transform(value: string) {
            return value.split('').reverse().join('');
        }
    }
            
the isolated test
no Angular or other dependencies imported
    import {ReversePipe } from './reverse.pipe';

    describe('Pipe: ReversePipe', () => {
      it('should reverse the inputs', () => {
        const reversePipe = new ReversePipe();
        expect(reversePipe.transform('hello')).toEqual('olleh');
      });
    });
            

Top

Index

n4jvp.com