Jasmine is Angular’s default test framework — it ships with every Angular project via the CLI. Its describe/it/expect structure maps directly to how tests are conceptualised: a suite of tests (describe) containing individual test cases (it) that make assertions (expect). Jasmine spies are the Angular equivalent of Moq mocks — they track method calls, stub return values, and verify interactions. Understanding spies is essential for testing services and components that depend on injected dependencies.
Jasmine Core Patterns
// ── describe/it/expect fundamentals ──────────────────────────────────────
describe('SlugGenerator', () => {
// beforeEach runs before every it() in this describe block
let generator: SlugGenerator;
beforeEach(() => {
generator = new SlugGenerator();
});
it('should convert title to lowercase hyphenated slug', () => {
const result = generator.generate('Getting Started with .NET');
expect(result).toBe('getting-started-with-net');
});
it('should remove special characters', () => {
expect(generator.generate('Hello, World!')).toBe('hello-world');
});
// Nested describe for grouping related cases
describe('with unicode input', () => {
it('should normalise accented characters', () => {
expect(generator.generate('Café au lait')).toBe('cafe-au-lait');
});
it('should return empty string for all non-ASCII', () => {
expect(generator.generate('日本語')).toBe('');
});
});
});
// ── Jasmine matchers reference ─────────────────────────────────────────────
expect(value).toBe(3); // strict equality (===)
expect(obj).toEqual({ id: 1, name: 'Test' });// deep equality
expect(array).toContain('item'); // array/string contains
expect(str).toMatch(/pattern/); // regex match
expect(fn).toThrow(); // function throws
expect(fn).toThrowError('message'); // throws with message
expect(value).toBeNull();
expect(value).toBeDefined();
expect(value).toBeTruthy();
expect(num).toBeGreaterThan(5);
expect(num).toBeCloseTo(3.14, 2); // float equality with precision
// ── Jasmine spies — the key tool for mocking ──────────────────────────────
describe('PostsApiService', () => {
let http: jasmine.SpyObj<HttpClient>;
let service: PostsApiService;
beforeEach(() => {
// Create a spy object — all methods are automatically spied
http = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
// Configure the spy's return value
http.get.and.returnValue(of({
items: [], total: 0, page: 1, pageSize: 10,
hasNextPage: false, hasPrevPage: false,
}));
service = new PostsApiService(http, { apiUrl: '' } as any);
});
it('should call GET /api/posts', () => {
service.getPublished().subscribe();
expect(http.get).toHaveBeenCalledWith('/api/posts', jasmine.any(Object));
expect(http.get).toHaveBeenCalledTimes(1);
});
it('should handle 404 as empty result', () => {
http.get.and.returnValue(throwError(() => ({ status: 404 })));
let result: any;
service.getPublished().subscribe({ error: e => result = e });
expect(result.status).toBe(404);
});
});
// ── fakeAsync and tick — testing synchronous-feeling async code ───────────
it('should debounce search after 350ms', fakeAsync(() => {
const searchSpy = spyOn(component, 'onSearch');
component.searchControl.setValue('dotnet');
tick(349);
expect(searchSpy).not.toHaveBeenCalled();
tick(1); // total 350ms
expect(searchSpy).toHaveBeenCalledWith('dotnet');
}));
toBe() uses JavaScript’s strict equality (===) — it checks reference equality for objects. toEqual() performs deep recursive comparison of object properties. For primitives (number, string, boolean), they are equivalent. For objects and arrays, always use toEqual() — two different array instances with identical contents fail toBe() but pass toEqual(). Use toBe() when you want to verify the exact same object reference is returned (rarely needed in Angular tests).fakeAsync() and tick() allow testing time-based async operations synchronously. Without fakeAsync, testing debounceTime(350) requires waiting 350ms in the test. With fakeAsync, Jasmine replaces the timer system with a fake — tick(350) advances the virtual clock by 350ms instantly. Use fakeAsync for any test involving setTimeout, setInterval, RxJS operators with timers (debounceTime, timer, interval), and Angular’s change detection with async pipe.this.getMethod = obj.method in the constructor), spyOn(obj, 'method') after construction will not intercept calls made through the cached reference. Always spy before creating the object that will use the method, or spy on the method before the call is made.Common Mistakes
Mistake 1 — Using toBe() for object/array comparison (always fails)
❌ Wrong — expect(result).toBe({ id: 1 }); different object references; fails even with identical properties.
✅ Correct — expect(result).toEqual({ id: 1 }); deep property comparison; passes for equivalent objects.
Mistake 2 — Testing async code without fakeAsync/async (test passes before assertion runs)
❌ Wrong — it('should ...', () => { service.loadAsync().subscribe(); expect(result).toBe(x) }); assertion runs before Observable emits.
✅ Correct — use fakeAsync + tick() or async/await with firstValueFrom() to wait for async completion.