Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spectacular Angular feature tests

Spectacular Angular feature tests

Spectacular is the first Angular testing library to enable integration tests across an entire Angular feature. Feature test suites support refactoring your codebase as they don’t rely on implementation details. Combine Spectacular and Angular Testing Library to test as a user across an entire routed Angular feature. Verify complete user flows in a fraction of the time that it would take using an end-to-end testing framework.

Presented at:
- NG Poland, January 2022
- Angular Vienna, July 2022

4f349dbe1d48627445735f7e2c818c97?s=128

Lars Gyrup Brink Nielsen

January 25, 2022
Tweet

More Decks by Lars Gyrup Brink Nielsen

Other Decks in Programming

Transcript

  1. Spectacular Angular feature tests Angular Vienna

  2. Angular’s testing capabilities

  3. Karma Angular is developed with testability in mind Dependency injection

    Angular TestBed CDK Component Harnesses
  4. Angular’s built-in testing modules Router TestingModule HttpClient TestingModule MatIcon TestingModule

  5. Angular’s RouterTestingModule Routed components Routing components Route guards

  6. Clear box testing and mystery box testing

  7. 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
  8. 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
  9. Angular component tests are slow

  10. Angular component tests are slow

  11. Angular component tests are fast 🌿

  12. Angular component tests are fast 🌿 but focus on implementation

    details
  13. Implementation details in component tests New up component or use

    Angular testbed? Replace dependencies with test doubles Trigger change detection
  14. Implementation details in component tests Control time Refactor without breaking

    tests Configure the Angular testing module
  15. Angular component tests are fast 🌿

  16. Angular component tests are fast 🌿 but complicated to write

  17. Complicated techniques in component tests Shallow or integrated test? Link

    up declarable dependencies Trigger DOM and component events
  18. Trigger event or call component method? Complicated techniques in component

    tests Query the component tree DebugElements or native elements?
  19. Create test host component Complicated techniques in component tests Route

    setup Navigate across routes and components
  20. End-to-end tests are hard to write

  21. End-to-end tests are hard to write

  22. End-to-end tests are easy to write

  23. End-to-end tests Ease of use Interact and observe as a

    user Reuse test setup Little to no use for test doubles
  24. End-to-end tests are easy to write

  25. End-to-end tests are easy to write but the feedback loop

    is less than ideal
  26. End-to-end tests Feedback loop End-to-end tests are SLOW End-to-end tests

    are flaky
  27. End-to-end tests are powerful

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

    tests
  29. End-to-end tests are powerful

  30. End-to-end tests are powerful but rigid

  31. 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
  32. End-to-end tests Rigidness Cannot replace application parts with test doubles

  33. Goldilocks as our test coach Getting the best of both

    worlds
  34. Goldilocks’ choice Repeatable tests Simple and reusable setup Pretty fast

    tests
  35. Goldilocks’ choice Mystery box feature testing Interact and observe as

    a user Exercise complete userflows
  36. Say hello

  37. Say hello to Spectacular

  38. Say hello to Spectacular … and Angular Testing Library

  39. Spectacular @ngworker/spectacular An Angular integration testing library with 3 APIs:

    • Application testing API • Feature testing API • Pipe testing API
  40. Spectacular Feature testing API Test routed Angular features Provide feature-

    aware navigation services Reusable test root component
  41. Angular Testing Library integration CDK Component Harnesses integration Spectacular Feature

    testing API
  42. Angular feature tests

  43. 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
  44. Angular feature tests are fast and independent No need for

    a host application Replace dependencies if needed Orders of magnitude faster than E2E tests
  45. 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'; it('Edit crisis name from crisis detail', async () => { const user = userEvent.setup(); const { fixture: { debugElement: { injector } } } = await render( SpectacularAppComponent, { providers: [ provideSpectacularFeatureTest ({ featurePath: crisisCenterPath, }), ], routes: crisisCenterRoutes, }); const location = injector.get(SpectacularFeatureLocation); const router = injector.get(SpectacularFeatureRouter); // Act and assert phases of test case omitted }); Setup for Edit crisis feature tests
  46. import { screen } from '@testing-library/angular'; it('Edit crisis name from

    crisis detail', async () => { // Common feature test setup omitted const crisisId = 2; await router.navigate(['~', crisisId]); await user.clear(await screen.findByPlaceholderText(/name/i)); await user.type( await screen.findByPlaceholderText(/name/i), 'The global temperature is rising', ); await user.click(await screen.findByRole('button', { name: /save/i })); expect(await screen.findByText(/the global temperature is rising/i, { selector: '.selected a', })).toBeInTheDocument(); expect(location.path()).toBe(`~/;id=${crisisId};foo=foo`); }); Edit crisis from detail page feature test
  47. import { screen } from '@testing-library/angular'; it('Edit a crisis from

    crisis center home', async () => { // Common feature test setup omitted await router.navigateByUrl('~/'); await user.click(await screen.findByRole('link', { name: /procrastinators meeting delayed again/i, })); await user.clear(await screen.findByPlaceholderText(/name/i)); await user.type( await screen.findByPlaceholderText(/name/i), 'Coral reefs are dying' ); await user.click(await screen.findByRole('button', { name: /save/i })); expect(await screen.findByText(/welcome to the crisis center/i)) .toBeInTheDocument(); expect(await screen.findByText(/coral reefs are dying/i, { selector: '.selected a', })).toBeInTheDocument(); }); Edit crisis from home page feature test
  48. import { provideSpectacularFeatureTest, SpectacularAppComponent, SpectacularFeatureLocation, SpectacularFeatureRouter, } from '@ngworker/spectacular'; import

    { render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import type { UserEvent } from '@testing-library/user-event/dist/types/setup'; import { crisisCenterPath, crisisCenterRoutes } from '@tour-of-heroes/crisis-center'; describe('Edit crisis name', () => { beforeEach(async () => { user = userEvent.setup(); const { fixture: { debugElement: { injector } } } = await render( SpectacularAppComponent, { providers: [ provideSpectacularFeatureTest ({ featurePath: crisisCenterPath, }), ], routes: crisisCenterRoutes, }); location = injector.get(SpectacularFeatureLocation); router = injector.get(SpectacularFeatureRouter); }); let location: SpectacularFeatureLocation; let router: SpectacularFeatureRouter; let user: UserEvent; }); Shared setup for Edit crisis feature tests
  49. 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<Matcher, number>) => 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
  50. // Named queries omitted describe('Edit crisis name', () => {

    // Shared test setup omitted it('from crisis detail', async () => { const crisisId = 2; await router.navigate(['~', crisisId]); await user.clear(await findNameControl()); await user.type(await findNameControl(), 'The global temperature is rising'); await user.click(await findSaveButton()); 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 named queries
  51. // Named queries omitted describe('Edit crisis name', () => {

    // Shared test setup omitted it('from crisis center home', async () => { await router.navigateByUrl('~/'); await user.click(await findCrisisLink(/procrastinators meeting delayed again/i)); await user.clear(await findNameControl()); await user.type(await findNameControl(), 'Coral reefs are dying'); await user.click(await findSaveButton()); expect(await findSelectedCrisis(/coral reefs are dying/i)) .toBeInTheDocument(); expect(await findCrisisCenterGreeting()).toBeInTheDocument(); }); }); Edit crisis from home page feature test using named queries
  52. 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
  53. it('Edit crisis name from crisis detail', async () => {

    const { location, router, user } = await setup(); const crisisId = 2; await router.navigate(['~', crisisId]); await user.clear(await findNameControl()); await user.type(await findNameControl(), 'The global temperature is rising'); await user.click(await findSaveButton()); 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
  54. it('Edit crisis name from crisis center home', async () =>

    { const { router, user } = await setup(); await router.navigateByUrl('~/'); await user.click(await findCrisisLink(/procrastinators meeting delayed again/i)); await user.clear(await findNameControl()); await user.type(await findNameControl(), 'Coral reefs are dying'); await user.click(await findSaveButton()); expect(await findCrisisCenterHomeGreeting()).toBeInTheDocument(); expect( await findSelectedCrisis(/coral reefs are dying/i) ).toBeInTheDocument(); }); Edit crisis from home page feature test using SIFERS
  55. Conclusion

  56. 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
  57. Angular feature tests are Goldilocks’ choice Resilient to refactoring Adequately

    granular Orders of magnitude faster than E2E tests
  58. Angular feature tests are Goldilocks’ choice High confidence in desired

    behavior Sufficiently isolated Interact and verify as a user
  59. Angular feature tests are Goldilocks’ choice Shared simple test setup

    Unaware of implementation details Flexible when needed
  60. Thank you for attending Lars Gyrup Brink Nielsen @LayZeeDK ngworker.github.io/ngworker

    @ngworker/spectacular All icons courtesy of Flaticon