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!