Angular Unit Testing Mastery: Jasmine, Karma, and Component Testing Explained
Angular Unit Testing Mastery: Jasmine, Karma, and Component Testing Explained
Angular Unit Testing Mastery: Jasmine, Karma, and Component Testing Explained
Why Component Testing Is Crucial
You’ve written a stunning Angular component. It looks perfect, but do you know if it produces the expected results?
Manual testing is too time-consuming and prone to error.
Component testing with Jasmine and Karma automates the process, allowing you to validate your UI logic and user interactions in milliseconds.
This guide includes the following topics:
- How to test Angular components (including inputs, outputs, and DOM interactions)
- Advanced techniques on services, HTTP calls, and user events
- Tools such as TestBed, ComponentFixture, and Spies
- Real-world examples to bulletproof your app.
Step by Step for Component Testing
1. Create a Simple Counter Component
Let's write a CounterComponent with increment/decrement buttons.
// counter.component.ts
@Component({
selector: 'app-counter',
template: `
<button (click)="decrement()">-</button>
{{ count }}
<button (click)="increment()">+</button>
`
})
export class CounterComponent {
count = 0;
increment() { this.count++; }
decrement() { this.count--; }
}
2. Write Test for Component's Initial State, Button Click, and UI Updates
// counter.component.spec.ts
describe('CounterComponent', () => {
let component: CounterComponent;
let fixture: ComponentFixture<CounterComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [CounterComponent]
}).compileComponents();
fixture = TestBed.createComponent(CounterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should start with count 0', () => {
expect(component.count).toBe(0);
});
it('should increment count when + is clicked', () => {
const button = fixture.nativeElement.querySelector('button:last-child');
button.click();
fixture.detectChanges();
expect(component.count).toBe(1);
expect(fixture.nativeElement.querySelector('span').textContent).toBe('1');
});
it('should decrement count when - is clicked', () => {
component.count = 5;
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button:first-child');
button.click();
expect(component.count).toBe(4);
});
});
Take Home Points:
- TestBed: Sets up the test environment (like NgModule).
- ComponentFixture: Wraps component instance and template.
- detectChanges(): Makes Angular detect changes and update the view.
Testing Component Inputs and Outputs
Example: Notification Banner Component
Test a component that takes an @Input() message and emits an @Output()on dismissal.
// notification.component.ts
@Component({
selector: 'app-notification',
template: `
<div>
{{ message }}
<button (click)="dismiss()">Close</button>
</div>
`
})
export class NotificationComponent {
@Input() message: string;
@Output() closed = new EventEmitter<void>();
dismiss() {
this.closed.emit();
}
}
Test Cases
// notification.component.spec.ts
it('should display the input message', () => {
component.message = 'Hello!';
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.textContent).toContain('Hello!');
});
it('should emit event when dismissed', () => {
component.message = 'Test';
fixture.detectChanges();
spyOn(component.closed, 'emit');
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(component.closed.emit).toHaveBeenCalled();
});
Testing Component with Dependencies
Test a UserProfileComponent that relies on a UserService
1. Mock the Dependency
// user.service.mock.ts
class MockUserService {
getUser() {
return of({ name: 'Alice', email: 'alice@test.com' });
}
}
// user-profile.component.spec.ts
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [UserProfileComponent],
providers: [
{ provide: UserService, useClass: MockUserService }
]
});
fixture = TestBed.createComponent(UserProfileComponent);
});
2. Test Data Rendering
it('should display user data', () => {
fixture.detectChanges(); // Triggers ngOnInit
const nameEl = fixture.nativeElement.querySelector('.name');
expect(nameEl.textContent).toContain('Alice');
});
Best Practices for Component Testing
- Test Templates and Logic: Validate both UI and component class logic.
- Use async/fakeAsync: Handle timers and asynchronous operations.
- Leverage Angular Utilities: DebugElement, By.css(), and triggerEventHandler().
- Isolate Tests: Mock services to avoid HTTP calls or state leaks.
Advanced Techniques
Simulating User Input with fakeAsync
Test a form input with debounce:
import { fakeAsync, tick } from '@angular/core/testing';
it('should update search term after debounce', fakeAsync(() => {
const input = fixture.nativeElement.querySelector('input');
input.value = 'test';
input.dispatchEvent(new Event('input'));
tick(300); // Fast-forward 300ms
expect(component.searchTerm).toBe('test');
}));
Code Coverage Reports
Generate coverage stats:
ng test --code-coverage
Open coverage/index.html to see which paths lack test coverage.
Conclusion: Key Point Rating
Testing components by simulating clicks, inputs, and outputs
TestBed for module setup and dependency simulation
Combine unit tests with integration tests for full code coverage