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
- Angular Tiny Conf: Peretor, December 2022

Recording: https://www.youtube.com/watch?v=M65J8pSqU14&t=9265s

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 Tiny Conf 2022

    View Slide

  2. Angular’s testing capabilities

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. Clear box testing and
    mystery box testing

    View Slide

  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

    View Slide

  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

    View Slide

  9. Angular component tests
    are slow

    View Slide

  10. Angular component tests
    are slow

    View Slide

  11. Angular component tests
    are fast 🌿

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. Angular component tests
    are fast 🌿

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. End-to-end tests
    are powerful

    View Slide

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

    View Slide

  29. End-to-end tests
    are powerful

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. Say hello

    View Slide

  37. Say hello to Spectacular

    View Slide

  38. Say hello to Spectacular
    … and Angular Testing Library

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  43. Angular feature tests

    View Slide

  44. 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

    View Slide

  45. 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

    View Slide

  46. 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

    View Slide

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

    View Slide

  48. Angular feature tests
    Using Spectacular and Angular Testing Library

    View Slide

  49. 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

    View Slide

  50. 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

    View Slide

  51. 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

    View Slide

  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

    View Slide

  53. Angular feature tests
    Using Spectacular and the Cypress component test
    runner

    View Slide

  54. 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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

  58. Conclusion

    View Slide

  59. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide