Testing Your Aurelia Application
Testing is a critical part of building reliable applications. This tutorial will guide you through testing Aurelia 2 applications, covering unit tests, component tests, and end-to-end tests.
Table of Contents
Setting Up Your Test Environment
Aurelia 2 applications typically use Jasmine or Mocha for unit and component tests. The @aurelia/testing package provides helpful utilities for testing Aurelia components.
Installing Testing Dependencies
npm install --save-dev @aurelia/testing jasmine @types/jasmine karma karma-jasmine karma-chrome-launcherFor E2E testing with Playwright:
npm install --save-dev playwright @playwright/testConfiguring Karma
Create a karma.conf.js file in your project root:
module.exports = function(config) {
config.set({
frameworks: ['jasmine'],
files: [
{ pattern: 'test/**/*.spec.js', type: 'module' }
],
browsers: ['ChromeHeadless'],
singleRun: true
});
};Unit Testing
Unit tests focus on testing individual functions and classes in isolation. Let's start with a simple service.
Testing a Service
// src/services/calculator.ts
import { DI } from '@aurelia/kernel';
export const ICalculator = DI.createInterface<ICalculator>(
'ICalculator',
x => x.singleton(Calculator)
);
export interface ICalculator extends Calculator {}
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
multiply(a: number, b: number): number {
return a * b;
}
divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero');
}
return a / b;
}
}// src/services/calculator.spec.ts
import { Calculator } from './calculator';
describe('Calculator', () => {
let calculator: Calculator;
beforeEach(() => {
calculator = new Calculator();
});
describe('add', () => {
it('should add two positive numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(calculator.add(-5, 3)).toBe(-2);
});
});
describe('multiply', () => {
it('should multiply two numbers', () => {
expect(calculator.multiply(4, 5)).toBe(20);
});
it('should return zero when multiplying by zero', () => {
expect(calculator.multiply(5, 0)).toBe(0);
});
});
describe('divide', () => {
it('should divide two numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});
it('should throw error when dividing by zero', () => {
expect(() => calculator.divide(10, 0)).toThrow('Cannot divide by zero');
});
});
});Component Testing
Component testing verifies that your Aurelia components render correctly and respond to user interactions. The @aurelia/testing package provides the TestContext utility for this purpose.
Testing a Simple Component
// src/components/greeting.ts
import { customElement } from '@aurelia/runtime-html';
@customElement('greeting-component')
export class GreetingComponent {
name = '';
get greeting(): string {
return this.name ? `Hello, ${this.name}!` : 'Hello, stranger!';
}
}<!-- src/components/greeting.html -->
<div class="greeting">
<input type="text" value.bind="name" placeholder="Enter your name">
<p class="message">${greeting}</p>
</div>// src/components/greeting.spec.ts
import { TestContext } from '@aurelia/testing';
import { Aurelia } from '@aurelia/runtime-html';
import { GreetingComponent } from './greeting';
describe('GreetingComponent', () => {
it('should display default greeting for empty name', async () => {
const ctx = TestContext.create();
const { container } = ctx;
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({
component: GreetingComponent,
host
}).start();
const message = host.querySelector('.message');
expect(message?.textContent).toBe('Hello, stranger!');
await au.stop(true);
});
it('should update greeting when name is entered', async () => {
const ctx = TestContext.create();
const { container, platform } = ctx;
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({
component: GreetingComponent,
host
}).start();
const component = au.root.controller.viewModel as GreetingComponent;
const input = host.querySelector('input') as HTMLInputElement;
const message = host.querySelector('.message');
// Simulate user input
component.name = 'Alice';
platform.domQueue.flush();
expect(message?.textContent).toBe('Hello, Alice!');
await au.stop(true);
});
});Creating a Test Fixture Helper
For complex components, create a reusable fixture helper:
// src/test-helpers/create-fixture.ts
import { Constructable } from '@aurelia/kernel';
import { TestContext } from '@aurelia/testing';
import { Aurelia, IPlatform } from '@aurelia/runtime-html';
export async function createFixture<T extends Constructable>(
Component: T,
registrations: any[] = []
) {
const ctx = TestContext.create();
const { container, platform } = ctx;
container.register(...registrations);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({ component: Component, host }).start();
return {
ctx,
au,
host,
component: au.root.controller.viewModel as InstanceType<T>,
platform,
container,
async tearDown() {
await au.stop(true);
}
};
}Now you can simplify your tests:
// src/components/greeting.spec.ts
import { createFixture } from '../test-helpers/create-fixture';
import { GreetingComponent } from './greeting';
describe('GreetingComponent', () => {
it('should display default greeting', async () => {
const { host, component, tearDown } = await createFixture(GreetingComponent);
const message = host.querySelector('.message');
expect(message?.textContent).toBe('Hello, stranger!');
await tearDown();
});
it('should update greeting when name changes', async () => {
const { host, component, platform, tearDown } = await createFixture(GreetingComponent);
component.name = 'Bob';
platform.domQueue.flush();
const message = host.querySelector('.message');
expect(message?.textContent).toBe('Hello, Bob!');
await tearDown();
});
});Testing with Dependency Injection
When testing components that use dependency injection, you can provide mock implementations.
Component with Dependencies
// src/services/user-service.ts
import { DI } from '@aurelia/kernel';
export const IUserService = DI.createInterface<IUserService>(
'IUserService',
x => x.singleton(UserService)
);
export interface IUserService extends UserService {}
export class UserService {
async getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
export interface User {
id: number;
name: string;
email: string;
}// src/components/user-profile.ts
import { customElement, resolve } from '@aurelia/runtime-html';
import { IUserService, User } from '../services/user-service';
@customElement('user-profile')
export class UserProfile {
private userService = resolve(IUserService);
user: User | null = null;
loading = false;
error = '';
async loadUser(userId: number): Promise<void> {
this.loading = true;
this.error = '';
try {
this.user = await this.userService.getUser(userId);
} catch (err) {
this.error = 'Failed to load user';
} finally {
this.loading = false;
}
}
}<!-- src/components/user-profile.html -->
<div class="user-profile">
<div if.bind="loading">Loading...</div>
<div if.bind="error" class="error">${error}</div>
<div if.bind="user" class="user-details">
<h2>${user.name}</h2>
<p>${user.email}</p>
</div>
</div>Testing with Mock Services
// src/components/user-profile.spec.ts
import { Registration } from '@aurelia/kernel';
import { createFixture } from '../test-helpers/create-fixture';
import { UserProfile } from './user-profile';
import { IUserService, User } from '../services/user-service';
class MockUserService {
async getUser(id: number): Promise<User> {
return {
id,
name: 'Test User',
email: '[email protected]'
};
}
}
describe('UserProfile', () => {
it('should load and display user', async () => {
const { host, component, platform, tearDown } = await createFixture(
UserProfile,
[Registration.singleton(IUserService, MockUserService)]
);
await component.loadUser(1);
platform.domQueue.flush();
expect(component.user).toBeDefined();
expect(component.user?.name).toBe('Test User');
expect(host.querySelector('.user-details h2')?.textContent).toBe('Test User');
await tearDown();
});
it('should display error message on failure', async () => {
class FailingUserService {
async getUser(): Promise<User> {
throw new Error('Network error');
}
}
const { host, component, platform, tearDown } = await createFixture(
UserProfile,
[Registration.singleton(IUserService, FailingUserService)]
);
await component.loadUser(1);
platform.domQueue.flush();
expect(component.error).toBe('Failed to load user');
expect(host.querySelector('.error')?.textContent).toBe('Failed to load user');
await tearDown();
});
});Testing Router Integration
Testing components that use the router requires setting up the router configuration.
// src/components/navigation.spec.ts
import { Registration } from '@aurelia/kernel';
import { TestContext } from '@aurelia/testing';
import { Aurelia } from '@aurelia/runtime-html';
import { RouterConfiguration, IRouter } from '@aurelia/router';
describe('Navigation Component', () => {
it('should navigate when link is clicked', async () => {
const ctx = TestContext.create();
const { container } = ctx;
container.register(RouterConfiguration);
const au = new Aurelia(container);
const host = ctx.createElement('div');
await au.app({
component: AppRoot,
host
}).start();
const router = container.get(IRouter);
// Test navigation
await router.load('/home');
expect(router.currentRoute.path).toBe('/home');
await au.stop(true);
});
});End-to-End Testing
E2E tests verify that your entire application works correctly from a user's perspective. We'll use Playwright for E2E testing.
Setting Up Playwright
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
use: {
baseURL: 'http://localhost:9000',
},
webServer: {
command: 'npm run dev',
port: 9000,
reuseExistingServer: true,
},
});Writing E2E Tests
// e2e/greeting.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Greeting Component', () => {
test('should display and update greeting', async ({ page }) => {
await page.goto('/');
// Check initial state
await expect(page.locator('.message')).toHaveText('Hello, stranger!');
// Enter a name
await page.locator('input[placeholder="Enter your name"]').fill('Alice');
// Verify the greeting updates
await expect(page.locator('.message')).toHaveText('Hello, Alice!');
});
});Testing User Flows
// e2e/user-profile.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Profile Flow', () => {
test('should load and display user profile', async ({ page }) => {
// Navigate to user profile page
await page.goto('/users/1');
// Wait for loading to complete
await expect(page.locator('.user-profile')).toBeVisible();
// Verify user details are displayed
await expect(page.locator('.user-details h2')).toContainText('Test User');
await expect(page.locator('.user-details p')).toContainText('[email protected]');
});
test('should display error on network failure', async ({ page }) => {
// Intercept API call and return error
await page.route('/api/users/*', route => {
route.abort('failed');
});
await page.goto('/users/1');
// Verify error message is displayed
await expect(page.locator('.error')).toContainText('Failed to load user');
});
});Best Practices
1. Keep Tests Focused
Each test should verify one specific behavior:
// Good
it('should add two numbers', () => {
expect(calculator.add(2, 3)).toBe(5);
});
it('should handle negative numbers', () => {
expect(calculator.add(-5, 3)).toBe(-2);
});
// Avoid
it('should perform calculations', () => {
expect(calculator.add(2, 3)).toBe(5);
expect(calculator.multiply(2, 3)).toBe(6);
expect(calculator.divide(6, 3)).toBe(2);
});2. Use Descriptive Test Names
Test names should clearly describe what is being tested:
// Good
it('should display error message when login fails', () => {
// test code
});
// Avoid
it('should work', () => {
// test code
});3. Clean Up After Tests
Always clean up resources after tests:
afterEach(async () => {
await tearDown();
});4. Mock External Dependencies
Isolate your tests by mocking external services:
class MockApiService {
async getData(): Promise<Data> {
return { id: 1, value: 'test' };
}
}5. Test Edge Cases
Don't just test the happy path:
describe('divide', () => {
it('should divide positive numbers', () => {
expect(calculator.divide(10, 2)).toBe(5);
});
it('should handle division by zero', () => {
expect(() => calculator.divide(10, 0)).toThrow();
});
it('should handle negative divisors', () => {
expect(calculator.divide(10, -2)).toBe(-5);
});
});6. Use Test Coverage Tools
Track your test coverage to identify untested code:
npm install --save-dev karma-coverageConfigure in karma.conf.js:
module.exports = function(config) {
config.set({
// ...
reporters: ['progress', 'coverage'],
coverageReporter: {
type: 'html',
dir: 'coverage/'
}
});
};7. Organize Tests by Feature
Structure your test files to mirror your source code:
src/
components/
user-profile.ts
user-profile.html
user-profile.spec.ts
services/
user-service.ts
user-service.spec.tsConclusion
Testing is essential for building reliable Aurelia applications. This tutorial covered:
Setting up your test environment with Jasmine and Karma
Writing unit tests for services
Testing components with
@aurelia/testingMocking dependencies with dependency injection
Testing router integration
End-to-end testing with Playwright
Best practices for writing maintainable tests
Start by testing critical business logic, then expand coverage to components and user flows. Remember to run tests regularly during development and as part of your CI/CD pipeline.
For more information, check out the official Aurelia Documentation and the @aurelia/testing API reference.
Last updated
Was this helpful?