Slide 1

Slide 1 text

Angular testing Tech talks with Santosh

Slide 2

Slide 2 text

Angular testing fundamentals

Slide 3

Slide 3 text

💬 Nacho Vázquez “What path would you recommend for a beginner making their way into testing?”

Slide 4

Slide 4 text

Recommended resources for newcomers Free text-based online workshops: • AURENA-Tech/angular-testing-exercises by Christian Janker • jevvilla/workshop-angular-testing by Juan Esteban & Juan Herrera • victor-mejia/angular-testing-workshop by Victor Mejia Free online text and video guides: • Angular.io: Developer Guides > Testing by the Angular team • CodeCraft.tv: Angular > Unit Testing by Asim Hussain

Slide 5

Slide 5 text

Recommended book for Angular testing Testing Angular Applications, Manning Publications by: • Jesse Palmer • Corinna Cohn • Michael Giambalvo • Craig Nishina With foreword by: • Brad Green

Slide 6

Slide 6 text

What you should learn General testing knowledge • Testing frameworks • Test suites • Test cases • Test steps • Test hooks • Assertions • Test doubles • Arrange-Act-Assert or Given-When-Then

Slide 7

Slide 7 text

Unit testing frameworks • AVA • Intern • Jasmine • Jest • Karma • Mocha • QUnit • Tape • Test’em

Slide 8

Slide 8 text

End-to-end testing frameworks • CodeceptJS • Cypress • Intern • Nightwatch.js • Playwright • Protractor • Puppeteer • Selenium • TestCafe • WebdriverIO

Slide 9

Slide 9 text

describe(LumberjackLogDriverLogger.name, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [LumberjackModule.forRoot(), SpyDriverModule.forRoot()], }); [logDriver] = (resolveDependency(lumberjackLogDriverToken) as unknown) as [SpyDriver]; logFactory = resolveDependency(LumberjackLogFactory); logger = resolveDependency(LumberjackLogDriverLogger); }); let logDriver: SpyDriver; let logFactory: LumberjackLogFactory; let logger: LumberjackLogDriverLogger; it('logs a critical log driver log', () => { const logDriverLog: LumberjackLogDriverLog = { formattedLog: LumberjackLevel.Critical, log: logFactory.createCriticalLog(LumberjackLevel.Critical).build(), }; logger.log(logDriver, logDriverLog); expect(logDriver.logCritical).toHaveBeenNthCalledWith(1, logDriverLog); }); });

Slide 10

Slide 10 text

describe(LumberjackLogDriverLogger.name, () => { // 👈 Test suite beforeEach(() => { // 👈 Test hook TestBed.configureTestingModule({ imports: [LumberjackModule.forRoot(), SpyDriverModule.forRoot()], }); [logDriver] = (resolveDependency(lumberjackLogDriverToken) as unknown) as [SpyDriver]; logFactory = resolveDependency(LumberjackLogFactory); logger = resolveDependency(LumberjackLogDriverLogger); }); let logDriver: SpyDriver; // 👈 Test double let logFactory: LumberjackLogFactory; let logger: LumberjackLogDriverLogger; it('logs a critical log driver log', () => { // 👈 Test case const logDriverLog: LumberjackLogDriverLog = { formattedLog: LumberjackLevel.Critical, log: logFactory.createCriticalLog(LumberjackLevel.Critical).build(), }; logger.log(logDriver, logDriverLog); // 👈 Test step expect(logDriver.logCritical).toHaveBeenNthCalledWith(1, logDriverLog); // 👈 Assertion }); });

Slide 11

Slide 11 text

describe(LumberjackLogDriverLogger.name, () => { it('logs a critical log driver log', () => { // 👇 Arrange const logDriverLog: LumberjackLogDriverLog = { formattedLog: LumberjackLevel.Critical, log: logFactory.createCriticalLog(LumberjackLevel.Critical).build(), }; // 👇 Act logger.log(logDriver, logDriverLog); // 👇 Assert expect(logDriver.logCritical).toHaveBeenNthCalledWith(1, logDriverLog); }); });

Slide 12

Slide 12 text

describe(LumberjackLogDriverLogger.name, () => { it('logs a critical log driver log', () => { // 👇 Given const logDriverLog: LumberjackLogDriverLog = { formattedLog: LumberjackLevel.Critical, log: logFactory.createCriticalLog(LumberjackLevel.Critical).build(), }; // 👇 When logger.log(logDriver, logDriverLog); // 👇 Then expect(logDriver.logCritical).toHaveBeenNthCalledWith(1, logDriverLog); }); });

Slide 13

Slide 13 text

What you should learn Angular testing knowledge • Types of Angular tests • Angular-specific tests • Use the platform: • TestBed • RouterTestingModule

Slide 14

Slide 14 text

Types of Angular tests • Unit tests • Snapshot tests • Component tests • Isolated component tests • Shallow component tests • Integrated component tests • Visual regression tests • Integration tests • End-to-end tests

Slide 15

Slide 15 text

Types of Angular tests End-to-end test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 16

Slide 16 text

Types of Angular tests End-to-end test with stubbed backend Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 17

Slide 17 text

Types of Angular tests Isolated component test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 18

Slide 18 text

Types of Angular tests Shallow component test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 19

Slide 19 text

Types of Angular tests Integrated component test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 20

Slide 20 text

Types of Angular tests Integration test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 21

Slide 21 text

Types of Angular tests Unit test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 22

Slide 22 text

Types of Angular tests Unit test Backend HeroesService HeroesContainer Component Heroes Component HeroesPresenter DOM

Slide 23

Slide 23 text

Angular-specific tests • Services • Providers • Injection tokens • Application hooks • Application initializers • Bootstrap listeners

Slide 24

Slide 24 text

Angular-specific tests • Angular modules • Feature modules • Service modules • UI modules • Shell modules

Slide 25

Slide 25 text

Angular-specific tests • Angular pipes • Directives • Attribute directives • Structural directives • Components • Presentational components • Container components • Mixed components • Routed components • Routing components

Slide 26

Slide 26 text

Angular-specific tests • Route guards • CanActivate • CanActivateChild • CanDeactivate • CanLoad • Route resolvers

Slide 27

Slide 27 text

Use the platform TestBed • Dependencies • Components • Directives • Angular pipes • Angular modules

Slide 28

Slide 28 text

Component tests Sources/targets of changes • URL/navigation • The DOM • Data binding API (input and output properties) • Services A typical component test’s structure 1. Configure the Angular testing module 2. Create component fixture 3. Set up test doubles and initial state 4. Trigger change from source 5. Assert on changed target

Slide 29

Slide 29 text

import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; describe(DashboardComponent.name, () => { beforeEach(() => { const routerSpy = { navigateByUrl: jest.fn() }; // 👈 [3] Set up test doubles const heroServiceSpy = {getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ // 👈 [1] Configure the Angular testing module declarations: [DashboardComponent, DashboardHeroComponent], providers: [ { provide: HeroService, useValue: heroServiceSpy }, { provide: Router, useValue: routerSpy }, ], }).compileComponents(); fixture = TestBed.createComponent(DashboardComponent); // 👈 [2] Create component fixture fixture.autoDetectChanges(true); // 👈 [4] Trigger change from source }); let fixture: ComponentFixture; it('displays heroes', () => { const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero')); expect(heroes).toHaveLength(4); // 👈 [5] Assert on changed target }); });

Slide 30

Slide 30 text

Use the platform RouterTestingModule • Feature modules • Routed components • Routing components • Route guards • Route resolvers

Slide 31

Slide 31 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; // 👈 import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { const heroServiceSpy = { getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ declarations: [TestAppComponent, TestHeroDetailComponent], imports: [RouterTestingModule.withRoutes([ // 👈 { path: 'heroes/:id', component: TestHeroDetailComponent }, { path: '', loadChildren: () => DashboardModule }, ])], providers: [{ provide: HeroService, useValue: heroServiceSpy }], }).compileComponents(); rootFixture = TestBed.createComponent(TestAppComponent); location = TestBed.inject(Location); rootFixture.ngZone?.run(() => TestBed.inject(Router).navigateByUrl('/dashboard')); rootFixture.autoDetectChanges(true); }); let location: Location; let rootFixture: ComponentFixture; });

Slide 32

Slide 32 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { const heroServiceSpy = { getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ declarations: [TestAppComponent, TestHeroDetailComponent /* 👈 */], imports: [RouterTestingModule.withRoutes([ { path: 'heroes/:id', component: TestHeroDetailComponent }, // 👈 { path: '', loadChildren: () => DashboardModule }, ])], providers: [{ provide: HeroService, useValue: heroServiceSpy }], }).compileComponents(); rootFixture = TestBed.createComponent(TestAppComponent); location = TestBed.inject(Location); rootFixture.ngZone?.run(() => TestBed.inject(Router).navigateByUrl('/dashboard')); rootFixture.autoDetectChanges(true); }); let location: Location; let rootFixture: ComponentFixture; });

Slide 33

Slide 33 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} // 👈 @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { const heroServiceSpy = { getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ declarations: [TestAppComponent /* 👈 */, TestHeroDetailComponent], imports: [RouterTestingModule.withRoutes([ { path: 'heroes/:id', component: TestHeroDetailComponent }, { path: '', loadChildren: () => DashboardModule }, ])], providers: [{ provide: HeroService, useValue: heroServiceSpy }], }).compileComponents(); rootFixture = TestBed.createComponent(TestAppComponent); // 👈 location = TestBed.inject(Location); rootFixture.ngZone?.run(() => TestBed.inject(Router).navigateByUrl('/dashboard')); rootFixture.autoDetectChanges(true); }); let location: Location; let rootFixture: ComponentFixture; });

Slide 34

Slide 34 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { const heroServiceSpy = { getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ declarations: [TestAppComponent, TestHeroDetailComponent], imports: [RouterTestingModule.withRoutes([ { path: 'heroes/:id', component: TestHeroDetailComponent }, { path: '', loadChildren: () => DashboardModule }, ])], providers: [{ provide: HeroService, useValue: heroServiceSpy }], }).compileComponents(); rootFixture = TestBed.createComponent(TestAppComponent); location = TestBed.inject(Location); // 👈 rootFixture.ngZone?.run(() => TestBed.inject(Router).navigateByUrl('/dashboard')); rootFixture.autoDetectChanges(true); }); let location: Location; // 👈 let rootFixture: ComponentFixture; });

Slide 35

Slide 35 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { const heroServiceSpy = { getHeroes: jest.fn().mockReturnValue(asyncData(getTestHeroes())) }; TestBed.configureTestingModule({ declarations: [TestAppComponent, TestHeroDetailComponent], imports: [RouterTestingModule.withRoutes([ { path: 'heroes/:id', component: TestHeroDetailComponent }, { path: '', loadChildren: () => DashboardModule }, ])], providers: [{ provide: HeroService, useValue: heroServiceSpy }], }).compileComponents(); rootFixture = TestBed.createComponent(TestAppComponent); location = TestBed.inject(Location); rootFixture.ngZone?.run(() => TestBed.inject(Router).navigateByUrl('/dashboard')); // 👈 rootFixture.autoDetectChanges(true); // 👈 }); let location: Location; let rootFixture: ComponentFixture; });

Slide 36

Slide 36 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; // 👈 import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { // (...) }); let location: Location; let rootFixture: ComponentFixture; // 👈 it('navigates to hero details when a hero is clicked', ) => { const heroDebugger = rootFixture.debugElement.query( By.directive(DashboardHeroComponent)); // 👈 const heroElement: HTMLElement = heroDebugger.query( By.css('.hero')).nativeElement; // 👈 heroElement.click(); const clickedHero = heroDebugger.componentInstance.hero; expect(location.path()).toBe('/heroes/' + clickedHero?.id); }); });

Slide 37

Slide 37 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { // (...) }); let location: Location; let rootFixture: ComponentFixture; it('navigates to hero details when a hero is clicked', ) => { const heroDebugger = rootFixture.debugElement.query( By.directive(DashboardHeroComponent)); const heroElement: HTMLElement = heroDebugger.query( By.css('.hero')).nativeElement; heroElement.click(); // 👈 const clickedHero = heroDebugger.componentInstance.hero; expect(location.path()).toBe('/heroes/' + clickedHero?.id); }); });

Slide 38

Slide 38 text

import { Location } from '@angular/common'; import { Component } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { DashboardHeroComponent } from './dashboard-hero.component'; import { DashboardComponent } from './dashboard.component'; import { DashboardModule } from './dashboard.module'; import { HeroService } from './hero.service'; import { asyncData, getTestHeroes } from './testing'; @Component({ template: '' }) class TestAppComponent {} @Component({ template: '' }) class TestHeroDetailComponent {} describe(DashboardComponent.name, () => { beforeEach(() => { // (...) }); let location: Location; // 👈 let rootFixture: ComponentFixture; it('navigates to hero details when a hero is clicked', ) => { const heroDebugger = rootFixture.debugElement.query( By.directive(DashboardHeroComponent)); const heroElement: HTMLElement = heroDebugger.query( By.css('.hero')).nativeElement; heroElement.click(); const clickedHero = heroDebugger.componentInstance.hero; expect(location.path()).toBe('/heroes/' + clickedHero?.id); // 👈 }); });

Slide 39

Slide 39 text

My RouterTestingModule article series on This is Angular • Testing Angular routing components with the RouterTestingModule • Testing routed Angular components with the RouterTestingModule • Testing Angular route guards with the RouterTestingModule

Slide 40

Slide 40 text

My article series on testing and faking dependencies • Testing and faking Angular dependencies • Faking dependencies in Angular applications • Tree-shakable dependencies in Angular projects

Slide 41

Slide 41 text

Angular testing strategy

Slide 42

Slide 42 text

💬 Vimal Patel “Advantages of unit test compared to integration test. In other words, how to choose which functionality we should cover in unit test and which one in integration test.”

Slide 43

Slide 43 text

💬 Ajit Singh “How to manage tests and some examples of how you manage them. Principles you follow while writing tests.”

Slide 44

Slide 44 text

Testing strategy ”What we test and how we test it.”

Slide 45

Slide 45 text

Unit test vs. integration test Event flow up the component tree. How do we test this flow?

Slide 46

Slide 46 text

Unit test vs. integration test Data flows down the component tree. How do we test this flow?

Slide 47

Slide 47 text

Unit test vs. integration test A complex UI feature. How do we test this dataflow?

Slide 48

Slide 48 text

Unit test vs. integration test The Remove hero control flow in Tour of Heroes. How do we test this?

Slide 49

Slide 49 text

Unit test vs. integration test The Add hero control flow in Tour of Heroes. How do we test this?

Slide 50

Slide 50 text

💬 C. Kettenbach “To Protractor or to Cypress. That is the question.”

Slide 51

Slide 51 text

💬 Vimal Patel “Which one to choose: Cypress or Protractor?”

Slide 52

Slide 52 text

End-to-end testing frameworks • CodeceptJS • Cypress • Intern • Nightwatch.js • Playwright • Protractor • Puppeteer • TestCafe • WebdriverIO

Slide 53

Slide 53 text

End-to-end testing Angular applications with Cypress Pros: • Big community • In active development by a company currently focusing on this product Cons: • Cypress’ assertion style is an acquired taste • No support for Internet Explorer, Microsoft Edge Legacy, or macOS Safari • Not focusing on Angular support: • Various issues related to testing Angular applications • No clean harness environment for Angular CDK Component Test Harnesses • Angular component tests not fully supported

Slide 54

Slide 54 text

End-to-end testing Angular applications with Protractor Pros: • Owned by the Angular team • Heavily used by Google • Supports legacy browsers, that is Internet Explorer and Microsoft Edge Legacy • Supports macOS Safari Cons: • Barely maintained for years • A generations behind on Selenium support • The Angular team is considering other alternatives going forward

Slide 55

Slide 55 text

Testing techniques

Slide 56

Slide 56 text

💬 Ajit Singh “How to write tests while keeping method extensions in mind. Methods can be extended and their inside workings can be changed. Should we write tests based only on the return statements or should we test the intermediate steps too?”

Slide 57

Slide 57 text

Testing extensible classes • Only test implementation details in case of side effects, that is commands sent to collaborators. • Verify inheritance and polymorphism.

Slide 58

Slide 58 text

@Injectable() export abstract class LumberjackLogger { constructor( protected lumberjack: LumberjackService, protected time: LumberjackTimeService ) {} /** Create a logger builder for a critical log with the specified message. */ protected createCriticalLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for a debug log with the specified message. */ protected createDebugLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for an error log with the specified message. */ protected createErrorLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for an info log with the specified message. */ protected createInfoLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for a trace log with the specified message. */ protected createTraceLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for a warning log with the specified message. */ protected createWarningLogger(message: string): LumberjackLoggerBuilder; /** Create a logger builder for a log with the specified log level and message. */ protected createLoggerBuilder( level: LumberjackLogLevel, message: string, ): LumberjackLoggerBuilder; }

Slide 59

Slide 59 text

@Injectable() export abstract class ScopedLumberjackLogger< TPayload extends LumberjackLogPayload | void = void > extends LumberjackLogger { abstract readonly scope: string; /** * Create a logger builder for a log with the shared scope as well as the * specified log level and message. */ protected createLoggerBuilder( level: LumberjackLogLevel, message: string, ): LumberjackLoggerBuilder; }

Slide 60

Slide 60 text

import { Injectable } from '@angular/core'; import { ScopedLumberjackLogger } from './scoped-lumberjack-logger.service'; @Injectable({ providedIn: 'root’ }) export class TestLogger extends ScopedLumberjackLogger { readonly scope = 'Test’; critical = (message: string): void => this.createCriticalLogger(message).build().call(this); debug = (message: string): void => this.createDebugLogger(message).build().call(this); error = (message: string): void => this.createErrorLogger(message).build().call(this); info = (message: string): void => this.createInfoLogger(message).build().call(this); trace = (message: string): void => this.createTraceLogger(message).build().call(this); warning = (message: string): void => this.createWarningLogger(message).build().call(this); }

Slide 61

Slide 61 text

describe(ScopedLumberjackLogger.name, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [LumberjackModule.forRoot()], providers: [{ provide: LumberjackTimeService, useClass: FakeTimeService }], }); logger = TestBed.inject(TestLogger); // 👈 logFactory = TestBed.inject(LumberjackLogFactory); const lumberjack = TestBed.inject(LumberjackService); lumberjackLogSpy = jest.spyOn(lumberjack, 'log').mockImplementation(() => {}); }); let logFactory: LumberjackLogFactory; let logger: TestLogger; // 👈 let lumberjackLogSpy: jest.SpyInstance]>; });

Slide 62

Slide 62 text

describe(ScopedLumberjackLogger.name, () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [LumberjackModule.forRoot()], providers: [{ provide: LumberjackTimeService, useClass: FakeTimeService }], }); logger = TestBed.inject(TestLogger); logFactory = TestBed.inject(LumberjackLogFactory); const lumberjack = TestBed.inject(LumberjackService); lumberjackLogSpy = jest.spyOn(lumberjack, 'log').mockImplementation(() => {}); // 👈 }); let logFactory: LumberjackLogFactory; let logger: TestLogger; let lumberjackLogSpy: jest.SpyInstance]>; // 👈 });

Slide 63

Slide 63 text

describe(ScopedLumberjackLogger.name, () => { let logFactory: LumberjackLogFactory; let logger: TestLogger; let lumberjackLogSpy: jest.SpyInstance]>; it('can create a critical logger', () => { const testMessage = 'Critical message’; logger.critical(testMessage); expect(lumberjackLogSpy).toHaveBeenCalledTimes(1); expect(lumberjackLogSpy).toHaveBeenCalledWith( logFactory.createCriticalLog(testMessage).withScope(logger.scope).build() ); }); it('can create a debug logger', () => { const testMessage = 'Debug message'; logger.debug(testMessage); expect(lumberjackLogSpy).toHaveBeenCalledTimes(1); expect(lumberjackLogSpy).toHaveBeenCalledWith( logFactory.createDebugLog(testMessage).withScope(logger.scope).build() ); }); });

Slide 64

Slide 64 text

💬 Ajit Singh “When to break down a test into smaller tests.”

Slide 65

Slide 65 text

Breaking a test into smaller steps Rule of thumb: • One interaction per test case • One assertion per test case • Follow the Arrange-Act-Assert pattern If the test is doing too much, the subject-under-test is probably doing too much or its design can be improved. Make sure that test cases are clear and concise.

Slide 66

Slide 66 text

💬 Ajit Singh “Which tests to skip, like I don't expect people to write tests for presentational components. What other cases can we skip or should we write tests for all components?”

Slide 67

Slide 67 text

💬 Gérôme Grignon “I currently question unit testing on dumb components, relying tests to visual testing with Storybook (and snapshots) and e2e testing. Any thoughts about that?”

Slide 68

Slide 68 text

Presentational components Is it really a presentational component? • Presents application state to the user • Changes application state triggered by user interaction • Uses the OnPush change detection strategy • No dependencies except presenters and content/view children • Only receives changes from input properties and the DOM • Minimal logic in its component template • Simple logic in its component model • Complex presentational logic extracted to presenters

Slide 69

Slide 69 text

Use-case specific presentational components Use case-specific presentational components which have a 1-to-1 relationship with a container component should only contain logic that’s barely worth testing. Instead, complex presentational logic is extracted to presenters which are easily tested as they don’t depend on Angular or even a DOM. Additionally, presenters enable presentational logic to be reused or shared.

Slide 70

Slide 70 text

Testing use-case specific presentational components When presentational components have minimal complexity, we can rely on static analysis, integration tests, and end-to-end tests to catch simple mistakes such as typos, type errors, and mapping errors.

Slide 71

Slide 71 text

Testing use-case specific presentational components We can use fake container components to test use case-specific presentational components. They provide realistic, static data to the presentational components.

Slide 72

Slide 72 text

Dynamic presentational components The data binding API of dynamic presentational components are usually not concerned about application state. Their most important traits are content projection or dynamic rendering in the form of: • Component outlets • Template outlets • Angular CDK portals • Simple content projection

Slide 73

Slide 73 text

Testing dynamic presentational components Tests for dynamic presentational component serve as documentation for their data binding API. These tests use a host component to verify the expected behavior.

Slide 74

Slide 74 text

Reusable presentational components Common examples of resusable presentational components are atoms, molecules, and organisms in the Atomic Design methodology. They are often part of a design system or a UI component library.

Slide 75

Slide 75 text

Testing reusable presentational components with Storybook We can use fake container components to test reusable presentational components. This technique can also be used in combination with Storybook or Angular Playground. Storybook can be combined with end-to-end testing. Make sure to watch Nx Tutorial: High Quality React apps with Nx, Storybook & Cypress (timestamp 12:20) by Nrwl.

Slide 76

Slide 76 text

Testing reusable presentational components with Storybook We can test reusable presentational components by using snapshot tests or visual regression tests. Visual regression tests can be combined with Storybook. Storybook is great for manual testing and exploration of reusable presentational components, but do you really want to test every feature and behavior manually for every merge/release?

Slide 77

Slide 77 text

Testing reusable presentational components with TestBed TestBed is perfectly fine for testing reusable presentational components. These tests require no stubbed dependencies. Input properties Either use a host component or set values on input properties imperatively. Output properties Output properties are not convenient to test without a host component as we then need to treat them as observables.

Slide 78

Slide 78 text

Angular testing libraries

Slide 79

Slide 79 text

💬 Ajit Singh “Best utils to use to reduce testbed code and framework test boilerplate.”

Slide 80

Slide 80 text

Reducing framework boilerplate • Angular CDK Component Test Harnesses, by the Angular Components team • Angular Testing Library, member of the Testing Library family • ng-mocks • ng-unit • ngx-speculoos by Ninja Squad • Plumbline • shallow-render • Spectacular, upcoming member of the @ngworker family • Spectator, member of the @ngneat family

Slide 81

Slide 81 text

💬 Gérôme Grignon “Do you plan to talk about Spectacular?”

Slide 82

Slide 82 text

Introducing Spectacular “Spectacular Angular integration testing.” Spectacular is an integration testing library which offers test harnesses to Angular applications and libraries. • Application testing API • Feature testing API • Pipe testing API Logo by Felipe Zambrano

Slide 83

Slide 83 text

Spectacular Application testing API The application test harness is used to test: • Application initializers • Bootstrap listeners • Configuration Angular modules

Slide 84

Slide 84 text

import { APP_BOOTSTRAP_LISTENER, FactoryProvider, NgModule } from '@angular/core’; import { createApplicationHarness } from '@ngworker/spectacular'; // 👈 let bootstrapped = false; const bootstrapListener: FactoryProvider = { multi: true, provide: APP_BOOTSTRAP_LISTENER, useFactory: () => () => { bootstrapped = true; }, }; describe('Bootstrap listeners', () => { beforeEach(() => { bootstrapped = false; }); it('registers and runs the specified bootstrap listener', () => { createApplicationHarness({ // 👈 providers: [bootstrapListener], }); expect(bootstrapped).toBe(true); }); });

Slide 85

Slide 85 text

import { APP_BOOTSTRAP_LISTENER, FactoryProvider, NgModule } from '@angular/core’; import { createApplicationHarness } from '@ngworker/spectacular'; let bootstrapped = false; const bootstrapListener: FactoryProvider = { multi: true, provide: APP_BOOTSTRAP_LISTENER, // 👈 useFactory: () => () => { bootstrapped = true; }, }; describe('Bootstrap listeners', () => { beforeEach(() => { bootstrapped = false; }); it('registers and runs the specified bootstrap listener', () => { createApplicationHarness({ providers: [bootstrapListener], // 👈 }); expect(bootstrapped).toBe(true); }); });

Slide 86

Slide 86 text

import { APP_BOOTSTRAP_LISTENER, FactoryProvider, NgModule } from '@angular/core’; import { createApplicationHarness } from '@ngworker/spectacular'; let bootstrapped = false; // 👈 const bootstrapListener: FactoryProvider = { multi: true, provide: APP_BOOTSTRAP_LISTENER, useFactory: () => () => { bootstrapped = true; // 👈 }, }; describe('Bootstrap listeners', () => { beforeEach(() => { bootstrapped = false; }); it('registers and runs the specified bootstrap listener', () => { createApplicationHarness({ providers: [bootstrapListener], }); expect(bootstrapped).toBe(true); // 👈 }); });

Slide 87

Slide 87 text

Spectacular Feature testing API The feature testing API is the crown jewel of Spectacular. It bridges the gap between component tests and end-to-end tests. It has the speed and utilities of TestBed with the test-as-a-user approach of end-to-end testing frameworks. Spectacular’s feature testing API can be integrated with Angular CDK component harness and Angular Testing Library.

Slide 88

Slide 88 text

Spectacular Feature testing API The feature test harness is used to test: • Routed feature modules • Shell modules Spectacular’s feature test harness configures RouterTestingModule and manages its related test setup.

Slide 89

Slide 89 text

Spectacular Feature testing API A feature test harness provides SpectacularFeatureRouter and SpectacularFeatureLocation which are both subsets of Angular services adjusted to the feature module under test to get feature-relative navigation and URL path evaluation. Example usage const harness = createFeatureHarness({ // 👈 featureModule: CrisisCenterModule, featurePath: 'crisis-center', }); harness.router.navigateByUrl('~/details'); // 👈 expect(harness.location.path()).toBe('~/details'); // 👈

Slide 90

Slide 90 text

Spectacular Feature testing API A feature test harness provides SpectacularFeatureRouter and SpectacularFeatureLocation which are both subsets of Angular services adjusted to the feature module under test to get feature-relative navigation and URL path evaluation. Example usage const harness = createFeatureHarness({ featureModule: CrisisCenterModule, featurePath: 'crisis-center', }); const angularRouter = harness.inject(Router); // 👈 angularRouter.navigateByUrl('/crisis-center/details'); // 👈 expect(harness.location.path()).toBe('~/details');

Slide 91

Slide 91 text

Spectacular Feature testing API A feature test harness provides SpectacularFeatureRouter and SpectacularFeatureLocation which are both subsets of Angular services adjusted to the feature module under test to get feature-relative navigation and URL path evaluation. Example usage const harness = createFeatureHarness({ featureModule: CrisisCenterModule, featurePath: 'crisis-center', }); const angularLocation = harness.inject(Location); // 👈 harness.router.navigateByUrl('~/details'); expect(angularLocation.path()).toBe('/crisis-center/details'); // 👈

Slide 92

Slide 92 text

import { createUserInteractions, FakeDialogService, UserInteractions } from '@internal/test-util'; import { createFeatureHarness, SpectacularFeatureHarness } from '@ngworker/spectacular'; // 👈 import { Crisis, CrisisCenterModule, crisisCenterPath, CrisisService, DialogService, } from '@tour-of-heroes/crisis-center'; describe('Tour of Heroes: Crisis center integration tests', () => { beforeEach(() => { harness = createFeatureHarness({ // 👈 featureModule: CrisisCenterModule, featurePath: crisisCenterPath, providers: [{ provide: DialogService, useClass: FakeDialogService }], }); fakeDialog = harness.inject(DialogService) as FakeDialogService; ui = createUserInteractions(harness.rootFixture); [aCrisis] = harness.inject(CrisisService).getCrises().value; }); let aCrisis: Crisis; let fakeDialog: FakeDialogService; let harness: SpectacularFeatureHarness; // 👈 const newCrisisName = 'Coral reefs are dying'; let ui: UserInteractions; });

Slide 93

Slide 93 text

import { createUserInteractions, FakeDialogService, UserInteractions } from '@internal/test-util'; import { createFeatureHarness, SpectacularFeatureHarness } from '@ngworker/spectacular'; // 👈 import { Crisis, CrisisCenterModule, crisisCenterPath, CrisisService, DialogService, } from '@tour-of-heroes/crisis-center'; describe('Tour of Heroes: Crisis center integration tests', () => { beforeEach(() => { harness = createFeatureHarness({ featureModule: CrisisCenterModule, featurePath: crisisCenterPath, providers: [{ provide: DialogService, useClass: FakeDialogService }], }); fakeDialog = harness.inject(DialogService) as FakeDialogService; ui = createUserInteractions(harness.rootFixture); // 👈 [aCrisis] = harness.inject(CrisisService).getCrises().value; }); let aCrisis: Crisis; let fakeDialog: FakeDialogService; let harness: SpectacularFeatureHarness; const newCrisisName = 'Coral reefs are dying'; let ui: UserInteractions; // 👈 Demo testing utilities, not included in Spectacular });

Slide 94

Slide 94 text

describe('Tour of Heroes: Crisis center integration tests', () => { function expectCrisisToBeSelected(crisis: Crisis) { expect(ui.getText('.selected')).toBe(crisis.id + crisis.name); } function expectToBeAtTheCrisisCenterHome() { expect(ui.getText('p')).toBe('Welcome to the Crisis Center'); } // (...) let ui: UserInteractions; });

Slide 95

Slide 95 text

describe('Tour of Heroes: Crisis center integration tests', () => { describe('Crisis detail', () => { describe('Editing crisis name', () => { beforeEach(async () => { await harness.router.navigateByUrl('~/' + aCrisis.id); // 👈 ui.enterText(newCrisisName, 'input’); }); describe('Canceling change', () => { describe('When discarding unsaved changes is confirmed', () => { beforeEach(() => { ui.clickButton('Cancel'); fakeDialog.clickOk(); // 👈 }); it('navigates to the crisis center home with the crisis selected', () => { expectToBeAtTheCrisisCenterHome(); // 👈 expectCrisisToBeSelected(aCrisis); }); }); }); }); }); });

Slide 96

Slide 96 text

describe('Tour of Heroes: Crisis center integration tests', () => { describe('Crisis detail', () => { describe('Editing crisis name', () => { beforeEach(async () => { await harness.router.navigateByUrl('~/' + aCrisis.id); ui.enterText(newCrisisName, 'input’); }); describe('Canceling change', () => { describe('When discarding unsaved changes is confirmed', () => { beforeEach(() => { ui.clickButton('Cancel'); fakeDialog.clickOk(); // 👈 }); it('navigates to the crisis center home with the crisis selected', () => { expectToBeAtTheCrisisCenterHome(); expectCrisisToBeSelected(aCrisis); }); }); }); }); }); });

Slide 97

Slide 97 text

describe('Tour of Heroes: Crisis center integration tests', () => { describe('Crisis detail', () => { describe('Editing crisis name', () => { beforeEach(async () => { await harness.router.navigateByUrl('~/' + aCrisis.id); ui.enterText(newCrisisName, 'input’); }); describe('Canceling change', () => { describe('When discarding unsaved changes is confirmed', () => { beforeEach(() => { ui.clickButton('Cancel'); fakeDialog.clickOk(); }); it('navigates to the crisis center home with the crisis selected', () => { expectToBeAtTheCrisisCenterHome(); // 👈 expectCrisisToBeSelected(aCrisis); // 👈 }); }); }); }); }); });

Slide 98

Slide 98 text

Spectacular Pipe testing API The pipe test harness is used to test Angular pipes. Spectacular manages a host component and change detection for the pipe under test: • The rendered text is readable • The value can be changed • The template can be replaced

Slide 99

Slide 99 text

import { Pipe, PipeTransform } from '@angular/core'; import { createPipeHarness, SpectacularPipeHarness } from '@ngworker/spectacular'; // 👈 @Pipe({ name: 'capitalize' }) export class CapitalizePipe implements PipeTransform { transform(value: string) { return value.split(/\s+/g).map(word => word[0].toUpperCase() + word.substring(1)).join(' '); } } describe(CapitalizePipe.name, () => { beforeEach(() => { harness = createPipeHarness({// 👈 pipe: CapitalizePipe, pipeName: 'capitalize', value: 'mr. potato head’, }); }); let harness: SpectacularPipeHarness; // 👈 it('capitalizes every word of the text', () => { expect(harness.text).toBe('Mr. Potato Head'); // 👈 harness.value = 'ms. potato head' // 👈 expect(harness.text).toBe('Ms. Potato Head'); // 👈 }); });

Slide 100

Slide 100 text

Stay tuned for Specatular… Spectacular will be published as @ngworker/spectacular. (Until then, feel free to try it out from the build artifacts of our GitHub Actions workflow) Roadmap • Verified cross-version compatibility with Angular • Examples of integrating Spectacular’s feature testing API with Angular CDK component harnesses • Examples of integrating Spectacular’s feature testing API with Angular Testing Library • Container component test harness

Slide 101

Slide 101 text

Automated testing and legacy systems

Slide 102

Slide 102 text

💬 Armen Vardanyan “Is writing unit tests really increasing the time spent on development significantly, or does the early planning and relative bug safety compensate for the time we spend on writing unit tests in Angular? Lots of client companies don’t want developers writing unit tests because they think it will take too much time and they don't want to overpay.”

Slide 103

Slide 103 text

💬 Armen Vardanyan “TDD is nice and shiny. Lots of huge codebases out there don’t have tests already but would benefit from adding them. Is it worth trying to add unit tests to a large existing project? What is the best sequence of steps we can take to make it less painful?”

Slide 104

Slide 104 text

Thank you! 👋 🐦@LayZeeDK

Slide 105

Slide 105 text

No content