Stubs, mocks & spies

Testing in Aurelia often involves testing components that have dependencies injected into them. Using dependency injection (DI) simplifies the process of replacing these dependencies with mocks, stubs, or spies during testing. This can be particularly useful when you need to isolate the component under test from external concerns like API calls or complex logic.

Understanding Mocks, Stubs, and Spies

  • Mocks are objects that replace real implementations with fake methods and properties that you define. They are useful for simulating complex behavior without relying on the actual implementation.

  • Stubs are like mocks but typically focus on replacing specific methods or properties rather than entire objects. They are useful when you want to control the behavior of a dependency for a particular test case.

  • Spies allow you to wrap existing methods so that you can record information about their calls, such as the number of times they were called or the arguments they received.

Using Sinon for Mocking, Stubbing, and Spying

Sinon is a popular library for creating mocks, stubs, and spies in JavaScript tests. It provides a rich API for controlling your test environment and can significantly simplify the process of testing components with dependencies.

Installing Sinon

To make use of Sinon in your Aurelia project, you need to install it along with its type definitions for TypeScript support:

npm install sinon @types/sinon -D

Using Sinon in Your Tests

After installing Sinon, import it in your test files to access its functionality. Let's look at how to apply Sinon to mock, stub, and spy on dependencies in Aurelia components.

my-component.ts
import { IRouter } from '@aurelia/router-direct';
import { customElement, resolve } from 'aurelia';

@customElement('my-component')
export class MyComponent {
    constructor(private router: IRouter = resolve(IRouter)) {}

    navigate(path: string) {
        return this.router.load(path);
    }
}

In this example, the MyComponent class has a dependency on IRouter and a method navigate that delegates to the router's load method.

Stubbing Individual Methods

To stub the load method of the router, use Sinon's stub method:

Mocking an Entire Dependency

When you need to replace the entire dependency, create a mock object and register it in place of the real one:

By using Registration.instance, we can ensure that any part of the application being tested will receive our mock implementation when asking for the IRouter dependency.

Spying on Methods

To observe and assert the behavior of methods, use Sinon's spies:

To test that the save method is called correctly, wrap it with a spy:

Mocking Dependencies Directly in the Constructor

Unit tests may require you to instantiate classes manually rather than using Aurelia's createFixture. In such cases, you can mock dependencies directly in the constructor:

In this test, we directly provide a mock router object when creating an instance of MyComponent. This technique is useful for more traditional unit testing where you want to test methods in isolation.

Conclusion

Mocking, stubbing, and spying are powerful techniques that can help you write more effective and isolated tests for your Aurelia components. By leveraging tools like Sinon and Aurelia's dependency injection system, you can create test environments that are both flexible and easy to control. Whether you're writing unit tests or integration tests, these methods will enable you to test your components' behavior accurately and with confidence.

Comprehensive Dependency Injection Mocking

Testing Components with @inject Decorator

Modern Aurelia 2 supports both constructor injection and the resolve() function. Here's how to test both patterns:

Testing the @inject pattern:

Testing the resolve() pattern:

Testing Interface-Based Dependency Injection

When using interface tokens (the recommended Aurelia 2 pattern):

Testing interface-based injection:

Testing Optional Dependencies

For services with optional dependencies:

Testing with optional dependency present:

Testing Service Dependency Chains

When Service A depends on Service B which depends on Service C:

Testing the entire chain:

Testing Mixed DI Patterns

Components that use both @inject in constructor and resolve() for some dependencies:

Testing mixed patterns:

Testing Factory and Transient Dependencies

When services are registered as transient (new instance each time) or factories:

Testing factory patterns:

Testing Circular Dependencies and Complex DI Scenarios

When you have complex DI scenarios with potential circular dependencies:

Testing circular dependencies:

Testing Scoped/Hierarchical DI

When you have child containers or scoped dependencies:

Testing hierarchical DI:

Testing Conditional DI and Dynamic Registration

When services are conditionally registered or resolved based on runtime conditions:

Testing conditional DI:

Advanced Mocking Patterns

Testing with Aurelia's Built-in Testing Utilities

Aurelia provides built-in testing utilities for common scenarios:

Mocking Complex Dependencies

For services with multiple methods and properties:

Testing Error Scenarios

Spy Verification Patterns

Testing with Official Aurelia Packages

When your application uses official Aurelia packages, you'll need specific testing strategies for each package. Here's comprehensive guidance for testing with the most commonly used Aurelia packages.

Testing with @aurelia/validation

The validation package provides form validation capabilities. Here's how to test components that use validation:

Testing validation behavior:

Testing with @aurelia/router

The router package handles navigation and routing. Here's how to test router-dependent components:

Testing router interactions:

Testing route parameters:

Testing with @aurelia/state

The state package provides state management capabilities. Here's how to test state-connected components:

Testing state connections:

Testing with @aurelia/dialog

The dialog package provides modal dialog functionality:

Testing dialog interactions:

Testing with @aurelia/fetch-client

The fetch client provides HTTP request capabilities:

Testing HTTP operations:

Testing with @aurelia/i18n

The internationalization package provides translation capabilities:

Testing i18n functionality:

Testing Package Integration Best Practices

  1. Mock Package Services: Always mock the main service interfaces from each package

  2. Test Package-Specific Behavior: Focus on how your components interact with package APIs

  3. Verify Method Calls: Use spies to ensure package methods are called with correct parameters

  4. Test Error Scenarios: Mock package errors to test error handling

  5. Integration Testing: Test the full flow of package interactions when needed

Common Package Testing Patterns

Last updated

Was this helpful?