DEV Community

LS
LS

Posted on

A Practical Strategy for Angular Testing

Testing in Angular is both a discipline and an investment. Done well, it ensures confidence in rapid releases, prevents regressions, and makes code easier to maintain. But writing effective tests isn’t just about hitting 80% coverage—it’s about designing a strategy that balances unit tests, integration tests, and end-to-end (E2E) tests while keeping tests fast, reliable, and meaningful.

  1. Testing Pyramid for Angular

A healthy Angular test suite should follow a pyramid-like structure:

Unit Tests (foundation, ~70%)
Fast, isolated, verifying a single component, service, or pipe.

Integration Tests (~20%)
Combine multiple Angular building blocks, e.g., a component with its service.

End-to-End Tests (~10%)
High-level UI flows using tools like Cypress or Playwright.

The goal is not “all tests everywhere,” but “right tests at the right level.”

  1. Unit Testing Best Practices Services

Use HttpClientTestingModule with HttpTestingController to intercept API calls.

Mock dependencies with spies (jasmine.createSpyObj) instead of real implementations.

Test input → output: e.g., does saveBulkAction() call the correct URL with headers?

it('should PUT with headers and task body', () => {
service.saveBulkAction(payload, appId, true, false, 't-1', task).subscribe();

const req = httpMock.expectOne(r => r.urlWithParams.includes('application-bulk'));
expect(req.request.method).toBe('PUT');
expect(req.request.headers.get('Current-Entitlement')).toBe('ent-123');
expect(req.request.body.task).toEqual(task);
});

Components

Use Angular’s TestBed and shallow render where possible.

Stub child components with ng-mocks or NO_ERRORS_SCHEMA to avoid testing Angular itself.

Focus on inputs, outputs, and template bindings.

it('should emit value on button click', () => {
const button = fixture.debugElement.query(By.css('button'));
spyOn(component.submit, 'emit');
button.nativeElement.click();
expect(component.submit.emit).toHaveBeenCalled();
});

Pipes & Utilities

Keep these simple: one test per logical branch is enough.

  1. Integration Testing

Sometimes you need to ensure multiple Angular parts work together:

Test a component with its real service but mock only HTTP calls.

Use TestBed.inject to bring in services and verify real Angular DI wiring.

Useful for catching template + service mismatches.

  1. End-to-End Testing

Unit and integration tests cover correctness, but E2E ensures the user journey works:

Use Cypress or Playwright for realistic browser interactions.

Keep E2E tests short and focused on critical flows (login, form submit, checkout).

Run them in CI/CD nightly or pre-release; don’t block dev flow with slow suites.

  1. Coverage vs. Confidence

Coverage is a metric, not a goal. Aim for meaningful coverage:

Cover decision branches (if/else, error handling).

Cover public APIs of services and components.

Don’t waste time on trivial Angular boilerplate (e.g., ngOnInit with no logic).

  1. Practical Tips

LocalStorage / SessionStorage: Mock with spies in unit tests.

Async testing: Prefer fakeAsync with tick() over done() callbacks.

Error handling: Always test how services/components behave on error responses.

Test doubles: Use factories for mock data (avoid copy-pasting JSON blobs).

CI/CD: Fail fast—run unit tests on every push, E2E tests on merge to main.

  1. Example Angular Testing Strategy (TL;DR)

Unit tests for every service, pipe, and component (70%).

Integration tests for components + services together (20%).

E2E tests for top workflows only (10%).

Focus on confidence, not coverage.

Keep tests fast, isolated, and readable.

Top comments (0)