Angular Mocking
Introduction
When writing tests for your Angular applications, you'll often encounter scenarios where your components depend on services, HTTP requests, or other external interactions. Testing these dependencies directly can lead to unreliable tests, increased complexity, and slower test execution. This is where mocking comes in.
Mocking is a technique used in unit testing where you replace real dependencies with simulated objects (mocks) that mimic the behavior of the real objects. In Angular testing, mocking helps you isolate the code you're testing from its dependencies, making your tests more focused, reliable, and faster.
In this guide, we'll explore how to effectively implement mocking in Angular tests, covering:
- Why mocking is important
- Different types of mocks in Angular
- How to create and use mock services
- Techniques for mocking HTTP requests
- How to mock component dependencies and inputs
Why Mocking is Important
Before diving into implementation, let's understand why mocking is crucial for effective Angular testing:
- Isolation: Mocking allows you to test a single unit of code without testing its dependencies.
- Speed: Tests run faster because they don't wait for real HTTP requests or database operations.
- Reliability: Tests become more reliable as they aren't affected by external factors like network issues.
- Control: You can easily simulate different scenarios, including edge cases and error states.
- Focus: Tests remain focused on specific component/service behavior rather than integration concerns.
Types of Mocks in Angular
Angular provides several approaches for creating mocks:
- Manual mocks: Create your own mock objects by hand.
- Jasmine spies: Use Jasmine's spyOn() to mock methods.
- Mock classes: Create simplified versions of actual classes.
- Dependency injection: Angular's testing module allows you to provide mock dependencies.
- HttpClientTestingModule: Specialized for mocking HTTP requests.
Let's explore each of these approaches with practical examples.
Creating Mock Services
Manual Mocks
The simplest approach is to create a mock service that implements the same interface as the real service:
// Real service
@Injectable()
export class UserService {
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/api/users');
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`/api/users/${id}`);
}
}
// Mock service for testing
export class MockUserService {
getUsers(): Observable<User[]> {
return of([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
}
getUserById(id: number): Observable<User> {
return of({ id, name: id === 1 ? 'John' : 'Jane' });
}
}
Then you can provide this mock in your testing module:
TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useClass: MockUserService }
]
});
Using Jasmine Spies
Jasmine spies allow you to track method calls and define return values:
describe('UserListComponent', () => {
let component: UserListComponent;
let fixture: ComponentFixture<UserListComponent>;
let userService: jasmine.SpyObj<UserService>;
beforeEach(() => {
// Create a spy object with the necessary methods
const spy = jasmine.createSpyObj('UserService', ['getUsers']);
TestBed.configureTestingModule({
declarations: [UserListComponent],
providers: [
{ provide: UserService, useValue: spy }
]
});
fixture = TestBed.createComponent(UserListComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
});
it('should load users on init', () => {
// Set up the spy to return mock data
const mockUsers = [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }];
userService.getUsers.and.returnValue(of(mockUsers));
// Trigger ngOnInit
component.ngOnInit();
// Verify the component property was updated
expect(component.users).toEqual(mockUsers);
// Verify the service was called
expect(userService.getUsers).toHaveBeenCalled();
});
});
Mocking HTTP Requests
Angular provides HttpClientTestingModule
specifically for testing HTTP requests without making actual network calls.
Using HttpClientTestingModule
Here's how to test a component that makes HTTP requests:
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
describe('UserService', () => {
let service: UserService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [UserService]
});
service = TestBed.inject(UserService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => {
// Verify that no requests are outstanding
httpMock.verify();
});
it('should retrieve users from the API', () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
// Make the service call
service.getUsers().subscribe(users => {
expect(users).toEqual(mockUsers);
});
// Set up the mock response
const req = httpMock.expectOne('/api/users');
expect(req.request.method).toBe('GET');
req.flush(mockUsers); // Respond with mock data
});
it('should handle errors', () => {
// Make the service call
service.getUsers().subscribe(
() => fail('should have failed with an error'),
error => {
expect(error.status).toBe(404);
}
);
// Respond with a mock error
const req = httpMock.expectOne('/api/users');
req.flush('Not Found', { status: 404, statusText: 'Not Found' });
});
});
Mocking Component Inputs and Outputs
When testing components that have @Input()
and @Output()
properties, you can mock these interactions as well:
// Component to test
@Component({
selector: 'app-user-detail',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<button (click)="onDelete()">Delete</button>
</div>
`
})
export class UserDetailComponent {
@Input() user: User;
@Output() delete = new EventEmitter<number>();
onDelete() {
this.delete.emit(this.user.id);
}
}
// Test
describe('UserDetailComponent', () => {
let component: UserDetailComponent;
let fixture: ComponentFixture<UserDetailComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserDetailComponent]
});
fixture = TestBed.createComponent(UserDetailComponent);
component = fixture.componentInstance;
});
it('should display user name', () => {
// Mock @Input
component.user = { id: 1, name: 'John Doe' };
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h2').textContent).toContain('John Doe');
});
it('should emit delete event', () => {
// Set up spy to watch for @Output events
spyOn(component.delete, 'emit');
// Mock @Input
component.user = { id: 1, name: 'John Doe' };
fixture.detectChanges();
// Trigger button click
const button = fixture.nativeElement.querySelector('button');
button.click();
// Verify output was emitted with correct value
expect(component.delete.emit).toHaveBeenCalledWith(1);
});
});
Mocking Router and ActivatedRoute
Navigation is common in Angular apps, so understanding how to mock the Router and ActivatedRoute is important:
import { Router } from '@angular/router';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
describe('UserDetailComponent', () => {
let component: UserDetailComponent;
let fixture: ComponentFixture<UserDetailComponent>;
let mockRouter: jasmine.SpyObj<Router>;
beforeEach(() => {
// Create router spy
mockRouter = jasmine.createSpyObj('Router', ['navigate']);
// Create mock ActivatedRoute
const mockActivatedRoute = {
paramMap: of(convertToParamMap({ id: '1' })),
snapshot: {
paramMap: {
get: (key: string) => '1'
}
}
};
TestBed.configureTestingModule({
declarations: [UserDetailComponent],
providers: [
{ provide: Router, useValue: mockRouter },
{ provide: ActivatedRoute, useValue: mockActivatedRoute }
]
});
fixture = TestBed.createComponent(UserDetailComponent);
component = fixture.componentInstance;
});
it('should navigate to users list after deletion', () => {
component.deleteUser();
expect(mockRouter.navigate).toHaveBeenCalledWith(['/users']);
});
});
Real-World Example: Testing a UserDashboardComponent
Let's put everything together in a realistic example. We'll test a UserDashboardComponent
that depends on multiple services:
// Component to test
@Component({
selector: 'app-user-dashboard',
template: `
<div class="dashboard">
<h1>User Dashboard</h1>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="users">
<app-user-card
*ngFor="let user of users"
[user]="user"
(select)="onSelectUser($event)">
</app-user-card>
</div>
</div>
`
})
export class UserDashboardComponent implements OnInit {
users: User[] = [];
loading = false;
error: string = null;
constructor(
private userService: UserService,
private authService: AuthService,
private router: Router
) {}
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.error = null;
this.userService.getUsers().pipe(
catchError(err => {
this.error = 'Failed to load users';
return throwError(err);
}),
finalize(() => this.loading = false)
).subscribe(users => {
this.users = users;
});
}
onSelectUser(userId: number) {
if (this.authService.hasPermission('view_user_details')) {
this.router.navigate(['/users', userId]);
}
}
}
// Test
describe('UserDashboardComponent', () => {
let component: UserDashboardComponent;
let fixture: ComponentFixture<UserDashboardComponent>;
let userService: jasmine.SpyObj<UserService>;
let authService: jasmine.SpyObj<AuthService>;
let router: jasmine.SpyObj<Router>;
beforeEach(() => {
// Create spies
const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']);
const authServiceSpy = jasmine.createSpyObj('AuthService', ['hasPermission']);
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
declarations: [
UserDashboardComponent,
// Mock child component
MockComponent(UserCardComponent)
],
providers: [
{ provide: UserService, useValue: userServiceSpy },
{ provide: AuthService, useValue: authServiceSpy },
{ provide: Router, useValue: routerSpy }
]
});
fixture = TestBed.createComponent(UserDashboardComponent);
component = fixture.componentInstance;
userService = TestBed.inject(UserService) as jasmine.SpyObj<UserService>;
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
router = TestBed.inject(Router) as jasmine.SpyObj<Router>;
});
it('should load users on init', () => {
const mockUsers = [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
];
userService.getUsers.and.returnValue(of(mockUsers));
fixture.detectChanges(); // triggers ngOnInit
expect(component.users).toEqual(mockUsers);
expect(component.loading).toBeFalse();
expect(component.error).toBeNull();
});
it('should handle error when loading users fails', () => {
userService.getUsers.and.returnValue(throwError('Server error'));
fixture.detectChanges(); // triggers ngOnInit
expect(component.error).toBe('Failed to load users');
expect(component.loading).toBeFalse();
});
it('should navigate to user details when user has permission', () => {
authService.hasPermission.and.returnValue(true);
component.onSelectUser(1);
expect(authService.hasPermission).toHaveBeenCalledWith('view_user_details');
expect(router.navigate).toHaveBeenCalledWith(['/users', 1]);
});
it('should not navigate when user lacks permission', () => {
authService.hasPermission.and.returnValue(false);
component.onSelectUser(1);
expect(authService.hasPermission).toHaveBeenCalledWith('view_user_details');
expect(router.navigate).not.toHaveBeenCalled();
});
});
Best Practices for Mocking in Angular
- Only mock what you need: If a dependency isn't relevant to the test, consider using the real implementation.
- Keep mocks simple: Implement only the methods your component or service actually uses.
- Use TypeScript to ensure mock compatibility: Make sure your mocks implement the same interfaces as the real dependencies.
- Reset spies between tests: Use
beforeEach
to reset or recreate spy objects to avoid test pollution. - Test edge cases: Use mocks to simulate error states, loading states, and boundary conditions.
- Don't over-mock: If you're mocking everything, you might not be testing anything valuable.
- Prefer constructor injection: This makes dependencies explicit and easier to mock.
Summary
Mocking is an essential technique in Angular testing that allows you to isolate components and services from their dependencies. By using mocks, you can create faster, more reliable tests that focus on specific unit behaviors.
In this guide, we've covered:
- Why mocking is important for effective testing
- Different approaches to mocking in Angular testing
- How to create mock services and spy on methods
- Techniques for mocking HTTP requests
- How to mock component inputs and outputs
- Testing components with router and service dependencies
- Best practices for effective mocking
With these techniques, you should be well-equipped to write robust, maintainable tests for your Angular applications.
Additional Resources
- Angular Testing Guide
- Jasmine Documentation
- HttpClientTestingModule Documentation
- Testing Components in Angular
Exercises
- Create a mock for a service that uses HTTP requests and test a component that depends on it.
- Write tests for a component that has both inputs and outputs, mocking parent component interactions.
- Test a service that depends on another service, using a mock for the dependency.
- Create a test for a component that uses the router for navigation, mocking the router service.
- Write a test that verifies error handling in a component when a service call fails.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)