Unit Testing with Jasmine and Karma
Angular's classic testing setup pairs two independent tools. Jasmine is a behavior-driven testing framework that supplies the vocabulary for writing specs: describe() blocks group related tests, it() blocks define individual expectations, and matchers like toBe(), toEqual(), and toHaveBeenCalled() assert outcomes. Karma is the test runner—it launches a real browser (or a headless one such as ChromeHeadless), injects your compiled spec files, executes them, and reports pass/fail results back to the terminal or CI pipeline. The Angular CLI wires these together automatically: ng generate creates a *.spec.ts file alongside every component, service, and pipe, and ng test boots Karma with a preconfigured karma.conf.js. Newer Angular CLI versions (17+) have shifted the default builder toward Jest or Web Test Runner for some starter projects, but Jasmine/Karma remains the long-standing default and is still widely used in production codebases and interview contexts.
Cricket analogy: Jasmine and Karma pairing is like a coaching manual (Jasmine) providing drill vocabulary — describe a session, define a drill, assert a technique is correct — while a real net session (Karma) actually runs the players through it and reports who passed, with the CLI auto-generating a scoresheet template for every new player.
Anatomy of a Spec File
A spec file mirrors the structure of the code it tests. The outer describe() names the unit under test, a beforeEach() block resets shared state before every test so tests don't leak side effects into one another, and each it() expresses a single behavioral expectation in plain language. Jasmine spies (jasmine.createSpy() or spyOn()) let you replace real dependencies with fakes, track calls, and control return values without touching the network or the DOM—critical for keeping unit tests fast and deterministic.
Cricket analogy: A describe() naming the unit under test with a beforeEach() resetting shared state is like a fresh net session where the pitch is re-rolled before every batter faces a new bowler, and a Jasmine spy replacing a real dependency is like using a bowling machine set to a known speed instead of an unpredictable live bowler.
import { CalculatorService } from './calculator.service';
describe('CalculatorService', () => {
let service: CalculatorService;
beforeEach(() => {
service = new CalculatorService();
});
it('should add two numbers correctly', () => {
expect(service.add(2, 3)).toBe(5);
});
it('should throw when dividing by zero', () => {
expect(() => service.divide(10, 0)).toThrowError('Division by zero');
});
it('should call the logger spy exactly once', () => {
const logSpy = jasmine.createSpy('log');
service.onResult = logSpy;
service.add(1, 1);
expect(logSpy).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledWith(2);
});
});Testing Services with Dependency Injection
Services that depend on other injectables are best tested through Angular's TestBed rather than by manual instantiation, because TestBed resolves the dependency graph the same way the real application does. TestBed.configureTestingModule() registers providers—often replacing a real HttpClient or a third-party SDK with a mock—and TestBed.inject() retrieves the configured instance. This keeps tests fast while still exercising Angular's DI resolution logic, catching wiring mistakes that a hand-instantiated object would never reveal.
Cricket analogy: Testing a service through TestBed instead of manual instantiation is like fielding a full training XI resolved through the actual selection process instead of hand-picking eleven random club players, catching wiring mistakes a scratch team would never reveal.
Karma requires a real browser engine, which is why CI pipelines typically install ChromeHeadless (via the Puppeteer or Karma-Chrome-Launcher packages) rather than a full desktop browser. Jest, by contrast, runs entirely in Node using jsdom to simulate the DOM, which is faster to start but doesn't execute in an actual browser rendering engine—a trade-off worth knowing when comparing Angular's default stack to tools used in React or Vue projects.
Forgetting to reset spies or shared mutable state between tests is one of the most common sources of flaky suites. Always initialize fixtures and spies inside beforeEach() rather than at the describe() level, so each test starts from a clean, predictable baseline.
Code Coverage and CI Integration
Running ng test --code-coverage produces an Istanbul-based coverage report showing which statements, branches, functions, and lines were exercised. Teams often enforce minimum coverage thresholds in karma.conf.js to prevent regressions. For continuous integration, ng test --watch=false --browsers=ChromeHeadless runs the suite once and exits with a non-zero code on failure, making it suitable for pipeline gating.
Cricket analogy: Running ng test --code-coverage for an Istanbul report is like a post-match analysis showing exactly which deliveries, shots, and fielding positions were actually tested during the innings, with teams enforcing a minimum coverage threshold like a board mandating every net session hit a fitness benchmark, and --watch=false --browsers=ChromeHeadless is like a scheduled fitness test run once with a pass/fail result for selection.
- Jasmine provides the describe/it/expect syntax and matchers; Karma is the browser-based test runner that executes the compiled specs.
- ng generate scaffolds a *.spec.ts file automatically for components, services, directives, and pipes.
- Use spyOn() or jasmine.createSpy() to fake dependencies and verify interactions without hitting real network or DOM APIs.
- TestBed.configureTestingModule() and TestBed.inject() resolve services through Angular's real dependency injection graph.
- beforeEach() should reset fixtures and spies to avoid state leaking between tests and causing flakiness.
- ng test --code-coverage generates an Istanbul report to track statement, branch, and line coverage over time.
Practice what you learned
1. In the Jasmine/Karma stack, what is Karma's specific responsibility?
2. Why should service unit tests generally use TestBed instead of `new MyService()`?
3. What is the main purpose of a Jasmine spy created with spyOn()?
4. Why is beforeEach() commonly used to reset spies and fixtures before every test?
5. What does ng test --code-coverage produce?
Was this page helpful?
You May Also Like
Testing Components with TestBed
See how Angular's TestBed compiles and instantiates real components in a test environment, letting you inspect the DOM, trigger change detection, and simulate user interaction.
Creating and Injecting Services
Learn how to define an Angular service class, register it with Angular's dependency injection system, and inject it into components and other services.
HttpClient and Fetching Data
Learn how Angular's HttpClient issues typed, Observable-based HTTP requests, and how to provide it in a standalone application.
The Angular CLI
How the Angular CLI's ng commands generate code, run builds, and enforce consistent project structure across the development lifecycle.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics