Slide 1

Slide 1 text

Spectacular Angular feature tests Angular Tiny Conf 2022

Slide 2

Slide 2 text

Angular’s testing capabilities

Slide 3

Slide 3 text

Karma Angular is developed with testability in mind Dependency injection Angular TestBed CDK Component Harnesses

Slide 4

Slide 4 text

Angular’s built-in testing providers Router TestingModule provideHttpClient Testing() MatIcon TestingModule

Slide 5

Slide 5 text

Angular’s RouterTestingModule Routed components Routing components Route guards

Slide 6

Slide 6 text

Clear box testing and mystery box testing

Slide 7

Slide 7 text

Clear box testing • Intricate awareness of implementation details • Drives code design • Documents and exercises low-level behavior • Often used for isolated unit tests Illustration by macrovector

Slide 8

Slide 8 text

Mystery box testing • Unaware of individual system parts • Verifies system behavior • Exercises implementation with little to no test doubles • Often used for end-to-end tests Illustration by macrovector

Slide 9

Slide 9 text

Angular component tests are slow

Slide 10

Slide 10 text

Angular component tests are slow

Slide 11

Slide 11 text

Angular component tests are fast 🌿

Slide 12

Slide 12 text

Angular component tests are fast 🌿 but focus on implementation details

Slide 13

Slide 13 text

Implementation details in component tests New up component or use Angular testbed? Replace dependencies with test doubles Trigger change detection

Slide 14

Slide 14 text

Implementation details in component tests Control time Refactor without breaking tests Configure the Angular testing module

Slide 15

Slide 15 text

Angular component tests are fast 🌿

Slide 16

Slide 16 text

Angular component tests are fast 🌿 but complicated to write

Slide 17

Slide 17 text

Complicated techniques in component tests Shallow or integrated test? Link up declarable dependencies Trigger DOM and component events

Slide 18

Slide 18 text

Trigger event or call component method? Complicated techniques in component tests Query the component tree DebugElements or native elements?

Slide 19

Slide 19 text

Create test host component Complicated techniques in component tests Route setup Navigate across routes and components

Slide 20

Slide 20 text

End-to-end tests are hard to write

Slide 21

Slide 21 text

End-to-end tests are hard to write

Slide 22

Slide 22 text

End-to-end tests are easy to write

Slide 23

Slide 23 text

End-to-end tests Ease of use Interact and observe as a user Reuse test setup Little to no use for test doubles

Slide 24

Slide 24 text

End-to-end tests are easy to write

Slide 25

Slide 25 text

End-to-end tests are easy to write but the feedback loop is less than ideal

Slide 26

Slide 26 text

End-to-end tests Feedback loop End-to-end tests are SLOW End-to-end tests are flaky

Slide 27

Slide 27 text

End-to-end tests are powerful

Slide 28

Slide 28 text

End-to-end tests are powerful Exercise complete userflows Refactor without breaking tests

Slide 29

Slide 29 text

End-to-end tests are powerful

Slide 30

Slide 30 text

End-to-end tests are powerful but rigid

Slide 31

Slide 31 text

End-to-end tests Rigidness Might not be able to replace native APIs Might not support all native browser features Might not support existing testing toolchains

Slide 32

Slide 32 text

End-to-end tests Rigidness Cannot replace application parts with test doubles

Slide 33

Slide 33 text

Goldilocks as our test coach Getting the best of both worlds

Slide 34

Slide 34 text

Goldilocks’ choice Repeatable tests Simple and reusable setup Pretty fast tests

Slide 35

Slide 35 text

Goldilocks’ choice Mystery box feature testing Interact and observe as a user Exercise complete userflows

Slide 36

Slide 36 text

Say hello

Slide 37

Slide 37 text

Say hello to Spectacular

Slide 38

Slide 38 text

Say hello to Spectacular … and Angular Testing Library

Slide 39

Slide 39 text

Say hello to Spectacular … and Angular Testing Library … and Cypress

Slide 40

Slide 40 text

Spectacular @ngworker/spectacular An Angular integration testing library with 3 APIs: • Application testing API • Feature testing API • Pipe testing API

Slide 41

Slide 41 text

Spectacular Feature testing API Test routed Angular features Provide feature- aware navigation services Reusable test root component

Slide 42

Slide 42 text

Angular Testing Library integration CDK Component Harnesses integration Spectacular Feature testing API Cypress component test runner integration

Slide 43

Slide 43 text

Angular feature tests

Slide 44

Slide 44 text

Angular feature tests • Test a routed Angular feature as a mystery box • Interact and verify as a user • Exercise complete userflows across routes and components Diagram inspired by Martin Fowler

Slide 45

Slide 45 text

Angular feature tests • Test a routed Angular feature as a mystery box • Interact and verify as a user • Exercise complete userflows across routes and components

Slide 46

Slide 46 text

Angular feature tests • Test a routed Angular feature as a mystery box • Interact and verify as a user • Exercise complete userflows across routes and components

Slide 47

Slide 47 text

Angular feature tests are fast and independent No need for a host application Replace dependencies if needed Orders of magnitude faster than E2E tests

Slide 48

Slide 48 text

Angular feature tests Using Spectacular and Angular Testing Library

Slide 49

Slide 49 text

it('Edit crisis name from crisis detail', async () => { // Arrange 1st route const { location, router, user } = await setup(); const crisisId = 2; await router.navigate(['~', crisisId]); // Act on 1st route await user.clear(await findNameControl()); await user.type(await findNameControl(), 'The global temperature is rising'); await user.click(await findSaveButton()); // Assert on 2nd route expect(await findSelectedCrisis(/the global temperature is rising/i)) .toBeInTheDocument(); expect(location.path()).toBe(`~/;id=${crisisId};foo=foo`); }); Edit crisis from detail page feature test using SIFERS

Slide 50

Slide 50 text

it('Edit crisis name from crisis center home', async () => { // Arrange 1st route const { router, user } = await setup(); await router.navigateByUrl('~/’); // Act on 1st route await user.click(await findCrisisLink(/procrastinators meeting delayed again/i)); // Act on 2nd route await user.clear(await findNameControl()); await user.type(await findNameControl(), 'Coral reefs are dying'); await user.click(await findSaveButton()); // Assert on 3rd route expect(await findCrisisCenterHomeGreeting()).toBeInTheDocument(); expect( await findSelectedCrisis(/coral reefs are dying/i) ).toBeInTheDocument(); }); Edit crisis from home page feature test using SIFERS

Slide 51

Slide 51 text

import { screen } from '@testing-library/angular'; import { Matcher } from '@testing-library/dom'; const findCrisisCenterGreeting = () => screen.findByText(/welcome to the crisis center/i); const findCrisisLink = (name: Exclude) => screen.findByRole('link', { name, }); const findNameControl = () => screen.findByPlaceholderText(/name/i); const findSaveButton = () => screen.findByRole('button', { name: /save/i }); const findSelectedCrisis = (name: Matcher) => screen.findByText(name, { selector: '.selected a', }); Named queries for Edit crisis feature tests

Slide 52

Slide 52 text

import { provideSpectacularFeatureTest, SpectacularAppComponent, SpectacularFeatureLocation, SpectacularFeatureRouter, } from '@ngworker/spectacular'; import { render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { crisisCenterPath, crisisCenterRoutes } from '@tour-of-heroes/crisis-center'; const setup = async () => { const user = userEvent.setup(); const { fixture: { debugElement: { injector } } } = await render( SpectacularAppComponent, { providers: [ provideSpectacularFeatureTest ({ featurePath: crisisCenterPath, }), ], routes: crisisCenterRoutes, }); return { location: injector.get(SpectacularFeatureLocation), router: injector.get(SpectacularFeatureRouter), user, }; }; SIFERS for Edit crisis feature tests

Slide 53

Slide 53 text

Angular feature tests Using Spectacular and the Cypress component test runner

Slide 54

Slide 54 text

it('Edit crisis name from crisis detail', () => { setup().then(async ({ location, ngZone, router }) => { // Arrange 1st route const crisisId = 2; await ngZone.run(() => router.navigate([crisisCenterPath, crisisId])); // Act on 1st route findNameControl() .clear({ force: true }) .type('The global temperature is rising'); findSaveButton().click(); // Assert on 2nd route findSelectedCrisis(/the global temperature is rising/i) .should('be.visible') .then(() => { expect(location.path()).to.deep.equal( `/${crisisCenterPath};id=${crisisId};foo=foo` ); }); }); }); Edit crisis from detail page feature test using SIFERS

Slide 55

Slide 55 text

it('Edit crisis name from crisis center home', () => { setup().then(() => { // Act on 1st route findCrisisLink(/procrastinators meeting delayed again/i).click(); // Act on 2nd route findNameControl().clear({ force: true }).type('Coral reefs are dying'); findSaveButton().click(); // Assert on 3rd route findCrisisCenterHomeGreeting().should('be.visible'); findSelectedCrisis(/coral reefs are dying/i).should('be.visible'); }); }); Edit crisis from home page feature test using SIFERS

Slide 56

Slide 56 text

const findCrisisCenterHomeGreeting = () => cy.contains(/welcome to the crisis center/i); const findCrisisLink = (name: string | RegExp) => cy.get('a').contains(name); const findNameControl = () => cy.get('[placeholder="name"]'); const findSaveButton = () => cy.get('button').contains(/save/i); const findSelectedCrisis = (name: string | RegExp) => cy.get('.selected a').contains(name); Named queries for Edit crisis feature tests

Slide 57

Slide 57 text

import { provideSpectacularFeatureTest, SpectacularAppComponent, } from '@ngworker/spectacular’; import { crisisCenterPath, crisisCenterRoutes } from '@tour-of-heroes/crisis-center'; const setup = () => cy.mount(SpectacularAppComponent, { imports: [RouterTestingModule.withRoutes([crisisCenterRoute])], providers: [ provideSpectacularFeatureTest({ featurePath: crisisCenterPath }), ], }).then(async({ fixture: { debugElement: { injector } } }) => { const location = injector.get(Location); const ngZone = injector.get(NgZone); const router = injector.get(Router); await ngZone.run(() => router.navigate([crisisCenterPath])); return { location, ngZone, router, }; } ); SIFERS for Edit crisis feature tests

Slide 58

Slide 58 text

Conclusion

Slide 59

Slide 59 text

Angular testing strategy • Feature tests can replace most end-to-end tests but should not replace all of them • Feature tests should complement unit tests and component tests, not replace all of them • Feature tests do not aid in system design or documentation but support refactoring without breaking tests Illustration from Kent C. Dodds’ ”Testing JavaScript” course

Slide 60

Slide 60 text

Angular feature tests are Goldilocks’ choice Resilient to refactoring Adequately granular Orders of magnitude faster than E2E tests

Slide 61

Slide 61 text

Angular feature tests are Goldilocks’ choice High confidence in desired behavior Sufficiently isolated Interact and verify as a user

Slide 62

Slide 62 text

Angular feature tests are Goldilocks’ choice Shared simple test setup Unaware of implementation details Flexible when needed

Slide 63

Slide 63 text

Write feature tests. Because testing a single component is boring. —Lars Gyrup Brink Nielsen “

Slide 64

Slide 64 text

Thank you for attending Lars Gyrup Brink Nielsen @[email protected] ngworker.github.io/ngworker @ngworker/spectacular discord.gg/wwjhWyx7p8 Some icons courtesy of Flaticon