Testing Aurelia Applications with Unit and Integration Tests
In this tutorial, you will learn about Testing Aurelia Applications with Unit and Integration Tests. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia provides testing utilities that make it straightforward to unit test components in isolation and write integration tests that exercise the full framework lifecycle.
What You'll Learn
- Setting up the Aurelia test harness
- Unit testing services and ViewModels
- Testing component rendering with the ComponentTester
- Writing integration tests for routed applications
- Mocking HTTP requests and dependencies
Why It Matters
Automated tests catch regressions early, document expected behavior, and give you confidence to refactor. Aurelia's testing helpers let you verify both logic and rendering without launching a browser manually.
Real-World Use
A continuous integration pipeline that runs hundreds of Aurelia component tests on every Pull Request, checking that form validation works, routes resolve correctly, and HTTP services handle errors properly.
Testing Strategy
flowchart TD
A[Test Suite] --> B[Unit Tests]
A --> C[Component Tests]
A --> D[Integration Tests]
B --> E[Service Classes]
B --> F[Value Converters]
B --> G[Custom Attributes]
C --> H[ViewModel Logic]
C --> I[Template Rendering]
D --> J[Route Navigation]
D --> K[Full App Workflows]
style A fill:#e6f3ff,stroke:#4a90d9,stroke-width:2px
Setting Up the Test Harness
Install the necessary packages:
npm install --save-dev @aurelia/testing jasmine karma karma-jasmine
Create a test setup file (test/setup.ts):
import 'aurelia-polyfills';
import { initialize } from 'aurelia-pal-browser';
import { DOM } from 'aurelia-pal';
initialize();
Unit Testing a Service
// src/services/math-service.ts
export class MathService {
add(a: number, b: number): number {
return a + b;
}
factorial(n: number): number {
if (n < 0) throw new Error('Negative input');
return n <= 1 ? 1 : n * this.factorial(n - 1);
}
}
// test/services/math-service.spec.ts
import { MathService } from '../../src/services/math-service';
describe('MathService', () => {
let service: MathService;
beforeEach(() => {
service = new MathService();
});
it('adds two numbers', () => {
expect(service.add(2, 3)).toBe(5);
});
it('computes factorial of 5 as 120', () => {
expect(service.factorial(5)).toBe(120);
});
it('throws for negative factorial input', () => {
expect(() => service.factorial(-1)).toThrow();
});
});
Expected output: All three tests pass: addition returns 5, factorial(5) returns 120, and negative input throws an error.
Unit Testing a ViewModel
// src/components/counter.ts
import { autoinject } from 'aurelia-framework';
@autoinject
export class Counter {
count = 0;
increment(): void {
this.count++;
}
decrement(): void {
if (this.count > 0) this.count--;
}
reset(): void {
this.count = 0;
}
}
// test/components/counter.spec.ts
import { Counter } from '../../src/components/counter';
describe('Counter ViewModel', () => {
let counter: Counter;
beforeEach(() => {
counter = new Counter();
});
it('starts at 0', () => {
expect(counter.count).toBe(0);
});
it('increments count by 1', () => {
counter.increment();
expect(counter.count).toBe(1);
});
it('does not decrement below 0', () => {
counter.decrement();
expect(counter.count).toBe(0);
});
it('resets to 0', () => {
counter.increment();
counter.increment();
counter.reset();
expect(counter.count).toBe(0);
});
});
Expected output: All four tests pass, verifying the counter ViewModel logic in isolation from the template.
Component Testing with StageComponent
Use StageComponent to render a component and test its DOM:
import { StageComponent } from 'aurelia-testing';
import { bootstrap } from 'aurelia-bootstrapper';
describe('Counter Component', () => {
let component;
beforeEach(() => {
component = StageComponent
.withResources('components/counter')
.inView('<counter></counter>')
.boundTo({});
});
afterEach(() => {
component.dispose();
});
it('renders the initial count', done => {
component.create(bootstrap).then(() => {
const element = document.querySelector('.counter-value');
expect(element.textContent).toBe('0');
done();
});
});
it('updates when increment button is clicked', done => {
component.create(bootstrap).then(() => {
const button = document.querySelector('.increment-btn') as HTMLElement;
button.click();
const element = document.querySelector('.counter-value');
expect(element.textContent).toBe('1');
done();
});
});
});
Expected output: The component renders in a detached DOM, initial count is 0, and clicking the button updates the displayed value to 1.
Mocking HTTP Requests
// test/services/api-service.spec.ts
import { ApiService } from '../../src/services/api-service';
import { HttpClient } from 'aurelia-fetch-client';
describe('ApiService', () => {
let service: ApiService;
let http: jasmine.SpyObj<HttpClient>;
beforeEach(() => {
http = jasmine.createSpyObj('HttpClient', ['fetch']);
service = new ApiService(http);
});
it('fetches users from the API', async () => {
const mockUsers = [{ id: 1, name: 'Alice' }];
http.fetch.and.returnValue(
Promise.resolve({ json: () => Promise.resolve(mockUsers) })
);
const users = await service.getUsers();
expect(users).toEqual(mockUsers);
expect(http.fetch).toHaveBeenCalledWith('users');
});
it('handles fetch errors gracefully', async () => {
http.fetch.and.returnValue(Promise.reject(new Error('Network error')));
await expectAsync(service.getUsers()).toBeRejectedWithError('Network error');
});
});
Expected output: The first test verifies that getUsers calls http.fetch('users') and returns the mocked JSON. The second test verifies that errors propagate correctly.
Testing Routed Components
import { Router } from 'aurelia-router';
describe('UserDetail Route', () => {
let router: Router;
beforeEach(() => {
router = new Router();
// Configure test routes
});
it('navigates to user detail with correct parameter', () => {
spyOn(router, 'navigate');
router.navigateToRoute('user-detail', { id: 42 });
expect(router.navigate).toHaveBeenCalledWith('/users/42');
});
});
Common Mistakes
Not disposing the component after tests -
StageComponentcreates real DOM elements. Failing to calldispose()inafterEachcauses memory leaks and test pollution.Forgetting to call
done()in async tests - Aurelia's component creation is asynchronous. Always calldone()or use async/await to prevent false passes.Testing implementation details instead of behavior - Test what the component does (renders data, responds to clicks), not how it does it (internal method calls).
Not mocking HTTP dependencies - Real HTTP calls make tests slow, flaky, and dependent on network availability. Always mock the
HttpClient.Skipping tests for edge cases - Empty arrays, null values, and error responses are common in production. Test these scenarios explicitly.
Practice Questions
- What class does
aurelia-testingprovide for rendering components in tests? - How do you mock an
HttpClientdependency for service tests? - Why must you call
component.dispose()after each component test? - What is the purpose of the
Bootstrapmodule in component tests? - How do you test that a route navigates with the correct parameters?
Challenge: Write a complete test suite for a TodoList component. Include unit tests for the ViewModel (add, toggle, delete, clearCompleted), component tests for rendering, and integration tests that verify the full add-and-display workflow.
FAQ
Mini Project
Write a test suite for a movie search application. Include unit tests for the search service (mock HTTP), component tests for the search view (renders results, shows loading, handles empty state), and integration tests for the route that navigates to a movie detail page.
What's Next
Put everything together in a complete Aurelia project that combines components, routing, HTTP, validation, and testing into a real-world application.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro