Advanced testing techniques
This guide covers advanced testing patterns, utilities, and techniques for comprehensive Aurelia 2 application testing. It builds upon the foundation provided in the basic testing guides and explores sophisticated testing scenarios.
Comprehensive Fixture API Reference
The createFixture
function returns a powerful fixture object with extensive testing utilities. Understanding all available methods enables more effective and expressive tests.
Complete Fixture Interface
interface IFixture<T> {
// Core properties
appHost: HTMLElement; // Root element containing your app
startPromise: Promise<void>; // Promise that resolves when app starts
stop: (dispose?: boolean) => Promise<void>; // Cleanup function
component: T; // Root component instance
container: IContainer; // DI container for registrations
platform: IPlatform; // Platform abstraction
testContext: TestContext; // Test context utilities
// Query methods
getBy<E extends HTMLElement = HTMLElement>(selector: string): E;
getAllBy<E extends HTMLElement = HTMLElement>(selector: string): E[];
queryBy<E extends HTMLElement = HTMLElement>(selector: string): E | null;
// Assertion methods
assertText(expectedText: string, options?: ITextAssertOptions): void;
assertText(selector: string, expectedText: string, options?: ITextAssertOptions): void;
assertTextContain(expectedText: string): void;
assertTextContain(selector: string, expectedText: string): void;
assertHtml(expectedHtml: string, options?: IHtmlAssertOptions): void;
assertHtml(selector: string, expectedHtml: string, options?: IHtmlAssertOptions): void;
assertAttr(selector: string, attr: string, expectedValue: string | null): void;
assertAttrNS(selector: string, namespace: string, attr: string, expectedValue: string | null): void;
assertClass(selector: string, ...expectedClasses: string[]): void;
assertClassStrict(selector: string, ...expectedClasses: string[]): void;
assertStyles(selector: string, expectedStyles: Record<string, string>): void;
assertValue(selector: string, expectedValue: string): void;
assertChecked(selector: string, expectedChecked: boolean): void;
// Event methods
trigger: IEventTrigger;
type(selector: string | HTMLElement, text: string): void;
scrollBy(selector: string | HTMLElement, options: number | ScrollOptions): void;
// Utility methods
printHtml(): string;
createEvent(type: string, init?: EventInit): Event;
hJsx(tag: string, attrs?: Record<string, any>, ...children: any[]): HTMLElement;
}
Event Triggering Interface
interface IEventTrigger {
// Generic event triggering
(selector: string | HTMLElement, eventType: string, init?: EventInit): void;
// Specific event methods
click(selector: string | HTMLElement, init?: MouseEventInit): void;
dblclick(selector: string | HTMLElement, init?: MouseEventInit): void;
mousedown(selector: string | HTMLElement, init?: MouseEventInit): void;
mouseup(selector: string | HTMLElement, init?: MouseEventInit): void;
mousemove(selector: string | HTMLElement, init?: MouseEventInit): void;
mouseover(selector: string | HTMLElement, init?: MouseEventInit): void;
mouseout(selector: string | HTMLElement, init?: MouseEventInit): void;
mouseenter(selector: string | HTMLElement, init?: MouseEventInit): void;
mouseleave(selector: string | HTMLElement, init?: MouseEventInit): void;
keydown(selector: string | HTMLElement, init?: KeyboardEventInit): void;
keyup(selector: string | HTMLElement, init?: KeyboardEventInit): void;
keypress(selector: string | HTMLElement, init?: KeyboardEventInit): void;
focus(selector: string | HTMLElement, init?: FocusEventInit): void;
blur(selector: string | HTMLElement, init?: FocusEventInit): void;
change(selector: string | HTMLElement, init?: EventInit): void;
input(selector: string | HTMLElement, init?: EventInit): void;
submit(selector: string | HTMLElement, init?: SubmitEventInit): void;
scroll(selector: string | HTMLElement, init?: EventInit): void;
resize(selector: string | HTMLElement, init?: UIEventInit): void;
dragstart(selector: string | HTMLElement, init?: DragEventInit): void;
drag(selector: string | HTMLElement, init?: DragEventInit): void;
dragend(selector: string | HTMLElement, init?: DragEventInit): void;
drop(selector: string | HTMLElement, init?: DragEventInit): void;
}
Built-in Mock Utilities
Aurelia's testing package provides sophisticated mock utilities for common testing scenarios.
Available Mock Classes
import {
MockBinding,
MockBindingBehavior,
MockBrowserHistoryLocation,
MockContext,
MockPropertySubscriber,
MockServiceLocator,
MockSignaler,
MockTracingExpression,
MockValueConverter,
SpySubscriber,
} from '@aurelia/testing';
Using Built-in Mocks
describe('Component with Bindings', () => {
it('handles binding changes correctly', async () => {
const mockBinding = new MockBinding();
mockBinding.value = 'initial value';
const { startPromise, stop } = createFixture(
'<my-component></my-component>',
class App {},
[MyComponent],
[Registration.instance(IBinding, mockBinding)]
);
await startPromise;
// Simulate binding value change
mockBinding.value = 'updated value';
mockBinding.handleChange('updated value', 'initial value');
// Verify binding interactions
expect(mockBinding.calls).toContainEqual(['handleChange', 'updated value', 'initial value']);
await stop(true);
});
});
Spy Subscriber for Observable Testing
import { SpySubscriber } from '@aurelia/testing';
describe('Observable Component', () => {
it('notifies subscribers of changes', async () => {
const spy = new SpySubscriber();
const { component, startPromise, stop } = createFixture(
'<observable-component></observable-component>',
class App {},
[ObservableComponent]
);
await startPromise;
// Subscribe spy to component's observable property
component.observableProperty.subscribe(spy);
// Change the observable value
component.observableProperty = 'new value';
// Verify notifications
expect(spy.handleChange).toHaveBeenCalledWith('new value', undefined);
await stop(true);
});
});
Advanced Assertion Patterns
Custom Assertion Helpers
// Create reusable assertion helpers
export class TestAssertions {
static assertComponentState<T>(
fixture: IFixture<T>,
expectedState: Partial<T>
): void {
Object.entries(expectedState).forEach(([key, value]) => {
expect(fixture.component[key as keyof T]).toEqual(value);
});
}
static assertElementPresence(
fixture: IFixture<any>,
selectors: { present: string[]; absent: string[] }
): void {
selectors.present.forEach(selector => {
expect(fixture.queryBy(selector)).not.toBeNull();
});
selectors.absent.forEach(selector => {
expect(fixture.queryBy(selector)).toBeNull();
});
}
static assertAccessibility(
fixture: IFixture<any>,
requirements: {
hasAriaLabels?: string[];
hasTabIndex?: string[];
hasRoles?: Array<{ selector: string; role: string }>;
}
): void {
requirements.hasAriaLabels?.forEach(selector => {
const element = fixture.getBy(selector);
expect(element.getAttribute('aria-label')).toBeTruthy();
});
requirements.hasTabIndex?.forEach(selector => {
const element = fixture.getBy(selector);
expect(element.hasAttribute('tabindex')).toBe(true);
});
requirements.hasRoles?.forEach(({ selector, role }) => {
fixture.assertAttr(selector, 'role', role);
});
}
}
Using Custom Assertions
describe('AccessibleComponent', () => {
it('meets accessibility requirements', async () => {
const { startPromise, stop, ...fixture } = createFixture(
'<accessible-component></accessible-component>',
class App {},
[AccessibleComponent]
);
await startPromise;
TestAssertions.assertAccessibility(fixture, {
hasAriaLabels: ['.main-button', '.close-button'],
hasTabIndex: ['.focusable-div'],
hasRoles: [
{ selector: '.navigation', role: 'navigation' },
{ selector: '.content', role: 'main' }
]
});
await stop(true);
});
});
Complex Event Simulation
Drag and Drop Testing
describe('DragDropComponent', () => {
it('handles drag and drop operations', async () => {
const { startPromise, stop, trigger } = createFixture(
'<drag-drop-component></drag-drop-component>',
class App {},
[DragDropComponent]
);
await startPromise;
const sourceElement = '.draggable-item';
const targetElement = '.drop-zone';
// Simulate complete drag and drop sequence
trigger.dragstart(sourceElement, {
dataTransfer: new DataTransfer(),
clientX: 100,
clientY: 100
});
trigger.drag(sourceElement, {
clientX: 150,
clientY: 150
});
trigger.drop(targetElement, {
dataTransfer: new DataTransfer(),
clientX: 200,
clientY: 200
});
trigger.dragend(sourceElement);
// Verify drop operation completed
expect(fixture.getBy('.drop-zone')).toContainElement(fixture.getBy('.draggable-item'));
await stop(true);
});
});
Keyboard Navigation Testing
describe('KeyboardNavigationComponent', () => {
it('handles keyboard navigation correctly', async () => {
const { startPromise, stop, trigger, getBy } = createFixture(
'<keyboard-nav-component></keyboard-nav-component>',
class App {},
[KeyboardNavigationComponent]
);
await startPromise;
const firstItem = getBy('.nav-item:first-child');
const secondItem = getBy('.nav-item:nth-child(2)');
// Focus first item
trigger.focus(firstItem);
expect(document.activeElement).toBe(firstItem);
// Navigate with arrow keys
trigger.keydown(firstItem, { key: 'ArrowDown' });
expect(document.activeElement).toBe(secondItem);
// Test Enter key activation
const activationSpy = jest.fn();
secondItem.addEventListener('activate', activationSpy);
trigger.keydown(secondItem, { key: 'Enter' });
expect(activationSpy).toHaveBeenCalled();
await stop(true);
});
});
Form Testing Patterns
Complex Form Validation
describe('ValidationFormComponent', () => {
it('validates form fields correctly', async () => {
const { startPromise, stop, type, trigger, assertClass } = createFixture(
'<validation-form></validation-form>',
class App {},
[ValidationFormComponent]
);
await startPromise;
// Test required field validation
const emailInput = '#email-input';
const submitButton = '#submit-button';
// Try to submit empty form
trigger.click(submitButton);
assertClass(emailInput, 'invalid');
// Enter invalid email
type(emailInput, 'invalid-email');
trigger.blur(emailInput);
assertClass(emailInput, 'invalid');
// Enter valid email
type(emailInput, '[email protected]');
trigger.blur(emailInput);
assertClass(emailInput, 'valid');
// Submit valid form
trigger.click(submitButton);
expect(component.isSubmitted).toBe(true);
await stop(true);
});
});
Multi-step Form Testing
describe('MultiStepFormComponent', () => {
it('navigates through form steps correctly', async () => {
const { startPromise, stop, type, trigger, assertText } = createFixture(
'<multi-step-form></multi-step-form>',
class App {},
[MultiStepFormComponent]
);
await startPromise;
// Step 1: Personal Information
assertText('.step-indicator', 'Step 1 of 3');
type('#first-name', 'John');
type('#last-name', 'Doe');
trigger.click('#next-button');
// Step 2: Contact Information
assertText('.step-indicator', 'Step 2 of 3');
type('#email', '[email protected]');
type('#phone', '123-456-7890');
trigger.click('#next-button');
// Step 3: Review
assertText('.step-indicator', 'Step 3 of 3');
assertText('.review-name', 'John Doe');
assertText('.review-email', '[email protected]');
// Test back navigation
trigger.click('#back-button');
assertText('.step-indicator', 'Step 2 of 3');
// Return to review and submit
trigger.click('#next-button');
trigger.click('#submit-button');
expect(component.isFormSubmitted).toBe(true);
await stop(true);
});
});
Performance and Memory Testing
Render Performance Testing
describe('PerformanceComponent', () => {
it('renders large datasets efficiently', async () => {
const largeDataset = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`
}));
const startTime = performance.now();
const { startPromise, stop } = createFixture(
'<performance-component items.bind="data"></performance-component>',
class App { data = largeDataset; },
[PerformanceComponent]
);
await startPromise;
const renderTime = performance.now() - startTime;
// Verify reasonable render time
expect(renderTime).toBeLessThan(500); // Should render in under 500ms
// Verify all items are rendered
const items = fixture.getAllBy('.list-item');
expect(items).toHaveLength(1000);
await stop(true);
});
it('handles updates efficiently', async () => {
const { component, startPromise, stop } = createFixture(
'<performance-component items.bind="data"></performance-component>',
class App {
data = Array.from({ length: 100 }, (_, i) => ({ id: i, name: `Item ${i}` }));
},
[PerformanceComponent]
);
await startPromise;
// Measure update performance
const updateStart = performance.now();
// Add 100 more items
component.data = [
...component.data,
...Array.from({ length: 100 }, (_, i) => ({ id: i + 100, name: `New Item ${i}` }))
];
await tasksSettled();
const updateTime = performance.now() - updateStart;
// Update should be fast
expect(updateTime).toBeLessThan(100);
// Verify correct number of items
expect(fixture.getAllBy('.list-item')).toHaveLength(200);
await stop(true);
});
});
Memory Leak Detection
describe('MemoryLeakComponent', () => {
it('does not leak memory on repeated creation/destruction', async () => {
if (!(performance as any).memory) {
pending('Memory measurement not available');
return;
}
const initialMemory = (performance as any).memory.usedJSHeapSize;
// Create and destroy component multiple times
for (let i = 0; i < 50; i++) {
const { startPromise, stop } = createFixture(
'<memory-test-component></memory-test-component>',
class App {},
[MemoryTestComponent]
);
await startPromise;
await stop(true);
}
// Force garbage collection if available
if ((global as any).gc) {
(global as any).gc();
}
const finalMemory = (performance as any).memory.usedJSHeapSize;
const memoryIncrease = finalMemory - initialMemory;
// Memory increase should be minimal (less than 5MB)
expect(memoryIncrease).toBeLessThan(5 * 1024 * 1024);
});
});
Integration Testing Patterns
Multi-Component Integration
describe('ComponentIntegration', () => {
it('integrates multiple components correctly', async () => {
const { startPromise, stop, trigger, assertText } = createFixture(
`<parent-component>
<child-component-a></child-component-a>
<child-component-b></child-component-b>
</parent-component>`,
class App {},
[ParentComponent, ChildComponentA, ChildComponentB]
);
await startPromise;
// Test component communication
trigger.click('.child-a-button');
assertText('.parent-status', 'Child A clicked');
trigger.click('.child-b-button');
assertText('.parent-status', 'Child B clicked');
// Test cross-component data flow
trigger.type('.child-a-input', 'shared data');
assertText('.child-b-display', 'shared data');
await stop(true);
});
});
Service Integration Testing
describe('ServiceIntegration', () => {
it('integrates with real services', async () => {
// Use real services instead of mocks for integration testing
const { container, startPromise, stop } = createFixture(
'<service-consumer></service-consumer>',
class App {},
[ServiceConsumer, DataService, AuthService]
);
await startPromise;
// Test actual service interactions
const dataService = container.get(DataService);
const authService = container.get(AuthService);
// Simulate authentication
await authService.login('[email protected]', 'password');
// Verify authenticated state affects component
expect(component.isAuthenticated).toBe(true);
// Test data loading
const data = await dataService.fetchUserData();
expect(component.userData).toEqual(data);
await stop(true);
});
});
Async Testing Advanced Patterns
Testing Race Conditions
describe('AsyncComponent', () => {
it('handles concurrent operations correctly', async () => {
const { component, startPromise, stop } = createFixture(
'<async-component></async-component>',
class App {},
[AsyncComponent]
);
await startPromise;
// Start multiple async operations concurrently
const promise1 = component.loadData('endpoint1');
const promise2 = component.loadData('endpoint2');
const promise3 = component.loadData('endpoint3');
// Wait for all to complete
await Promise.all([promise1, promise2, promise3]);
// Verify final state is consistent
expect(component.isLoading).toBe(false);
expect(component.data).toBeDefined();
expect(component.errors).toHaveLength(0);
await stop(true);
});
});
Testing Timeout Scenarios
describe('TimeoutComponent', () => {
it('handles operation timeouts gracefully', async () => {
jest.useFakeTimers();
const { component, startPromise, stop } = createFixture(
'<timeout-component></timeout-component>',
class App {},
[TimeoutComponent]
);
await startPromise;
// Start operation with timeout
const operationPromise = component.performOperationWithTimeout();
// Fast-forward past timeout
jest.advanceTimersByTime(30000);
// Wait for timeout to be handled
await operationPromise;
// Verify timeout was handled
expect(component.hasTimedOut).toBe(true);
expect(component.errorMessage).toContain('timeout');
jest.useRealTimers();
await stop(true);
});
});
Best Practices Summary
1. Test Organization
describe('ComponentName', () => {
describe('initialization', () => {
// Test component setup and initial state
});
describe('user interactions', () => {
// Test user-driven behaviors
});
describe('error scenarios', () => {
// Test error handling and edge cases
});
describe('performance', () => {
// Test performance characteristics
});
});
2. Assertion Strategies
Use the most specific assertion available
Create custom assertions for domain-specific validations
Test both positive and negative scenarios
Verify side effects and state changes
3. Mock Management
Use real implementations for integration tests
Mock external dependencies and slow operations
Prefer built-in mocks when available
Clean up mock state between tests
4. Async Handling
Always await
startPromise
before assertionsUse
tasksSettled()
for complex async operationsTest timeout and error scenarios
Handle race conditions explicitly
5. Performance Considerations
Monitor render times for large datasets
Test memory usage patterns
Verify cleanup prevents memory leaks
Use performance budgets in CI/CD
This advanced testing guide provides comprehensive patterns for testing sophisticated Aurelia 2 applications. Combine these techniques with the foundational testing guides to create robust, maintainable test suites that ensure application quality and reliability.
Last updated
Was this helpful?