Outcome Recipes

Advanced testing patterns for complex scenarios including async operations, routing, service mocking, and end-to-end component interaction testing.

These recipes show how to test complex real-world scenarios in Aurelia applications. Use them when basic component testing isn't enough.

1. Testing components with async API calls

Goal: Test components that load data from APIs with proper handling of loading states, successful responses, and error scenarios.

Steps

  1. Create a mock HTTP client with configurable responses:

    import { IHttpClient } from '@aurelia/fetch-client';
    import { Registration } from '@aurelia/kernel';
    
    export class MockHttpClient implements IHttpClient {
      private mockResponses = new Map<string, any>();
      private mockErrors = new Map<string, Error>();
    
      baseUrl = '';
      activeRequestCount = 0;
      isRequesting = false;
    
      configure() {
        return this;
      }
    
      setMockResponse(url: string, data: any, status: number = 200) {
        this.mockResponses.set(url, { data, status });
      }
    
      setMockError(url: string, error: Error) {
        this.mockErrors.set(url, error);
      }
    
      async fetch(input: RequestInfo | Request): Promise<Response> {
        const url = typeof input === 'string' ? input : input.url;
    
        if (this.mockErrors.has(url)) {
          throw this.mockErrors.get(url);
        }
    
        const mock = this.mockResponses.get(url);
        if (!mock) {
          throw new Error(`No mock response configured for ${url}`);
        }
    
        return new Response(JSON.stringify(mock.data), {
          status: mock.status,
          headers: { 'Content-Type': 'application/json' }
        });
      }
    }
    
    export const MockHttpClientRegistration = Registration.instance(
      IHttpClient,
      new MockHttpClient()
    );
  2. Test component with successful data loading:

    import { createFixture } from '@aurelia/testing';
    import { MockHttpClient, MockHttpClientRegistration } from './mock-http-client';
    import { ProductList } from './product-list';
    
    describe('ProductList', () => {
      it('should load and display products', async () => {
        const mockHttp = new MockHttpClient();
        mockHttp.setMockResponse('/api/products', {
          products: [
            { id: '1', name: 'Product 1', price: 10 },
            { id: '2', name: 'Product 2', price: 20 }
          ]
        });
    
        const { component, assertText, platform } = await createFixture
          .component(ProductList)
          .html`<div>
            <div if.bind="loading">Loading...</div>
            <div if.bind="error">\${error}</div>
            <div repeat.for="product of products">\${product.name}</div>
          </div>`
          .deps(Registration.instance(IHttpClient, mockHttp))
          .build()
          .started;
    
        // Initial loading state
        assertText('Loading...');
    
        // Wait for async attached() to complete
        await tasksSettled();
    
        // Verify products are displayed
        assertText('Product 1Product 2');
        expect(component.loading).toBe(false);
        expect(component.products.length).toBe(2);
    
        await fixture.stop(true);
      });
    });
  3. Test error handling:

    it('should display error message when API fails', async () => {
      const mockHttp = new MockHttpClient();
      mockHttp.setMockError('/api/products', new Error('Network error'));
    
      const { component, assertText, platform } = await createFixture
        .component(ProductList)
        .html`<div>
          <div if.bind="loading">Loading...</div>
          <div if.bind="error">\${error}</div>
          <div repeat.for="product of products">\${product.name}</div>
        </div>`
        .deps(Registration.instance(IHttpClient, mockHttp))
        .build()
        .started;
    
      // Wait for async operation
      await tasksSettled();
    
      // Verify error is displayed
      expect(component.error).toBeTruthy();
      expect(component.products.length).toBe(0);
      expect(component.loading).toBe(false);
    
      await fixture.stop(true);
    });
  4. Test retry functionality:

    it('should retry loading when retry button is clicked', async () => {
      const mockHttp = new MockHttpClient();
      let callCount = 0;
    
      // First call fails, second succeeds
      mockHttp.fetch = async (input: RequestInfo | Request) => {
        callCount++;
        if (callCount === 1) {
          throw new Error('Temporary error');
        }
        return new Response(JSON.stringify({
          products: [{ id: '1', name: 'Product 1', price: 10 }]
        }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' }
        });
      };
    
      const { component, trigger, assertText, platform } = await createFixture
        .component(ProductList)
        .html`<div>
          <div if.bind="loading">Loading...</div>
          <div if.bind="error">
            \${error}
            <button click.trigger="retry()">Retry</button>
          </div>
          <div repeat.for="product of products">\${product.name}</div>
        </div>`
        .deps(Registration.instance(IHttpClient, mockHttp))
        .build()
        .started;
    
      // Wait for initial failed load
      await tasksSettled();
      expect(component.error).toBeTruthy();
    
      // Click retry button
      trigger.click('button');
    
      // Wait for retry to complete
      await tasksSettled();
    
      // Verify success
      assertText('Product 1');
      expect(component.error).toBeNull();
      expect(callCount).toBe(2);
    
      await fixture.stop(true);
    });

Checklist

  • Loading state displays before data arrives

  • Successful API calls populate component data

  • Error states are handled and displayed

  • Retry functionality reloads data

  • All async operations can be waited using await tasksSettled() for timing

2. Testing router navigation and route parameters

Goal: Test components that use routing for navigation, parameter extraction, and route guards.

Steps

  1. Set up a test with router configuration:

  2. Test route parameter extraction:

  3. Test route guards:

  4. Test route guard redirects:

Checklist

  • Router navigation works in tests

  • Route parameters are extracted correctly

  • canLoad guards are invoked and respected

  • Redirects from guards work as expected

  • Current route state is verifiable

3. Testing with validation

Goal: Test form validation including rules, error display, and submission prevention.

Steps

  1. Set up validation testing environment:

  2. Test validation with valid input:

  3. Test field-level validation on blur:

Checklist

  • Validation rules are checked on submit

  • Invalid data prevents form submission

  • Valid data passes validation

  • Field-level validation triggers on blur

  • Error messages are accessible via controller

4. Testing complex component interactions

Goal: Test parent-child component communication, custom events, and state synchronization.

Steps

  1. Test parent-child data binding:

  2. Test child-to-parent communication via custom events:

  3. Test sibling component communication via shared service:

Checklist

  • Parent-to-child data flows via @bindable

  • Child-to-parent communication works via events

  • Sibling components share state via services

  • Changes propagate correctly across component tree

  • Custom events are dispatched and handled

5. Testing lifecycle hooks in complex scenarios

Goal: Test components with complex initialization, async lifecycle hooks, and cleanup operations.

Steps

  1. Test async data loading in lifecycle hooks:

  2. Test cleanup in detaching/unbinding:

  3. Test lifecycle hook order:

Checklist

  • Async lifecycle hooks complete before component is ready

  • Cleanup hooks properly dispose of resources

  • Hook execution order is correct

  • .started waits for all async hooks to complete

  • stop(true) triggers cleanup hooks

6. Testing with real-world dependencies

Goal: Test components that depend on multiple services, handle complex state, and integrate with external systems.

Steps

  1. Create comprehensive mocks for service dependencies:

  2. Test component with multiple service dependencies:

  3. Test error scenarios and recovery:

Checklist

  • Multiple service dependencies are properly mocked

  • Service interactions are tested in integration

  • Error scenarios are tested and handled

  • Retry/recovery mechanisms work correctly

  • Mock services provide realistic behavior

Testing pattern cheat sheet

Scenario
Key Approach
Tools/APIs

Async API calls

Mock HTTP client + await tasksSettled()

MockHttpClient, await tasksSettled()

Router navigation

Router configuration + router.load()

RouterConfiguration, IRouter

Form validation

Validation rules + controller

IValidationRules, IValidationController

Component interaction

Bindables + custom events

@bindable, CustomEvent

Lifecycle hooks

Hook execution + timing

.started, stop(true)

Service dependencies

Mock registrations

Registration.instance(), mock services

Best practices

  1. Always await .started: Ensures all async lifecycle hooks complete

  2. Use await tasksSettled(): After async operations or state changes

  3. Mock external dependencies: HTTP clients, auth services, APIs

  4. Test error paths: Don't just test happy scenarios

  5. Clean up with stop(true): Prevents memory leaks and interference

  6. Isolate tests: Each test should be independent

  7. Use descriptive test names: Clear "should..." statements

  8. Test user interactions: Clicks, typing, form submission

  9. Verify state changes: Check both component properties and DOM

  10. Test accessibility: Verify ARIA attributes and keyboard navigation

See also

Last updated

Was this helpful?