Welcome to our complete guide on Angular testing! Ensuring your application works as expected requires a solid testing strategy. Let’s explore the Angular testing ecosystem in depth.
1. Angular Testing Pyramid
A balanced testing approach:
text
E2E (5%)
/ \
Integration (15%)
/ \
Unit Tests (80%)
2. Unit Testing Components
Basic Component Test
typescript
describe('ButtonComponent', () => {
let component: ButtonComponent;
let fixture: ComponentFixture<ButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ButtonComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should emit on click', () => {
spyOn(component.clicked, 'emit');
const button = fixture.nativeElement.querySelector('button');
button.click();
expect(component.clicked.emit).toHaveBeenCalled();
});
});
Testing Component Templates
typescript
it('should display the input label', () => {
component.label = 'Test Label';
fixture.detectChanges();
const label = fixture.nativeElement.querySelector('label');
expect(label.textContent).toContain('Test Label');
});
3. Testing Services
HTTP Service Testing
typescript
describe('DataService', () => {
let service: DataService;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule]
});
service = TestBed.inject(DataService);
httpTestingController = TestBed.inject(HttpTestingController);
});
afterEach(() => {
httpTestingController.verify(); // Verify no outstanding requests
});
it('should fetch data', () => {
const testData = { id: 1, name: 'Test' };
service.getData(1).subscribe(data => {
expect(data).toEqual(testData);
});
const req = httpTestingController.expectOne('api/data/1');
expect(req.request.method).toBe('GET');
req.flush(testData); // Simulate response
});
});
4. Integration Testing
Testing Component Interactions
typescript
describe('UserListComponent', () => {
let fixture: ComponentFixture<UserListComponent>;
let userService: UserService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [UserListComponent, UserCardComponent],
providers: [UserService],
imports: [SharedModule]
}).compileComponents();
userService = TestBed.inject(UserService);
spyOn(userService, 'getUsers').and.returnValue(of([
{ id: 1, name: 'Test User' }
]));
});
it('should display users from service', () => {
fixture.detectChanges();
const cards = fixture.nativeElement.querySelectorAll('app-user-card');
expect(cards.length).toBe(1);
expect(cards[0].textContent).toContain('Test User');
});
});
5. End-to-End (E2E) Testing with Cypress
typescript
describe('Login Flow', () => {
it('should login successfully', () => {
cy.visit('/login');
cy.get('[data-cy=email]').type('test@example.com');
cy.get('[data-cy=password]').type('password123');
cy.get('[data-cy=submit]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-cy=welcome-message]').should('contain', 'Welcome');
});
});
6. Advanced Testing Techniques
Testing RxJS Streams
typescript
it('should debounce search input', fakeAsync(() => {
const searchService = TestBed.inject(SearchService);
spyOn(searchService, 'search');
component.searchControl.setValue('test');
tick(300); // Wait for debounce
expect(searchService.search).toHaveBeenCalledWith('test');
}));
Component Harnesses (for Material)
typescript
describe('MatSelectHarness', () => {
let loader: HarnessLoader;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MatSelectModule],
declarations: [SelectComponent]
}).compileComponents();
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should select option', async () => {
const select = await loader.getHarness(MatSelectHarness);
await select.open();
const options = await select.getOptions();
await options[1].click();
expect(await select.getValueText()).toBe('Option 2');
});
});
7. Code Coverage Reports
Generate reports with:
bash
ng test --code-coverage
Coverage thresholds in karma.conf.js:
javascript
coverageReporter: {
check: {
global: {
statements: 80,
branches: 75,
functions: 85,
lines: 80
}
}
}
8. Continuous Integration Setup
Example GitHub Actions config (.github/workflows/test.yml):
yaml
name: Angular Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm ci
- run: npm run test -- --watch=false --browsers=ChromeHeadless
- run: npm run e2e
Testing Best Practices
- Test Behavior, Not Implementation
- Keep Tests Isolated
- Use Descriptive Test Names
- Follow the Arrange-Act-Assert Pattern
- Maintain Fast, Deterministic Tests
- Test Edge Cases and Error Conditions
Hands-On Testing Challenge
For a simple counter component:
- Write unit tests for all public methods
- Test template bindings
- Verify emitted events
- Create an integration test with a parent component
- Write a Cypress E2E test
What’s Next?
In Part 15, we’ll explore Angular Deployment Strategies – how to get your app production-ready!
