19. Testing

1st Test (ng test || npm test):

describe('1st tests', () => {
    it('true is true', () => expect(true).toBe(true));
});

BannerComponent(Component Under Test):

import { Component } from '@angular/core';

@Component({
  selector: 'app-banner',
  template: `<h1>{{ title }}</h1>`
})
export class BannerComponent {
  title = 'Test Tour of Technologies';

}

Test:

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { BannerComponent } from './app.banner.component';

describe('BannerComponent', () => {

  let comp: BannerComponent;
  let fixture: ComponentFixture<BannerComponent>;
  let de: DebugElement;
  let el: HTMLElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [BannerComponent] // declare test component
    });

    fixture = TestBed.createComponent(BannerComponent);

    comp = fixture.componentInstance; // bannercomponent test instance

    // query for title <h1> by CSS element selector
    de = fixture.debugElement.query(By.css('h1'));
    el = de.nativeElement;
  });

  it('no title in DOM until manually call detectChanges', () => {
    expect(el.textContent).toEqual('');
  });

  it('should display original title', () => {
    fixture.detectChanges();
    expect(el.textContent).toContain(comp.title);
  });

  it('should display a different test title', () => {
    comp.title = 'My Polymer Princess, I love you.';
    fixture.detectChanges();
    expect(el.textContent).toContain(comp.title);
  });

});

Let's view the results of ng test in Chromium and in our Terminal:

Test Component with External Template:

import { Component } from '@angular/core';

@Component({
  selector: 'app-banner-external',
  templateUrl: './app.banner-external.component.html',
  styleUrls: ['./app.banner-external.component.css']
})
export class BannerExternalComponent {
  title = 'Test Tour of Technologies';
}

External Template and CSS Files:

<h1>{{ title }}</h1>

h1 {
  color: red;
  font-size: 450%;
}

Test(async):

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { BannerExternalComponent } from './app.banner-external.component';


describe('BannerExternalComponent', () => {

        let comp: BannerExternalComponent;
        let fixture: ComponentFixture<BannerExternalComponent>;
        let de: DebugElement;
        let el: HTMLElement;

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

        // synchronous beforeEach
        beforeEach(() => {
          fixture = TestBed.createComponent(BannerExternalComponent);
          comp = fixture.componentInstance; // bannerexternalcomponent test instance

          de = fixture.debugElement.query(By.css('h1'));
          el = de.nativeElement;
        });

        it('no title in DOM until manually call detectChanges', () => {
            expect(el.textContent).toEqual('');
        });

        it('should display original title', () => {
          fixture.detectChanges();
          expect(el.textContent).toContain(comp.title);
        });

        it('should display a different test title', () => {
          comp.title = 'Hi baby!';
          fixture.detectChanges();
          expect(el.textContent).toContain('Hi baby!');
        });

})

Test Component with Dependency

UserService:

import { Injectable } from '@angular/core';

@Injectable()
export class UserService {
  isLoggedIn = true;
  user = { name: 'Nils-Holger Nägele'};

}

WelcomeComponent:

import { Component, OnInit } from '@angular/core';

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

@Component({
  selector: 'app-welcome',
  template: '<h3 class="welcome"><i>{{ welcome }}</i></h3>'
})
export class WelcomeComponent implements OnInit {
  welcome = '--- not initialized yet ---';

  constructor(private userService: UserService) { }

  ngOnInit() {
    this.welcome = this.userService.isLoggedIn ? 'Welcome, ' + this.userService.user.name :
                    'Please log in.';
  }

}

Test Spec:

import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { UserService } from './model';
import { WelcomeComponent } from './welcome.component';

describe('WelcomeComponent', () => {

        let comp: WelcomeComponent;
        let fixture: ComponentFixture<WelcomeComponent>;
        let componentUserService: UserService; // actually injected service
        let userService: UserService; // testbed injected service
        let de: DebugElement; // debugelement with welcome message
        let el: HTMLElement; // dom element with welcome message

        let userServiceStub: {
          isLoggedIn: boolean;
          user: { name: string }
        };
        beforeEach(() => {

        // stub userservice for test purposes
        userServiceStub = {
          isLoggedIn: true,
          user: { name: 'Test User' }
        };

        TestBed.configureTestingModule({
          declarations: [ WelcomeComponent ],
          // providers: [ UserService ], NO don't provide real service! provide test-double instead
          providers: [{ provide: UserService, useValue: userServiceStub }]
        });

        fixture = TestBed.createComponent(WelcomeComponent);
        comp = fixture.componentInstance;

        // userservice actually injected into component
        userService = fixture.debugElement.injector.get(UserService);
        componentUserService = userService;
        // userservice from root injector
        userService = TestBed.get(UserService);

        // get welcome element by css selector (e.g., by class name)
        de = fixture.debugElement.query(By.css('.welcome'));
        el = de.nativeElement;
      });

      it('should welcome the user', () => {
          fixture.detectChanges();
          const content = el.textContent;
          expect(content).toContain('Welcome', '"Welcome ..."');
          expect(content).toContain('Test User', 'expected name');
      });

      it('should welcome "Polymer Princess"', () => {
          userService.user.name = 'Polymer Princess'; // welcome message hasn't been shown yet
          fixture.detectChanges();
          expect(el.textContent).toContain('Polymer Princess');
      });

      it('should request login if not logged in', () => {
          userService.isLoggedIn = false; // welcome message hasn't been shown yet
          fixture.detectChanges();
          const content = el.textContent;
          expect(content).not.toContain('Welcome', 'not welcomed');
          expect(content).toMatch(/log in/i, '"log in"');
      });

      it('should inject the component\'s user service instance',
          inject([UserService], (service: UserService) => {
            expect(service).toBe(componentUserService);
          }));

      it('TestBed and Component UserService should be the same', () => {
          expect(userService === componentUserService).toBe(true);
      });

      it('stub object and injected User Service should not be the same', () => {
          expect(userServiceStub === userService).toBe(false);
          // changing the stub object has no effect on the injected service
          userServiceStub.isLoggedIn = false;
          expect(userService.isLoggedIn).toBe(true);
      });
});

Testing Component with Async Service

LinuxService:

import { Injectable } from '@angular/core';

const quotes = [
'We are using Linux daily to UP our productivity - so UP yours!',
'The Linux philosophy is "laugh in the face of danger". Oops. Wrong one. "Do it yourself".',
'Linux is the choice of a GNU generation.',
'Only wimps use tape backup: _real_men just upload the important stuff on ftp, and let the rest of the world mirror it.',
'Be warned that typing killall name may not have the desired effect on non-Linux systems, especially when done by a privileged user.',
'A multithreaded file system is only a performance hack.',
'Anyone can build a fast processor, the trick is to build a fast system.',
'If Bill Gates is the Devil then Linus Torvalds must be the Messiah.',
'All the best people in Life seem to like Linux.',
'I trust my family jewels only to Linux.',
'Making Linux GPL\'d was definitely the best thing I ever did.'
];

@Injectable()
export class LinuxService {
  private next = 0;

  getQuote(): Promise<string> {
    return new Promise(resolve => {
        setTimeout(() => resolve(this.nextQuote()), 500);
        });
    }

  private nextQuote() {
    if (this.next === quotes.length) { this.next = 0; }
    return quotes[ this.next++ ];
  }

}

LinuxComponent:

import { Component, OnInit } from '@angular/core';

import { LinuxService } from './linux.service';

@Component({
  selector: 'app-linux-quote',
  template: '<p class="linux"><i>{{ quote }}</i></p>'
})
export class LinuxComponent implements OnInit {
  quote = '...';
  constructor(private linuxService: LinuxService) { }

  ngOnInit() {
    this.linuxService.getQuote().then(quote => this.quote = quote);
  }

}

Test Specs:

import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { LinuxService } from './linux.service';
import { LinuxComponent } from './linux.component';

describe('LinuxComponent', () => {

    let comp: LinuxComponent;
    let fixture: ComponentFixture<LinuxComponent>;

    let spy: jasmine.Spy;
    let de: DebugElement;
    let el: HTMLElement;
    let linuxService: LinuxService; // actually injected service

    const myFavoriteQuote = 'Software is like sex, it\'s better when it\'s free.';

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

      fixture = TestBed.createComponent(LinuxComponent);
      comp = fixture.componentInstance;

      // LinuxService actually injected into component
      linuxService = fixture.debugElement.injector.get(LinuxService);

      // setup spy on getquote method
      spy = spyOn(linuxService, 'getQuote').and.returnValue(Promise.resolve(myFavoriteQuote));

      // get linux quote element by CSS selector(e.g., by class name)
      de = fixture.debugElement.query(By.css('.linux'));
      el = de.nativeElement;
    });

    it('should not show a quote before OnInit', () => {
      expect(el.textContent).toBe('', 'nothing displayed');
      expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
    });

    it('should still not show quote after component initialized', () => {
      fixture.detectChanges();
      // getQuote service is async => still has not returned with quote
      expect(el.textContent).toBe('...', 'no quote yet');
      expect(spy.calls.any()).toBe(true, 'getQuote called');
    });

    it('should show quote after getQuote promise (async)', async(() => {
      fixture.detectChanges();

      fixture.whenStable().then(() => {                   // wait for async getQuote
          fixture.detectChanges();                        // update view with quote
          expect(el.textContent).toBe(myFavoriteQuote);
        });
      }));

    it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      expect(el.textContent).toBe(myFavoriteQuote);
    }));

    it('should show quote after getQuote promise (done)', (done: any) => {
      fixture.detectChanges();
      // get spy promise and wait for it to resolve
      spy.calls.mostRecent().returnValue.then(() => {
        fixture.detectChanges(); // update view with quote
        expect(el.textContent).toBe(myFavoriteQuote);
        done();
        });
      });

});

Let's view the results of ng test in our Chromium Browser:

Test Component with Inputs and Outputs:

DashboardTechnologyComponent:

import { Component, EventEmitter, Input, Output } from '@angular/core';

import { Technology } from '../model';

@Component({
  selector: 'app-dashboard-technology',
  template: `
              <div (click)="click()" class="technology">
                    {{ technology.name | uppercase }}
              </div>
  `,
  styleUrls: ['./dashboard-technology.component.css']
})
export class DashboardTechnologyComponent {
  @Input() technology: Technology;
  @Output() selected = new EventEmitter<Technology>();

  click() { this.selected.emit(this.technology); }

}

Test Specs:

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

import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

import { addMatchers, click } from '../../testing';

import { Technology } from '../model/technology';
import { DashboardTechnologyComponent } from './dashboard-technology.component';

beforeEach( addMatchers );

describe('DashboardTechnologyComponent when tested directly', () => {

  let comp: DashboardTechnologyComponent;
  let expectedTechnology: Technology;
  let fixture: ComponentFixture<DashboardTechnologyComponent>;
  let technologyEl: DebugElement;

  // async beforeeach
  beforeEach( async() => {
    TestBed.configureTestingModule({
      declarations: [ DashboardTechnologyComponent ]
    })
    .compileComponents(); // compile template and css
  });

  // synchronous beforeeach
  beforeEach(() => {
    fixture = TestBed.createComponent(DashboardTechnologyComponent);
    comp = fixture.componentInstance;
    technologyEl = fixture.debugElement.query(By.css('.technology')); // find technology element

    // pretend it was wired to something that supplies a technology
    expectedTechnology = new Technology(42, 'Test Future Killer Technology');
    comp.technology = expectedTechnology;
    fixture.detectChanges(); // trigger initial data binding
  });

  it('should display technology name', () => {
    const expectedPipedName = expectedTechnology.name.toUpperCase();
    expect(technologyEl.nativeElement.textContent).toContain(expectedPipedName);
  });

  it('should raise selected event when clicked', () => {
    let selectedTechnology: Technology;
    comp.selected.subscribe((technology: Technology) => selectedTechnology = technology);
    technologyEl.triggerEventHandler('click', null);
    expect(selectedTechnology).toBe(expectedTechnology);
  });

  it('should raise selected event when clicked (click helper)', () => {
    let selectedTechnology: Technology;
    comp.selected.subscribe((technology: Technology) => selectedTechnology = technology);
    click(technologyEl); // triggereventhandler helper
    expect(selectedTechnology).toBe(expectedTechnology);
  });
});

  describe('DashboardTechnologyComponent when inside a test host', () => {
    let testHost: TestHostComponent;
    let fixture: ComponentFixture<TestHostComponent>;
    let technologyEl: DebugElement;

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

    beforeEach(() => {
      fixture = TestBed.createComponent(TestHostComponent);
      testHost = fixture.componentInstance;
      technologyEl = fixture.debugElement.query(By.css('.technology'));
      fixture.detectChanges();
    });

    it('should display technology name', () => {
      const expectedPipedName = testHost.technology.name.toUpperCase();
      expect(technologyEl.nativeElement.textContent).toContain(expectedPipedName);
    });

    it('should raise selected event when clicked', () => {
      click(technologyEl);
      expect(testHost.selectedTechnology).toBe(testHost.technology);
    });

});

////// Test Host Component //////
import { Component } from '@angular/core';
@Component({
  template: `<app-dashboard-technology [technology]="technology" (selected)="onSelected($event)">
             </app-dashboard-technology>`
})
class TestHostComponent {
  technology = new Technology(42, 'Test Future Killer Technology');
  selectedTechnology: Technology;
  onSelected(technology: Technology) { this.selectedTechnology = technology; }
}

Let's view the test Results in Chromium:

Let's have a look at the running App in our Chromium Browser:

Test Routed Component:

ABC.XYZ 123

results matching ""

    No results matching ""