Jasmine Interview Questions and Answers
Last updated:
Check out 35 of the most common Jasmine interview questions, then take an AI-powered practice interview
Q1What is the difference between Jasmine's declaration phase and its execution phase?
BasicExecution Model
Answer
Jasmine builds the entire suite tree before it evaluates a single expectation. When the runner loads your spec files it executes every describe callback synchronously, top to bottom. Calls to it, beforeEach, afterEach, beforeAll and afterAll do not run your callbacks at that moment, they only register them on the enclosing suite node.
Once every file is loaded, Jasmine walks the tree and executes the registered functions. So any statement written directly inside a describe body, a new Cart(), a call to a fixture factory, a console.log, runs exactly once during loading, long before the first it. Three consequences show up constantly in interviews.
First, state created in a describe body is shared by every spec in that block, so one spec mutating it silently changes what the next spec sees. Second, because Jasmine randomises spec order by default, that shared-state bug shows up as a spec that passes on your machine and fails in CI under a different seed. Third, anything expensive in a describe body is paid at load time for the whole file even when you narrow the run with --filter, so heavy setup there slows down every single invocation.
The fix is boring and reliable: declare with let in the describe body, assign inside beforeEach. Interviewers also check whether you know spec names are fixed at load time, which is why data-driven suites loop with a plain forEach over a literal array inside describe rather than awaiting data first.
// WRONG: cart is built once at load time and shared by both specs
describe('Cart', () => {
const cart = new Cart();
it('adds an item', () => {
cart.add({ sku: 'A', price: 100 });
expect(cart.total()).toBe(100);
});
it('starts empty', () => {
expect(cart.total()).toBe(0); // fails if the spec above ran first
});
});
// RIGHT: fresh instance per spec
describe('Cart', () => {
let cart;
beforeEach(() => {
cart = new Cart();
});
it('adds an item', () => {
cart.add({ sku: 'A', price: 100 });
expect(cart.total()).toBe(100);
});
it('starts empty', () => {
expect(cart.total()).toBe(0);
});
});
Key Points
- describe bodies run at load time; it/beforeEach callbacks run later
- Anything constructed in a describe body is shared across specs
- Random ordering turns that sharing into an intermittent CI failure
- Declare with let in describe, assign in beforeEach
Q2When do you use toBe, toEqual, toBeTruthy and toBeCloseTo in Jasmine?
BasicMatchers
Answer
toBe is identity: it passes only when actual === expected, so it is correct for primitives, enum values and reference checks such as expect(returned).toBe(sameInstance). Using it on two structurally identical object literals always fails, and Jasmine prints a diff that looks confusingly equal, which is why beginners lose time on it. toEqual is the deep structural matcher. It walks arrays, plain objects, Date, RegExp, Map, Set and DOM nodes recursively, and it understands asymmetric matchers embedded anywhere inside the structure, so expect(user).toEqual({ id: jasmine.any(Number), name: 'Asha' }) works. toBeTruthy and toBeFalsy apply JavaScript truthiness, which means expect('false').toBeTruthy() passes and expect(0).toBeFalsy() passes.
When you actually mean the boolean literal, use toBeTrue and toBeFalse instead, they assert === true and === false and catch a function that returns a truthy string when it was supposed to return a boolean. toBeCloseTo exists because binary floating point makes expect(0.1 + 0.2).toBe(0.3) fail, its second argument is the number of decimal places to compare, defaulting to 2. Other matchers worth naming in an interview: toContain for arrays, strings and Sets, toHaveSize for length or size without reaching into .length, toBeInstanceOf, toBeNull, toBeUndefined and toBeNaN. Interviewers usually follow up by asking which matcher gives the best failure message, and the honest answer is the most specific one available, because toBeTruthy on a complex assertion prints 'Expected false to be truthy' and tells you nothing.
const a = { id: 1, tags: ['pro'] };
const b = { id: 1, tags: ['pro'] };
expect(a).toBe(a); // passes: same reference
expect(a).toBe(b); // FAILS: different references
expect(a).toEqual(b); // passes: deep structural equality
expect(0.1 + 0.2).toBe(0.3); // FAILS (0.30000000000000004)
expect(0.1 + 0.2).toBeCloseTo(0.3, 10); // passes
expect('false').toBeTruthy(); // passes: non-empty string
expect('false').toBeTrue(); // FAILS: not the boolean true
expect(['a', 'b']).toHaveSize(2);
expect(new Set(['a'])).toContain('a');
expect({ id: 1, createdAt: new Date() }).toEqual({
id: 1,
createdAt: jasmine.any(Date),
});
Key Points
- toBe is ===; toEqual is recursive structural equality
- toBeTrue / toBeFalse are stricter than toBeTruthy / toBeFalsy
- toBeCloseTo(expected, decimalPlaces) for floating point
- toEqual understands jasmine.any and other asymmetric matchers inline
Q3In what order do beforeAll, beforeEach, afterEach and afterAll run across nested describe blocks?
BasicLifecycle Hooks
Answer
For any given spec, Jasmine runs every beforeAll from the outermost suite inward once per suite, then every beforeEach from outermost to innermost, then the spec body, then every afterEach from innermost to outermost, and finally afterAll from innermost to outermost when the suite finishes. The mental model is a set of nested shells: setup unwinds outside in, teardown unwinds inside out, exactly like a stack. beforeAll and afterAll fire once per suite no matter how many specs it contains, so they are the right place for genuinely expensive, read-only setup such as compiling a template or opening a database connection. They are the wrong place for anything mutable, because the second spec inherits whatever the first spec did to it and the failure only appears under certain seeds.
If a beforeEach throws, Jasmine marks the spec failed and skips the spec body but still runs the matching afterEach hooks, which is why cleanup belongs in afterEach rather than at the end of the spec body. If a beforeAll throws, every spec in that suite is reported as failed with the same error, a pattern you will recognise in CI logs as forty identical failures from one bad fixture. Hooks are also async-aware: return a promise or mark the callback async and Jasmine waits, subject to jasmine.DEFAULT_TIMEOUT_INTERVAL. A common interview follow-up is where to put spy cleanup, and the answer is usually nowhere, because spies installed with spyOn are restored automatically at the end of each spec.
describe('outer', () => {
beforeAll(() => console.log('1 outer beforeAll'));
beforeEach(() => console.log('2 outer beforeEach'));
afterEach(() => console.log('6 outer afterEach'));
afterAll(() => console.log('7 outer afterAll'));
describe('inner', () => {
beforeAll(() => console.log(' inner beforeAll (once)'));
beforeEach(() => console.log('3 inner beforeEach'));
afterEach(() => console.log('5 inner afterEach'));
it('runs the spec', () => console.log('4 spec body'));
});
});
// Async hooks: return a promise or use async/await
beforeEach(async () => {
await db.migrate();
});
Key Points
- beforeEach runs outer to inner; afterEach runs inner to outer
- beforeAll / afterAll fire once per suite, not once per spec
- A throwing beforeAll fails every spec in the suite with one error
- afterEach still runs when the spec body or beforeEach throws
Q4How does spyOn work, and what is the difference between and.returnValue, and.callThrough and and.callFake?
BasicSpies
Answer
spyOn(obj, 'method') replaces a property on an existing object with a spy function and records every call to it. By default the spy is a stub: it swallows the call and returns undefined, so the real implementation never executes. That default is deliberate, it forces you to state explicitly what the collaborator should do.
The strategies are chained off .and. Use .and.returnValue(x) for a fixed return, .and.returnValues(a, b, c) when consecutive calls should return different things, .and.callThrough() to record the call and still run the original implementation, .and.callFake(fn) to substitute your own function (the most flexible option, and the one to use when the return value depends on the arguments), .and.throwError('boom') to simulate a failure path, and .and.resolveTo(value) or .and.rejectWith(err) for promise-returning methods, which are cleaner than returnValue(Promise.resolve(value)) because they read as intent. .and.stub() resets the spy back to the do-nothing default, useful when a beforeEach configured a strategy that one specific spec does not want. The behaviour interviewers care about most is restoration: a spy created with spyOn is automatically reverted to the original property when the spec finishes, so you never write manual teardown.
That guarantee disappears the moment you assign a spy yourself with obj.method = jasmine.createSpy(), which leaks into every later spec in the run. Jasmine also throws '<method> has already been spied upon' if you spy twice on the same property in one spec, which usually means a beforeEach and a spec are both setting it up.
describe('PaymentService', () => {
let gateway;
beforeEach(() => {
gateway = { charge: (amt) => ({ status: 'ok', amt }) };
});
it('stubs by default and returns undefined', () => {
spyOn(gateway, 'charge');
expect(gateway.charge(500)).toBeUndefined();
expect(gateway.charge).toHaveBeenCalled();
});
it('returns a canned value', () => {
spyOn(gateway, 'charge').and.returnValue({ status: 'failed' });
expect(gateway.charge(500).status).toBe('failed');
});
it('keeps the real implementation', () => {
spyOn(gateway, 'charge').and.callThrough();
expect(gateway.charge(500)).toEqual({ status: 'ok', amt: 500 });
});
it('computes from arguments', () => {
spyOn(gateway, 'charge').and.callFake((amt) => ({ status: amt > 1000 ? 'review' : 'ok' }));
expect(gateway.charge(2000).status).toBe('review');
});
it('simulates async rejection', async () => {
spyOn(gateway, 'charge').and.rejectWith(new Error('gateway down'));
await expectAsync(gateway.charge(100)).toBeRejectedWithError('gateway down');
});
});
Key Points
- spyOn stubs by default; the original is not called
- callThrough records and executes; callFake substitutes logic
- resolveTo / rejectWith for promise-returning collaborators
- spyOn restores the original automatically at the end of the spec
Q5When do you use jasmine.createSpy versus jasmine.createSpyObj versus spyOn?
BasicSpies
Answer
The three cover different situations. spyOn(obj, 'method') is for replacing a method on an object that already exists, typically a real collaborator or a module namespace, and it is the only one of the three that Jasmine restores automatically after the spec. jasmine.createSpy('name') creates a standalone spy function with no host object, which is what you pass as a callback or an event handler: expect(onSuccess).toHaveBeenCalledWith(payload). Always give it a name, because the failure message quotes it, and 'Expected spy unknown to have been called' wastes review time. jasmine.createSpyObj is for whole fake collaborators. Called as jasmine.createSpyObj('UserApi', ['fetch', 'save']) it returns an object with those two methods already spied and stubbed.
The second form, jasmine.createSpyObj('UserApi', { fetch: of(user), save: true }), takes an object literal and pre-configures each return value in one line, which is the shape Angular teams use for injected services in TestBed providers. You can also pass a third argument to add plain non-spy properties. The trap interviewers look for is that createSpyObj gives you a lying double: it satisfies the TypeScript interface only if you list every method, and any method you forget is simply undefined, so the code under test dies with 'this.userApi.refresh is not a function' rather than a clear test failure. In TypeScript projects, typing the double as jasmine.SpyObj<UserApi> restores compile-time checking and is the pattern most Angular style guides in Indian service companies now mandate.
// 1. standalone callback spy
const onDone = jasmine.createSpy('onDone');
processQueue(items, onDone);
expect(onDone).toHaveBeenCalledOnceWith({ processed: 3 });
// 2. whole fake collaborator, array form
const api = jasmine.createSpyObj('UserApi', ['fetch', 'save']);
api.fetch.and.returnValue(Promise.resolve({ id: 7 }));
// 3. object form: methods and return values in one shot
const api2 = jasmine.createSpyObj('UserApi', {
fetch: Promise.resolve({ id: 7 }),
save: true,
});
// 4. typed double in an Angular spec
let api3: jasmine.SpyObj<UserApi>;
beforeEach(() => {
api3 = jasmine.createSpyObj<UserApi>('UserApi', ['fetch', 'save']);
TestBed.configureTestingModule({
providers: [{ provide: UserApi, useValue: api3 }],
});
});
Key Points
- spyOn replaces a method on a real object and auto-restores
- createSpy makes a nameable standalone function double
- createSpyObj builds a full fake service, array or object form
- jasmine.SpyObj<T> keeps TypeScript checking the double's shape
Q6How do you assert what a spy was called with, and what does the calls API give you?
BasicSpies
Answer
The matchers cover the common cases. toHaveBeenCalled() checks it ran at least once, toHaveBeenCalledTimes(n) pins the count, toHaveBeenCalledWith(...args) passes if any call matched those arguments, and toHaveBeenCalledOnceWith(...args) asserts exactly one call and that its arguments matched, which is the one to reach for when a duplicate call would be a real bug such as double-charging a card. toHaveBeenCalledBefore(otherSpy) checks ordering between two spies. There is also a negative form worth knowing, expect(spy).not.toHaveBeenCalled(), and in recent versions expect(obj).toHaveSpyInteractions() reports whether any spy on a spy object was touched at all. When matchers are not enough, drop into the calls property: spy.calls.count(), spy.calls.argsFor(0) for the first call's argument array, spy.calls.allArgs() for every call, spy.calls.mostRecent() and spy.calls.first() which return an object with args, returnValue, object (the this value) and invocationOrder, and spy.calls.reset() to clear the record without removing the spy, useful when a beforeEach triggers a call you do not want to count.
The gotcha that costs people offers is argument capture by reference. Jasmine stores the argument object itself, not a copy, so if the code under test mutates that object after the call, your assertion sees the mutated version and fails with a diff that makes no sense. spy.calls.saveArgumentsByValue() switches the spy to storing clones. Also remember toHaveBeenCalledWith uses the same deep equality as toEqual, so jasmine.objectContaining and jasmine.any work inside it.
const logger = jasmine.createSpy('logger');
logger({ level: 'warn', msg: 'slow query' });
logger({ level: 'error', msg: 'timeout' });
expect(logger).toHaveBeenCalledTimes(2);
expect(logger).toHaveBeenCalledWith({ level: 'error', msg: 'timeout' });
expect(logger).toHaveBeenCalledWith(
jasmine.objectContaining({ level: 'warn' })
);
expect(logger.calls.argsFor(0)[0].msg).toBe('slow query');
expect(logger.calls.mostRecent().args[0].level).toBe('error');
expect(logger.calls.count()).toBe(2);
logger.calls.reset();
expect(logger).not.toHaveBeenCalled();
// Mutation trap: capture clones instead of references
const save = jasmine.createSpy('save');
save.calls.saveArgumentsByValue();
const draft = { status: 'new' };
save(draft);
draft.status = 'sent';
expect(save).toHaveBeenCalledWith({ status: 'new' }); // still passes
Key Points
- toHaveBeenCalledOnceWith catches accidental duplicate calls
- calls.argsFor / allArgs / mostRecent for fine-grained inspection
- calls.reset() clears history without removing the spy
- Arguments are stored by reference unless saveArgumentsByValue() is on
Q7How do fdescribe, fit, xdescribe, xit and pending() differ, and why are focused specs dangerous in CI?
BasicSuite Control
Answer
xdescribe and xit disable a suite or spec. Jasmine still reports them, but as pending rather than passed, so the summary line reads something like '42 specs, 0 failures, 3 pending'. An it with no callback at all is also pending, which is a legitimate way to leave a placeholder for behaviour you have specified but not implemented. pending('reason') is different again: it is called from inside a running spec, marks that spec pending at runtime with the reason printed in the report, and is the right tool when a spec should be skipped conditionally, for example because a feature flag is off in this environment. fdescribe and fit focus: as soon as one focused node exists anywhere in the run, Jasmine executes only the focused ones and reports every other spec as skipped.
That is excellent while debugging a single failure and catastrophic when committed, because CI will happily report '3 specs, 0 failures' and go green while nine hundred specs never ran. Every team eventually gets burned by this, and the standard defences are worth naming in an interview: an ESLint rule (jasmine/no-focused-tests from eslint-plugin-jasmine, or no-focused-tests in the Jest plugin for equivalent projects), a pre-commit hook grepping for fdescribe and fit, or a CI step that fails the build when the reported spec count drops below a floor. Some teams also gate on the pending count so that xit does not become a permanent parking lot for broken specs.
describe('Invoice', () => {
it('is implemented later'); // no body: reported as pending
xit('skips this one', () => {
expect(true).toBe(false); // never runs
});
it('skips conditionally at runtime', () => {
if (!features.gstEInvoicing) {
pending('GST e-invoicing disabled in this environment');
}
expect(generateIrn(invoice)).toMatch(/^[0-9a-f]{64}$/);
});
fit('only this spec runs in the whole suite', () => {
expect(total(invoice)).toBe(11800);
});
});
// .eslintrc: fail the build if a focused spec is committed
// "plugins": ["jasmine"],
// "rules": { "jasmine/no-focused-tests": "error" }
Key Points
- x-prefix skips, f-prefix focuses the entire run
- pending('reason') skips at runtime from inside the spec
- A committed fdescribe makes CI green while skipping everything else
- Guard with eslint-plugin-jasmine or a pre-commit grep
Q8What are the three ways to test asynchronous code in Jasmine, and how do timeouts work?
BasicAsync Testing
Answer
First, the done callback: declare a parameter on the spec function and Jasmine waits until you call done(). Call done.fail(err) to fail explicitly, though most people now pass the error to done() itself, which Jasmine treats as a failure. Second, return a promise from the spec and Jasmine waits for it to settle, failing the spec if it rejects.
Third, declare the spec async and use await, which is the modern default and by far the easiest to read. All three work identically in beforeEach, afterEach, beforeAll and afterAll. What you must not do is mix them: taking a done parameter and also returning a promise makes Jasmine raise an error telling you the function was passed a done callback but also returned a promise, because it cannot tell which signal to trust.
The other classic failure is forgetting to call done in an error path, which produces 'Timeout: Async function did not complete within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)'. That 5000ms default can be raised globally by assigning to jasmine.DEFAULT_TIMEOUT_INTERVAL in a helper file, or per spec by passing a timeout as the third argument to it, beforeEach or the other hooks. Raising the global timeout is almost always the wrong fix, because it turns a two-second failure into a thirty-second one without making anything more reliable. The correct instinct in an interview is to say that a timeout means either an unresolved promise, an un-awaited call, or a real hang, and to go find which.
// 1. done callback (still needed for event-emitter style APIs)
it('emits ready', (done) => {
emitter.on('ready', (payload) => {
expect(payload.ok).toBeTrue();
done();
});
emitter.start();
});
// 2. return a promise
it('resolves the user', () => {
return api.fetchUser(7).then((u) => expect(u.id).toBe(7));
});
// 3. async / await (preferred)
it('resolves the user', async () => {
const u = await api.fetchUser(7);
expect(u.id).toBe(7);
});
// Per-spec timeout as the third argument (10 seconds)
it('runs a slow migration', async () => {
await db.migrate();
}, 10000);
// Global default, usually set in spec/helpers/timeout.js
jasmine.DEFAULT_TIMEOUT_INTERVAL = 8000;
Key Points
- done callback, returned promise, or async/await; never two at once
- Default timeout is 5000ms via jasmine.DEFAULT_TIMEOUT_INTERVAL
- Third argument to it/beforeEach overrides the timeout per spec
- A timeout usually means a missing await, not a too-small limit
Q9How do you assert that a function throws, and what is the difference between toThrow, toThrowError and toThrowMatching?
BasicMatchers
Answer
All three take a function reference, not the result of calling it. expect(parse(bad)).toThrow() is the mistake everyone makes once: the call happens while building the argument, the error escapes before Jasmine sees it, and the spec fails with the raw exception instead of a clean assertion. You must wrap it: expect(() => parse(bad)).toThrow(). toThrow() with no argument passes for any thrown value. Passing an argument compares the thrown value with deep equality, so toThrow(new Error('bad input')) matches on both constructor and message, which is often stricter than you want. toThrowError is the more precise tool: toThrowError() for any Error instance, toThrowError('bad input') for an exact message, toThrowError(/timeout/) for a message pattern, toThrowError(TypeError) for a type, and toThrowError(RangeError, 'index out of bounds') for both. toThrowMatching(predicate) covers everything else, including custom error classes carrying a code field, which is the shape most production JavaScript uses: toThrowMatching((e) => e.code === 'E_GST_INVALID').
For asynchronous code none of these work, because an async function returns a rejected promise rather than throwing synchronously. Use await expectAsync(promise).toBeRejectedWithError(TypeError, /timeout/) instead. A good interview answer also mentions the negative case, expect(fn).not.toThrow(), and the reason to prefer toThrowError over toThrow in review: toThrow with a string argument checks the thrown value is that exact string, so a codebase that throws Error objects will silently never match and the spec becomes a false negative.
function parseGstin(value) {
if (typeof value !== 'string') throw new TypeError('gstin must be a string');
if (value.length !== 15) {
const err = new RangeError('gstin must be 15 characters');
err.code = 'E_GSTIN_LENGTH';
throw err;
}
return value.toUpperCase();
}
it('rejects non-strings', () => {
expect(() => parseGstin(42)).toThrowError(TypeError, 'gstin must be a string');
});
it('rejects wrong length', () => {
expect(() => parseGstin('27AAA')).toThrowError(RangeError, /15 characters/);
});
it('carries a machine-readable code', () => {
expect(() => parseGstin('27AAA')).toThrowMatching((e) => e.code === 'E_GSTIN_LENGTH');
});
it('accepts a valid value', () => {
expect(() => parseGstin('27AAAPZ1234C1ZV')).not.toThrow();
});
it('handles async rejection', async () => {
await expectAsync(fetchGstin('bad')).toBeRejectedWithError(/not found/);
});
Key Points
- Always pass a function reference, never an invoked call
- toThrowError(Type, messageOrRegex) is the precise form
- toThrowMatching(predicate) for custom errors with a code field
- Async rejections need expectAsync().toBeRejectedWithError()
Q10What are asymmetric matchers like jasmine.any, jasmine.objectContaining and jasmine.stringMatching used for?
BasicMatchers
Answer
Asymmetric matchers are placeholders you embed inside an expected value so that equality is checked loosely at that position while staying exact everywhere else. They work anywhere Jasmine's deep equality runs, which means inside toEqual, inside toHaveBeenCalledWith, and nested arbitrarily deep. The set you should be able to name: jasmine.any(Constructor) matches any instance of a type including Number, String, Boolean, Function, Object, Date and your own classes; jasmine.anything() matches anything except null and undefined; jasmine.objectContaining({...}) matches an object that has at least these properties, ignoring the rest; jasmine.arrayContaining([...]) matches an array containing at least these elements in any order, while jasmine.arrayWithExactContents([...]) requires the same elements in any order and nothing more; jasmine.stringContaining('x') and jasmine.stringMatching(/re/) for substrings and patterns; jasmine.mapContaining and jasmine.setContaining for the corresponding collection types; and jasmine.truthy(), jasmine.falsy(), jasmine.empty() and jasmine.notEmpty() as loose predicates.
This is exactly how you assert on payloads containing values you cannot predict, a generated id, a Date.now() timestamp, a request signature. Without them, teams either freeze the clock for every spec or delete the unpredictable fields before comparing, both of which weaken the assertion. You can also write your own asymmetric matcher: any object with an asymmetricMatch(actual, matchersUtil) method works, and adding jasmineToString() gives you a readable name in failure output instead of the default object dump.
const track = jasmine.createSpy('track');
checkout({ userId: 91, amount: 4999 }, track);
expect(track).toHaveBeenCalledWith(
'purchase_completed',
jasmine.objectContaining({
userId: 91,
orderId: jasmine.stringMatching(/^ORD-[0-9]{8}$/),
at: jasmine.any(Date),
})
);
expect(response.tags).toEqual(jasmine.arrayContaining(['upi', 'india']));
expect(response.tags).toEqual(jasmine.arrayWithExactContents(['india', 'upi']));
expect(response.error).toEqual(jasmine.falsy());
// Custom asymmetric matcher
const validUpiId = {
asymmetricMatch: (actual) => /^[\w.-]+@[a-z]{3,}$/.test(actual),
jasmineToString: () => '<a valid UPI id>',
};
expect({ vpa: 'asha@okaxis' }).toEqual({ vpa: validUpiId });
Key Points
- Work inside toEqual and toHaveBeenCalledWith at any nesting depth
- objectContaining ignores extra keys; arrayWithExactContents does not
- Use them for ids, timestamps and signatures you cannot predict
- Custom ones need asymmetricMatch plus jasmineToString
Q11How do you set up a Jasmine project on Node, and what goes in spec/support/jasmine.json?
BasicTooling
Answer
Install two packages: jasmine-core holds the framework, and jasmine is the Node CLI that loads it. npm i -D jasmine followed by npx jasmine init creates spec/support/jasmine.json and a spec directory. Recent versions also accept an ES module config at spec/support/jasmine.mjs that default-exports the same object, which is what you want in a project with type: module in package.json. The keys that matter: spec_dir is the root Jasmine resolves everything else against; spec_files is an array of globs, defaulting to something like '**/*[sS]pec.?(m)js'; helpers lists files loaded before any spec, the right place for jasmine.DEFAULT_TIMEOUT_INTERVAL, custom matchers and reporters; requires lists modules to require first, typically ts-node/register or a babel hook; and the env object holds runtime behaviour, including random, seed, stopSpecOnExpectationFailure, stopOnSpecFailure, failSpecWithNoExpectations and forbidDuplicateNames.
Anything in env can be overridden on the command line, which is how you reproduce a CI failure locally: npx jasmine --seed=54321 --random=true. Other flags worth knowing are --filter='Cart adds' to run specs whose full name contains a string, --config=spec/support/ci.json to point at a different config, --fail-fast to stop at the first failure, and --reporter=jasmine-spec-reporter to swap the console output. For TypeScript, the common setups are either compiling to a build directory and pointing spec_files there, or registering ts-node through requires so specs run straight from source. Add "test": "jasmine" to package.json scripts so CI and developers run the identical command.
// package.json
// "scripts": { "test": "jasmine", "test:ci": "jasmine --config=spec/support/ci.json" }
// spec/support/jasmine.json
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.js"],
"helpers": ["helpers/**/*.js"],
"requires": ["ts-node/register"],
"stopSpecOnExpectationFailure": false,
"random": true,
"env": {
"failSpecWithNoExpectations": true,
"forbidDuplicateNames": true
}
}
// Useful CLI invocations
// npx jasmine --filter="Cart"
// npx jasmine --seed=54321
// npx jasmine --fail-fast
// npx jasmine spec/unit/cart.spec.js
Key Points
- jasmine-core is the framework, jasmine is the Node CLI
- npx jasmine init scaffolds spec/support/jasmine.json
- helpers load before specs; requires loads ts-node or babel hooks
- Every env key has a matching CLI flag for reproducing CI runs
Q12How does jasmine.clock() let you test setTimeout, setInterval and Date without waiting?
BasicFake Clock
Answer
jasmine.clock().install() swaps the global setTimeout, clearTimeout, setInterval and clearInterval with fake implementations that never fire on their own. You then advance virtual time with jasmine.clock().tick(ms), and every callback scheduled to run within that window executes synchronously, in order, before tick returns. jasmine.clock().uninstall() puts the real functions back, and it belongs in an afterEach, because leaving the clock installed makes every later spec in the file hang or time out with the misleading 5000ms message. jasmine.clock().mockDate(new Date('2026-04-01T09:30:00+05:30')) additionally freezes Date so that new Date() and Date.now() return that instant, and each tick advances the mocked date by the same amount, which is what you need for testing anything that stamps a created_at or computes an age. There is also jasmine.clock().withMock(fn), which installs, runs your function and uninstalls automatically, handy for a one-off spec.
Two important limitations come up in interviews. The fake clock does not touch requestAnimationFrame, process.nextTick or promise microtasks, so a debounce implemented with setTimeout is testable but a retry loop built on await new Promise(r => setTimeout(r, 100)) needs both a tick and a real await for the microtask queue to drain. And in Angular, the framework's own fakeAsync/tick helpers from @angular/core/testing are usually the better fit, because they also drain the zone's microtask queue, whereas jasmine.clock() knows nothing about Zone.js.
describe('debounce', () => {
beforeEach(() => jasmine.clock().install());
afterEach(() => jasmine.clock().uninstall());
it('fires once after the quiet period', () => {
const onSearch = jasmine.createSpy('onSearch');
const debounced = debounce(onSearch, 300);
debounced('sh');
debounced('shi');
debounced('shirt');
jasmine.clock().tick(299);
expect(onSearch).not.toHaveBeenCalled();
jasmine.clock().tick(1);
expect(onSearch).toHaveBeenCalledOnceWith('shirt');
});
it('stamps a deterministic timestamp', () => {
jasmine.clock().mockDate(new Date('2026-04-01T09:30:00Z'));
const invoice = createInvoice({ amount: 500 });
expect(invoice.createdAt.toISOString()).toBe('2026-04-01T09:30:00.000Z');
jasmine.clock().tick(60000);
expect(Date.now()).toBe(new Date('2026-04-01T09:31:00Z').getTime());
});
});
Key Points
- install() fakes the timer globals; tick(ms) runs due callbacks
- mockDate() freezes Date.now() and advances with tick
- Always uninstall() in afterEach or later specs will time out
- The clock does not drain promise microtasks or requestAnimationFrame
Q13How do withContext, fail() and failSpecWithNoExpectations improve a Jasmine failure report?
BasicReporting
Answer
Jasmine's default message names the matcher and prints both values, which is fine for a single assertion and useless inside a loop, where 'Expected 118 to be 236' never says which row broke. withContext(string) chains between expect and the matcher and prepends your label to the failure message, so expect(total).withContext('row 3, Maharashtra').toBe(236) points straight at the data. It works with every matcher, including asynchronous ones through expectAsync(p).withContext('...'), and it costs nothing when the expectation passes. fail(message) is the escape hatch for control-flow assertions: it marks the running spec failed with your message, and it belongs in a branch that should never execute, such as the resolve path of a promise you expected to reject or the default arm of a switch. Two things about fail() catch people out.
It does not stop execution, so the rest of the spec body still runs unless you return, and calling it from a callback that fires after the spec finished attaches the failure to whichever spec is running at that moment, producing a mystery failure in an unrelated file. Finally, failSpecWithNoExpectations in the env block of spec/support/jasmine.json turns a spec that ran zero expectations into a failure instead of a silent pass, and prints 'Spec has no expectations'. That one flag catches the most embarrassing class of bug in a suite: assertions sitting after an early return, inside a callback that never fires, or behind an await someone forgot to write. A green suite that asserts nothing is worse than no suite, and interviewers at product teams ask about it for exactly that reason.
// withContext labels which iteration failed
[100, 250, 990].forEach((amount, i) => {
expect(applyGst(amount))
.withContext('row ' + i + ', amount ' + amount)
.toBeCloseTo(amount * 1.18, 2);
});
// fail() covers a branch that must never run
it('rejects an expired token', async () => {
try {
await verify(expiredToken);
fail('verify() should have rejected an expired token');
} catch (e) {
expect(e.code).toBe('E_TOKEN_EXPIRED');
}
});
// spec/support/jasmine.json
// "env": {
// "failSpecWithNoExpectations": true,
// "stopSpecOnExpectationFailure": false
// }
// Now this fails instead of passing silently
it('asserts nothing', () => {
const cart = new Cart();
cart.add({ sku: 'A' });
}); // -> Spec 'asserts nothing' has no expectations.
Key Points
- withContext(label) prefixes the failure message, loop-friendly
- fail(msg) does not return; add an explicit return after it
- failSpecWithNoExpectations turns silent no-op specs into failures
- A late fail() from a stale callback lands on an unrelated spec
Q14What does expectAsync give you that expect does not, and which async matchers exist?
BasicAsync Testing
Answer
expectAsync takes a promise instead of a value and returns matchers that themselves return promises, so every call must be awaited or returned. The set is small and worth memorising: toBeResolved(), toBeResolvedTo(value) which compares with the same deep equality as toEqual, toBePending(), toBeRejected(), toBeRejectedWith(value) and toBeRejectedWithError(ErrorTypeOrMessageOrRegex, optionalMessage). There is also a .already modifier, as in await expectAsync(p).already.toBeResolved(), which asserts the promise has settled at this instant rather than waiting for it, useful when the point of the test is that a cache answered synchronously. withContext works here too.
The dominant failure mode is forgetting await: expectAsync(p).toBeRejected() without await schedules the check, the spec body finishes, the spec is reported as passed, and the eventual failure either vanishes or is attributed to a later spec. Turning on failSpecWithNoExpectations does not save you, because the expectation was registered. The second gotcha is that expectAsync only accepts a promise or thenable and throws 'Expected expectAsync to be called with a promise' otherwise, which happens when you pass the result of an already-awaited call.
The third is timeouts: a promise that never settles produces the standard 'Async function did not complete within 5000ms' message, not an assertion failure, so the report tells you nothing about which promise hung. Compared with the older pattern of try/catch plus fail(), expectAsync is shorter and cannot accidentally pass when the promise resolves, which is precisely why reviewers ask for it in Angular and Node specs alike.
it('resolves to the parsed profile', async () => {
await expectAsync(api.fetchProfile(91))
.toBeResolvedTo({ id: 91, city: 'Pune' });
});
it('rejects with a typed error', async () => {
await expectAsync(api.fetchProfile(-1))
.toBeRejectedWithError(RangeError, /id must be positive/);
});
it('is still pending while the request is in flight', async () => {
const p = api.fetchProfile(91);
await expectAsync(p).toBePending();
transport.resolve({ id: 91, city: 'Pune' });
await expectAsync(p).toBeResolved();
});
// .already: settled right now, no microtask yield
it('answers from cache without hitting the network', async () => {
cache.set(91, { id: 91, city: 'Pune' });
await expectAsync(api.fetchProfile(91)).already.toBeResolved();
});
// BROKEN: no await, so the spec passes whatever happens
it('looks fine and tests nothing', () => {
expectAsync(api.fetchProfile(-1)).toBeRejected();
});
Key Points
- Every expectAsync matcher returns a promise; await or return it
- toBeResolvedTo uses toEqual semantics, not identity
- toBePending and .already assert settlement state, not the value
- A hung promise shows up as a 5000ms timeout, not a matcher failure
Q15How do you write a custom matcher with jasmine.addMatchers, and what changed in the matcher factory signature in Jasmine 4?
IntermediateCustom Matchers
Answer
A custom matcher is a factory function that receives matchersUtil and returns an object with a compare method. compare gets the actual value plus whatever arguments the caller passed and must return { pass: boolean, message: string }. The message is used for both directions, so write it for the failing case of pass === false and also supply text for pass === true, because that string is what .not prints. If negation needs different logic rather than the inverse of pass, add a negativeCompare method alongside compare.
Register matchers with jasmine.addMatchers inside a beforeEach, not at file scope: Jasmine scopes custom matchers to the current suite and clears them between specs, so a top-level call in a helper only works because helpers run before the tree is built. The change to know is the signature. Older Jasmine passed two arguments, util and customEqualityTesters, and you threaded the second into util.equals(a, b, customEqualityTesters).
Jasmine 4 removed that second parameter, so the factory now takes matchersUtil alone and you call matchersUtil.equals(a, b), which already applies any registered custom equality testers. Code copied from a 2019 blog post still compiles and then silently ignores your testers, which is a favourite interview trap. In TypeScript you also need a declaration merge into the global jasmine namespace so that expect(x).toBeValidGstin() type-checks, otherwise the compiler reports that the property does not exist on Matchers. Good custom matchers pay for themselves in failure output: one line saying the GSTIN checksum was wrong beats a fifteen-line object diff.
// spec/helpers/matchers.js
beforeEach(() => {
jasmine.addMatchers({
toBeValidGstin: (matchersUtil) => ({
compare(actual) {
const ok = /^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][1-9A-Z]Z[0-9A-Z]$/.test(actual);
return {
pass: ok,
message: ok
? 'Expected ' + actual + ' not to be a valid GSTIN'
: 'Expected ' + actual + ' to be a valid 15-character GSTIN',
};
},
}),
toHaveLineItem: (matchersUtil) => ({
compare(invoice, expected) {
const pass = invoice.items.some((i) => matchersUtil.equals(i, expected));
return { pass, message: 'Expected invoice to contain ' + JSON.stringify(expected) };
},
}),
});
});
// TypeScript declaration merge
declare global {
namespace jasmine {
interface Matchers<T> {
toBeValidGstin(): boolean;
toHaveLineItem(expected: unknown): boolean;
}
}
}
Key Points
- compare returns { pass, message }; message covers the .not case too
- Jasmine 4 dropped customEqualityTesters from the factory arguments
- matchersUtil.equals already honours registered equality testers
- Register inside beforeEach; matchers are cleared between specs
Q16What do jasmine.addCustomEqualityTester and jasmine.addCustomObjectFormatter do, and when are they the right tool?
IntermediateCustom Matchers
Answer
A custom equality tester changes how Jasmine's deep equality treats a specific pair of values. Register it in a beforeEach with jasmine.addCustomEqualityTester(fn); the function receives two values and returns true, false, or undefined to defer to the built-in algorithm. Returning undefined for anything you do not care about is essential, because a tester that returns false by default breaks every unrelated comparison in the suite.
Once registered it applies to toEqual, toContain, toHaveBeenCalledWith, jasmine.objectContaining and any custom matcher that calls matchersUtil.equals, which is exactly what you want for value objects such as a Money class where two instances with the same paise count should compare equal even though they are different references. Custom object formatters solve the other half of the problem: readability of failures. jasmine.addCustomObjectFormatter(fn) takes a function that returns a string for values it recognises and undefined for everything else, and Jasmine uses that string in failure messages instead of dumping the object graph. On a Decimal or a Luxon DateTime this turns a forty-line diff into 'Expected Rs 1,180.00 to equal Rs 1,000.00'.
Both are per-spec state and both are reset after each spec, so a tester added in one file cannot leak into another, unlike a monkey patch. The judgement call interviewers probe is when not to use them. A tester that makes two structurally different objects compare equal hides real bugs, and once it is in a shared helper nobody remembers it exists. The safer pattern for one-off leniency is an asymmetric matcher at the specific position, which is visible in the spec itself.
class Money {
constructor(paise) { this.paise = paise; }
}
beforeEach(() => {
jasmine.addCustomEqualityTester((a, b) => {
if (a instanceof Money && b instanceof Money) {
return a.paise === b.paise;
}
return undefined; // defer to Jasmine for everything else
});
jasmine.addCustomObjectFormatter((value) => {
if (value instanceof Money) {
return 'Rs ' + (value.paise / 100).toFixed(2);
}
return undefined;
});
});
it('compares value objects by amount', () => {
expect(new Money(118000)).toEqual(new Money(118000));
expect([new Money(500)]).toContain(new Money(500));
const charge = jasmine.createSpy('charge');
charge(new Money(118000));
expect(charge).toHaveBeenCalledWith(new Money(118000));
});
Key Points
- Return undefined from a tester for pairs you do not handle
- Testers affect toEqual, toContain, toHaveBeenCalledWith and matchersUtil.equals
- Object formatters only change failure output, never pass/fail
- Both are registered per spec in beforeEach and reset afterwards
Q17How does spyOnProperty work, and why do you sometimes get 'is not declared configurable'?
IntermediateSpies
Answer
spyOn only replaces data properties whose value is a function. Accessors need spyOnProperty(object, 'propertyName', 'get' | 'set'), which replaces the getter or setter in the property descriptor and returns a spy you configure the usual way with and.returnValue or and.callFake. Omit the third argument and it defaults to 'get'.
Ask for an access type that does not exist and Jasmine throws 'Property x does not have access type set'. Like spyOn, it restores the original descriptor at the end of the spec. Two situations trip people up.
First, class getters live on the prototype, not on the instance, so spyOnProperty(instance, 'isAdmin', 'get') throws 'isAdmin is not declared configurable' or complains the property is not found; you spy on Object.getPrototypeOf(instance) or on TheClass.prototype instead. Second, several host objects define non-configurable properties, and navigator.onLine or window.location in some browsers cannot be redefined at all, which produces the same 'not declared configurable' error. The workaround is to redefine the descriptor yourself with Object.defineProperty(obj, 'prop', { configurable: true, get }) in a beforeEach and restore it in afterEach, though the better long-term fix in application code is to wrap the browser API in a small injectable service and spy on that.
In Angular specs the same technique covers component getters and service properties, and it composes with the read-only inputs pattern. Interviewers usually follow up with 'how would you fake navigator.onLine', and the answer they want is the descriptor dance plus the observation that a thin wrapper service makes the test trivial and the code more portable.
class Session {
get isAdmin() { return this.roles.includes('admin'); }
}
it('spies on a prototype getter', () => {
const s = new Session();
spyOnProperty(Session.prototype, 'isAdmin', 'get').and.returnValue(true);
expect(s.isAdmin).toBeTrue();
});
it('spies on a setter', () => {
const store = { _theme: 'light', set theme(v) { this._theme = v; } };
const setter = spyOnProperty(store, 'theme', 'set');
store.theme = 'dark';
expect(setter).toHaveBeenCalledWith('dark');
});
// navigator.onLine may be non-configurable: redefine, then restore
let original;
beforeEach(() => {
original = Object.getOwnPropertyDescriptor(Navigator.prototype, 'onLine');
Object.defineProperty(Navigator.prototype, 'onLine', {
configurable: true,
get: () => true,
});
});
afterEach(() => {
Object.defineProperty(Navigator.prototype, 'onLine', original);
});
it('shows the offline banner', () => {
spyOnProperty(Navigator.prototype, 'onLine', 'get').and.returnValue(false);
expect(shouldShowOfflineBanner()).toBeTrue();
});
Key Points
- spyOnProperty(obj, name, 'get' | 'set') for accessors, defaults to get
- Class getters sit on the prototype, so spy on the prototype
- 'not declared configurable' means the descriptor forbids redefinition
- Wrap browser globals in an injectable service to avoid the problem
Q18How do jasmine.setDefaultSpyStrategy, spy.withArgs and spyOnAllFunctions help you build strict test doubles?
IntermediateSpies
Answer
By default an unconfigured spy returns undefined, which means a collaborator method you forgot to stub fails silently somewhere downstream as 'Cannot read properties of undefined'. jasmine.setDefaultSpyStrategy changes that default for every spy created afterwards. Called inside a describe it applies to that suite; called in a top-level beforeEach or a helper it applies globally. The strict setup is jasmine.setDefaultSpyStrategy((and) => and.throwError('unstubbed spy call')), which converts every unplanned interaction into an immediate, clearly located failure.
Teams that adopt it usually pair it with jasmine.setDefaultSpyStrategy((and) => and.callThrough()) in a specific describe where they want real behaviour by default. spy.withArgs(...args) creates an argument-specific strategy on an existing spy, so one double can answer differently per input without a hand-written callFake full of if statements. Calls that match no withArgs clause fall back to the spy's base strategy, which is where setDefaultSpyStrategy and withArgs combine nicely: match the arguments you expect, throw on anything else. spyOnAllFunctions(object) spies every own function property in one call and returns the object so you can chain; pass true as the second argument to include non-enumerable properties. It is convenient for large legacy service objects and dangerous on anything with getters, because reading a property to check whether it is a function can trigger side effects. The interview point behind all three is the difference between a lenient double, which lets a wrong call slip through and produces a confusing failure later, and a strict double, which fails at the exact line where the code under test did something you did not plan for.
describe('OrderService with strict doubles', () => {
let pricing;
beforeAll(() => {
jasmine.setDefaultSpyStrategy((and) =>
and.throwError('unstubbed spy called')
);
});
beforeEach(() => {
pricing = jasmine.createSpyObj('Pricing', ['quote', 'audit']);
pricing.quote.withArgs('IN', 'standard').and.returnValue(4999);
pricing.quote.withArgs('IN', 'express').and.returnValue(6999);
pricing.audit.and.stub(); // explicitly allowed to do nothing
});
it('prices an express order', () => {
expect(new OrderService(pricing).total('IN', 'express')).toBe(6999);
});
it('fails loudly on an unplanned combination', () => {
expect(() => new OrderService(pricing).total('AE', 'standard'))
.toThrowError('unstubbed spy called');
});
});
// One call to double a whole legacy object
const legacy = spyOnAllFunctions(window.LegacyAnalytics);
legacy.track.and.stub();
Key Points
- setDefaultSpyStrategy turns unstubbed calls into immediate failures
- withArgs gives per-argument return values with a base fallback
- spyOnAllFunctions doubles an entire object, returns it for chaining
- Strict doubles fail at the wrong call, not three frames later
Q19Jasmine randomises spec order by default. How do you use the seed to reproduce and fix a flaky suite?
IntermediateFlaky Tests
Answer
Randomisation is on by default and Jasmine prints the seed at the end of every run, as 'Randomized with seed 54321'. That number is the entire reproduction recipe: npx jasmine --seed=54321 replays the same order locally, and in Karma you pass it through the client block or the URL. Random order exists to surface order dependence, which is the root cause of most flakiness in JavaScript suites: a module-level cache, a spy assigned directly onto a shared object instead of through spyOn, a jasmine.clock never uninstalled, an unstopped interval, a DOM node appended to document.body and never removed, or a test database row created in beforeAll and mutated by one spec.
The debugging procedure worth describing in an interview is mechanical. Reproduce with the seed, then confirm order dependence by running the failing spec alone with --filter='exact spec name'; if it passes alone, another spec is the culprit. Bisect by running halves of the file list, or use --random=false to get declaration order and see whether the failure disappears.
Then look for the shared thing. Along the way, three env options earn their place: forbidDuplicateNames rejects two specs with the same full name, which otherwise makes --filter ambiguous and reports confusing; stopOnSpecFailure, exposed as --fail-fast, stops the run at the first failure so CI logs stay readable; and stopSpecOnExpectationFailure aborts the current spec body at its first failed expectation, which stops one bad assumption from producing ten cascading errors. What interviewers do not want to hear is 'we set random to false', because that hides the bug rather than fixing it.
// Run output ends with: Randomized with seed 54321
// Reproduce exactly
// npx jasmine --seed=54321
// Is it order dependent?
// npx jasmine --filter="Cart applies GST"
// npx jasmine --random=false
// Stop at the first failure while bisecting
// npx jasmine --seed=54321 --fail-fast
// spec/support/jasmine.json
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.js"],
"env": {
"random": true,
"forbidDuplicateNames": true,
"stopSpecOnExpectationFailure": false,
"stopOnSpecFailure": false
}
}
// Karma equivalent (karma.conf.js)
// client: { jasmine: { random: true, seed: '54321', failFast: false } }
// The usual culprit: a spy assigned instead of installed
// window.analytics.track = jasmine.createSpy('track'); // leaks forever
// spyOn(window.analytics, 'track'); // auto-restored
Key Points
- The printed seed reproduces the exact order via --seed
- Passes alone but fails in the run means order dependence
- forbidDuplicateNames keeps --filter and reports unambiguous
- Disabling random hides the bug; find the shared state instead
Q20How do you write a custom Jasmine reporter, and how do you get JUnit XML out of a suite for CI?
IntermediateReporting
Answer
A reporter is a plain object with any subset of six methods: jasmineStarted(suiteInfo) with totalSpecsDefined and the order, suiteStarted(result), specStarted(result), specDone(result), suiteDone(result) and jasmineDone(result) with overallStatus, incompleteReason and the run duration. Register it with jasmine.getEnv().addReporter(reporter) from a helpers file so it is installed before the tree is executed. Every callback may return a promise and Jasmine awaits it, which is how reporters that upload results to a dashboard avoid dropping the last batch. jasmine.getEnv().clearReporters() removes the built-in console reporter when you want to replace it entirely rather than add to it.
The result objects are where the value is: specDone gives you fullName, status ('passed', 'failed', 'pending', 'excluded'), duration in milliseconds, failedExpectations with message and stack, and a properties bag you can populate from inside a spec. A twenty-line reporter that prints every spec slower than 500ms is the cheapest performance tool a suite can have. For CI you rarely write your own. jasmine-reporters provides JUnitXmlReporter, which writes the junitresults-*.xml files that Jenkins, GitLab CI and Azure DevOps parse into a test tab with per-spec history, and jasmine-spec-reporter gives readable nested console output with timing.
In Karma the equivalents are karma-junit-reporter and karma-coverage. One caveat worth raising: when you run specs in parallel with the jasmine CLI, each worker reports separately, so a reporter that keeps state across specs has to be built for that model or it will produce partial files.
// spec/helpers/reporters.js
class SlowSpecReporter {
constructor(thresholdMs = 500) {
this.thresholdMs = thresholdMs;
this.slow = [];
}
specDone(result) {
if (result.status === 'passed' && result.duration > this.thresholdMs) {
this.slow.push(result.duration + 'ms ' + result.fullName);
}
if (result.status === 'failed') {
console.log('FAIL ' + result.fullName);
result.failedExpectations.forEach((f) => console.log(' ' + f.message));
}
}
jasmineDone(result) {
console.log('overall: ' + result.overallStatus);
this.slow.sort().reverse().slice(0, 10).forEach((l) => console.log(l));
}
}
const { JUnitXmlReporter } = require('jasmine-reporters');
jasmine.getEnv().addReporter(new SlowSpecReporter(500));
jasmine.getEnv().addReporter(
new JUnitXmlReporter({
savePath: 'reports/junit',
consolidateAll: false,
})
);
Key Points
- Six callbacks: jasmineStarted, suiteStarted/Done, specStarted/Done, jasmineDone
- Reporter callbacks may return promises and Jasmine awaits them
- specDone.duration and failedExpectations power slow-spec reports
- jasmine-reporters JUnitXmlReporter feeds Jenkins and GitLab test tabs
Q21How do you run Jasmine specs in a real browser with jasmine-browser-runner, and what goes in jasmine-browser.json?
IntermediateTooling
Answer
jasmine-browser-runner is the officially maintained replacement for Karma when you need real browser execution without Angular's build pipeline. Install it, run npx jasmine-browser-runner init, and you get spec/support/jasmine-browser.json. The keys that matter are srcDir and srcFiles for the application code, specDir and specFiles for the specs, helpers for files loaded first, browser for the target, and env for the same runtime options as the Node config, including random, seed and stopOnSpecFailure.
Two commands drive it: serve starts a server on http://localhost:8888 so you can open the suite in any browser and use devtools, and runSpecs launches the configured browser through WebDriver, runs everything once and exits with a non-zero code on failure, which is what CI calls. Set browser to 'headlessChrome' or 'headlessFirefox' for pipelines and 'chrome' locally when you want to see the page. Module support is the part interviewers probe.
Setting esmFilenameExtension to '.mjs' makes matching files load as ES modules with type="module", enableTopLevelAwait switches the spec wrapper so top-level await parses, and importMap lets bare specifiers such as 'lodash-es' resolve from node_modules without any bundler at all. That combination is why teams with plain ESM libraries can drop Karma and webpack entirely. Practical CI notes: in a container you need a real Chrome or Firefox binary plus the matching driver, headless Chrome usually needs --no-sandbox, and because runSpecs proxies node_modules only for the paths you list, a missing entry surfaces as a 404 in the browser console rather than a Jasmine error.
// spec/support/jasmine-browser.json
{
"srcDir": "src",
"srcFiles": [],
"specDir": "spec",
"specFiles": ["**/*[sS]pec.?(m)js"],
"helpers": ["helpers/**/*.?(m)js"],
"esmFilenameExtension": ".mjs",
"enableTopLevelAwait": true,
"importMap": {
"moduleRootDir": "node_modules",
"imports": { "lodash-es": "/__node_modules__/lodash-es/lodash.js" }
},
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
},
"browser": { "name": "headlessChrome" }
}
// package.json
// "scripts": {
// "test:browser": "jasmine-browser-runner runSpecs",
// "test:watch": "jasmine-browser-runner serve"
// }
Key Points
- init scaffolds spec/support/jasmine-browser.json
- serve for interactive debugging, runSpecs for CI exit codes
- esmFilenameExtension plus importMap runs real ESM without a bundler
- headlessChrome needs a browser binary and driver inside the CI image
Q22What does karma-jasmine actually do in an Angular project, and which karma.conf.js settings break CI most often?
IntermediateAngular Integration
Answer
Karma is a browser launcher and file server; jasmine-core supplies the framework; karma-jasmine is the adapter that boots Jasmine inside the Karma iframe and forwards Jasmine's reporter events back to the Karma server over a socket. In an Angular workspace the builder adds its own framework entry so the compiled test bundle is served, and karma-jasmine-html-reporter renders the clickable page at localhost:9876/debug.html. The client block is where Jasmine configuration lives: client.jasmine takes random, seed, stopOnSpecFailure and failFast, and clearContext false keeps the HTML reporter visible after the run.
Four settings cause most CI pain. First, browsers: ChromeHeadless will not start inside Docker without a custom launcher adding --no-sandbox and --disable-gpu, and the error is a bare 'Chrome failed to start'. Second, CHROME_BIN: without puppeteer or an installed Chrome, Karma cannot find a binary at all, which is why many pipelines set process.env.CHROME_BIN = require('puppeteer').executablePath() at the top of karma.conf.js.
Third, browserNoActivityTimeout, which defaults to 30 seconds and produces 'Disconnected, because no message in 30000 ms' on slow shared runners or when one spec blocks the event loop; raising it hides a real hang, so investigate before you edit it. Fourth, singleRun and the --watch=false flag, because a pipeline that leaves the watcher on simply never finishes. The strategic point to make in 2026 is that Karma is deprecated and Angular has moved to a Vitest-based unit test builder, so new work should target that path while existing karma.conf.js files are maintained only until migration.
// karma.conf.js
process.env.CHROME_BIN = require('puppeteer').executablePath();
module.exports = function (config) {
config.set({
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
clearContext: false,
jasmine: { random: true, seed: '', stopOnSpecFailure: false },
},
reporters: ['progress', 'kjhtml'],
browsers: ['ChromeHeadlessCI'],
customLaunchers: {
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
},
},
browserNoActivityTimeout: 60000,
restartOnFileChange: true,
});
};
// CI command
// ng test --watch=false --browsers=ChromeHeadlessCI --code-coverage
Key Points
- karma-jasmine only adapts Jasmine to Karma's socket protocol
- client.jasmine carries random, seed and stopOnSpecFailure
- Docker needs --no-sandbox and --disable-dev-shm-usage
- 'no message in 30000 ms' means a hang, not a too-small timeout
Q23How do you wire a jasmine.SpyObj into TestBed for an Angular component spec?
IntermediateAngular Integration
Answer
The standard shape is: build the double with jasmine.createSpyObj typed as jasmine.SpyObj<T>, configure its return values, then register it in TestBed.configureTestingModule with { provide: RealService, useValue: theDouble }. For a standalone component you list the component itself in imports rather than declarations. Call compileComponents when templates are not inlined, create the fixture with TestBed.createComponent, and run fixture.detectChanges() to trigger the first change detection pass, which is what actually invokes ngOnInit.
Retrieve anything else you need with TestBed.inject(Token), which is the current API; the older TestBed.get is gone. Three failure modes come up constantly. NullInjectorError: No provider for OrderService means you forgot the providers entry or you provided the class instead of the double.
A spy that returns undefined where the component expects an Observable produces 'Cannot read properties of undefined (reading pipe)' inside the template, which is why you configure returnValue before creating the component, not after. And a method you did not list in createSpyObj is simply absent, so you get 'orders.refresh is not a function' rather than a helpful assertion, which is exactly the reason to type the double as jasmine.SpyObj<OrderService>: adding a method to the real service then breaks compilation in the spec instead of at runtime. On teardown, Angular destroys fixtures after each spec by default, so you rarely need manual cleanup, but a component that starts an interval or a subscription still needs the destroy path exercised. Assert against the rendered DOM through fixture.nativeElement with stable data-testid hooks rather than CSS classes, since class names change with every design refresh.
describe('OrderListComponent', () => {
let fixture: ComponentFixture<OrderListComponent>;
let orders: jasmine.SpyObj<OrderService>;
beforeEach(async () => {
orders = jasmine.createSpyObj<OrderService>('OrderService', ['list', 'cancel']);
orders.list.and.returnValue(of([{ id: 'ORD-1', total: 4999 }]));
orders.cancel.and.returnValue(of(void 0));
await TestBed.configureTestingModule({
imports: [OrderListComponent],
providers: [{ provide: OrderService, useValue: orders }],
}).compileComponents();
fixture = TestBed.createComponent(OrderListComponent);
fixture.detectChanges(); // runs ngOnInit
});
it('renders one row per order', () => {
const rows = fixture.nativeElement.querySelectorAll('[data-testid="order-row"]');
expect(rows.length).toBe(1);
expect(orders.list).toHaveBeenCalledTimes(1);
});
it('cancels the selected order', () => {
fixture.nativeElement.querySelector('[data-testid="cancel-ORD-1"]').click();
fixture.detectChanges();
expect(orders.cancel).toHaveBeenCalledOnceWith('ORD-1');
});
});
Key Points
- jasmine.SpyObj<T> keeps the double in sync with the real interface
- Configure return values before TestBed.createComponent
- TestBed.inject replaces the removed TestBed.get
- NullInjectorError means the provider override is missing or misspelled
Q24How do you test an Angular HTTP service with HttpTestingController, and why does verify() belong in afterEach?
IntermediateAngular Integration
Answer
Provide provideHttpClient() together with provideHttpClientTesting(), then inject HttpTestingController. The testing backend queues requests instead of sending them, and you drive each one manually. The order matters: HttpClient observables are cold, so nothing is issued until something subscribes.
Subscribe first, then call httpMock.expectOne(url) or expectOne(req => req.method === 'POST' && req.url.endsWith('/orders')), assert on req.request for method, headers, body and params, and finally call req.flush(responseBody) to deliver it. flush accepts a second options object, so req.flush('boom', { status: 500, statusText: 'Server Error' }) exercises the error path, and req.error(new ProgressEvent('error')) simulates a network failure rather than an HTTP status. expectNone asserts nothing was sent, which is how you prove a cache or a debounce worked, and match(predicate) returns an array when several requests are legitimately in flight. verify() asserts there are no outstanding requests and throws 'Expected no open requests, found 1' listing the URL. Putting it in afterEach is the discipline that catches an entire class of bug: a service that fires a duplicate call, a retry you did not intend, or a component that requests data twice because both ngOnInit and a value setter trigger it. Without verify those extra calls are invisible and the spec still passes. The other message you will see is 'Expected one matching request for criteria ..., found none', which almost always means you asserted before subscribing or the URL includes query parameters you did not account for, since expectOne matches the full URL string unless you pass a predicate.
describe('ProfileService', () => {
let service: ProfileService;
let httpMock: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ProfileService, provideHttpClient(), provideHttpClientTesting()],
});
service = TestBed.inject(ProfileService);
httpMock = TestBed.inject(HttpTestingController);
});
afterEach(() => httpMock.verify());
it('sends the bearer token and maps the payload', () => {
let result;
service.fetchProfile(91).subscribe((r) => (result = r));
const req = httpMock.expectOne('/api/d2/profile/91');
expect(req.request.method).toBe('GET');
expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
req.flush({ id: 91, city: 'Pune' });
expect(result).toEqual({ id: 91, city: 'Pune' });
});
it('turns a 500 into a friendly message', () => {
let err;
service.fetchProfile(91).subscribe({ error: (e) => (err = e) });
httpMock.expectOne('/api/d2/profile/91')
.flush('boom', { status: 500, statusText: 'Server Error' });
expect(err.message).toBe('Could not load profile, please retry');
});
it('serves the second call from cache', () => {
service.fetchProfile(91).subscribe();
httpMock.expectOne('/api/d2/profile/91').flush({ id: 91 });
service.fetchProfile(91).subscribe();
httpMock.expectNone('/api/d2/profile/91');
});
});
Key Points
- Subscribe before expectOne; HttpClient observables are cold
- flush(body, { status, statusText }) drives the error path
- expectNone proves a cache or debounce suppressed a call
- afterEach verify() catches duplicate and unintended requests
Q25When do you use fakeAsync with tick and flush instead of jasmine.clock or waitForAsync?
IntermediateAsync Testing
Answer
jasmine.clock only patches the timer globals. Angular code runs inside Zone.js, which tracks both macrotasks and microtasks, so the framework provides its own helpers. fakeAsync(fn) wraps the spec body in a fake async zone where timers and promises are queued rather than executed. Inside it, tick(ms) advances virtual time and also drains the microtask queue, tick() with no argument drains microtasks only, flushMicrotasks() does the same explicitly, and flush() runs every pending macrotask until the queue is empty and returns the virtual milliseconds elapsed, which is what you use when you do not want to hard-code a debounce interval.
If anything is still queued when the body ends, the spec fails with 'Error: 1 timer(s) still in the queue' or '1 periodic timer(s) still in the queue'; for setInterval you either clear it in the component's destroy path or call discardPeriodicTasks(). fakeAsync cannot handle a real XHR or anything that resolves outside the zone, and attempting it produces an error about a pending macrotask that never completes. waitForAsync is the other tool: it runs the body in an async test zone and pairs with await fixture.whenStable(), which resolves once the zone's queue is empty. Use it when real asynchrony is unavoidable, for example a component under test that awaits a genuine promise from a library you cannot fake. Two additional notes for 2026 interviews. fakeAsync depends on Zone.js being loaded, so in a zoneless configuration the idiomatic pattern is await fixture.whenStable() after triggering the change. And jasmine.clock still has its place in plain TypeScript service specs with no Angular involvement, where Zone.js is not in the picture at all.
it('debounces the search box', fakeAsync(() => {
const fixture = TestBed.createComponent(SearchComponent);
const api = TestBed.inject(SearchApi) as jasmine.SpyObj<SearchApi>;
fixture.detectChanges();
typeInto(fixture, 'shi');
typeInto(fixture, 'shirt');
tick(299);
expect(api.query).not.toHaveBeenCalled();
tick(1);
expect(api.query).toHaveBeenCalledOnceWith('shirt');
fixture.destroy();
}));
it('flushes without hard-coding the delay', fakeAsync(() => {
const svc = TestBed.inject(RetryService);
let done = false;
svc.withRetry().then(() => (done = true));
const elapsed = flush(); // drains every pending macrotask
expect(done).toBeTrue();
expect(elapsed).toBeGreaterThan(0);
}));
it('cleans up a polling interval', fakeAsync(() => {
const fixture = TestBed.createComponent(LivePriceComponent);
fixture.detectChanges();
tick(5000);
discardPeriodicTasks(); // otherwise: 1 periodic timer(s) still in the queue
}));
// Real asynchrony that cannot be faked
it('loads the config', waitForAsync(async () => {
const fixture = TestBed.createComponent(AppShellComponent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.componentInstance.ready).toBeTrue();
}));
Key Points
- tick advances virtual time and drains microtasks; flush empties the queue
- '1 timer(s) still in the queue' means uncleared setTimeout or setInterval
- discardPeriodicTasks for intervals you deliberately leave running
- jasmine.clock knows nothing about Zone.js; fakeAsync needs Zone.js
Q26What are the practical ways to assert on RxJS observables inside a Jasmine spec?
IntermediateAsync Testing
Answer
Pick the technique that matches the stream. For a synchronous observable such as of(...) or a BehaviorSubject, just subscribe, capture into a local variable and assert after the subscribe call; nothing async is involved and adding async only slows the suite down. For a stream that emits once and completes, await firstValueFrom(source) or lastValueFrom(source) in an async spec, which is far cleaner than the older toPromise and gives you a proper rejection when the stream completes empty, throwing EmptyError.
For multiple emissions, push each value into an array and assert the array once the stream completes, ideally driving completion yourself so the spec cannot hang. The done callback still has a place for event-style streams, but every branch must call done, and a stream that errors without an error handler will time out at 5000ms with no indication of which subscription hung. For time-based operators, debounceTime, throttleTime, interval, retryWhen with a delay, you have two good options: fakeAsync plus tick in Angular, or RxJS's own TestScheduler for pure logic.
TestScheduler.run gives you cold and hot helpers with marble strings and an expectObservable assertion that compares emissions and their virtual frames, and inside run() all time-based operators use the virtual scheduler automatically. The last thing interviewers look for is teardown discipline: capture the Subscription and unsubscribe in afterEach, or use takeUntil with a subject, because a live subscription from spec one can fire during spec three and produce a failure that looks random.
// 1. Synchronous stream: no async needed
it('exposes the current cart total', () => {
let total;
cart.total$.subscribe((t) => (total = t));
cart.add({ sku: 'A', price: 4999 });
expect(total).toBe(4999);
});
// 2. Single emission then complete
it('resolves the profile', async () => {
const profile = await firstValueFrom(service.fetchProfile(91));
expect(profile.city).toBe('Pune');
});
// 3. Several emissions, assert the sequence
it('emits loading then loaded', async () => {
const seen = [];
const sub = service.state$.subscribe((s) => seen.push(s));
await service.load();
sub.unsubscribe();
expect(seen).toEqual(['idle', 'loading', 'loaded']);
});
// 4. Time-based operators with the RxJS TestScheduler
it('debounces keystrokes', () => {
const scheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
scheduler.run(({ cold, expectObservable }) => {
const source = cold('a 50ms b 400ms c|');
const result = source.pipe(debounceTime(300));
expectObservable(result).toBe('351ms b 401ms (c|)');
});
});
Key Points
- firstValueFrom throws EmptyError when the stream completes with nothing
- Collect emissions into an array for multi-value assertions
- TestScheduler.run virtualises time for debounce, throttle and interval
- Unsubscribe in afterEach or a stale subscription fails a later spec
Q27What is Jasmine's per-spec userContext, and why does using an arrow function break this.someValue?
IntermediateExecution Model
Answer
Jasmine creates a fresh context object for every spec and calls the beforeEach, the spec body and the afterEach with that object as this. It inherits prototypally from the enclosing suite's context, so a property set in an outer beforeEach is visible in an inner one, and anything set during the spec is discarded when the spec ends. That is the mechanism older Jasmine suites used to share fixtures without module-level variables, and it is genuinely leak-proof: because the object is recreated per spec, there is no way for spec one to hand state to spec two.
It only works with function expressions. An arrow function has no own this, so it captures whatever this was where the arrow was written, which in a module is undefined under strict mode, and you get 'Cannot set properties of undefined (setting cart)' or, worse in a non-strict script, an assignment onto the global object that then does leak between specs. That is why the two styles must not be mixed inside a describe.
Modern TypeScript codebases usually skip userContext entirely and use let declared in the describe body with assignment in beforeEach, because the compiler can type a let and cannot usefully type this in a Jasmine callback without an explicit this parameter annotation, and noImplicitThis flags it. Both patterns are acceptable; what is not acceptable is a beforeEach written as an arrow assigning to this while the spec reads this, since that silently reads a different object. Expect this question when the interviewer has a large legacy suite and wants to know whether you can read it.
// userContext style: function expressions everywhere
describe('Cart', function () {
beforeEach(function () {
this.cart = new Cart(); // fresh object every spec
});
it('starts empty', function () {
expect(this.cart.total()).toBe(0);
});
describe('with an item', function () {
beforeEach(function () {
this.cart.add({ sku: 'A', price: 4999 }); // inherits outer context
});
it('totals the item', function () {
expect(this.cart.total()).toBe(4999);
});
});
});
// BROKEN: arrow in beforeEach, function in the spec
describe('Cart', () => {
beforeEach(() => {
this.cart = new Cart(); // `this` is not the spec context
});
it('fails', function () {
expect(this.cart).toBeUndefined();
});
});
// Preferred in TypeScript
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); });
it('starts empty', () => expect(cart.total()).toBe(0));
});
Key Points
- Every spec gets a fresh this that inherits from the suite context
- Arrow functions capture lexical this and lose the context entirely
- Never mix arrow and function styles inside one describe
- let plus beforeEach is the TypeScript-friendly equivalent
Q28How does Jasmine handle uncaught exceptions and unhandled promise rejections, and why do they show up on the wrong spec?
IntermediateProduction Failure Modes
Answer
Jasmine installs global error handlers so that failures escaping the normal call stack are not lost: process.on('uncaughtException') and process.on('unhandledRejection') under Node, window.onerror and the unhandledrejection event in the browser. When one fires, Jasmine attributes it to whichever spec is currently executing and adds it to that spec's failedExpectations with a message such as 'Unhandled promise rejection: Error: socket hang up'. If no spec is running, it becomes a suite-level or top-level failure and you see wording like 'An error was thrown in afterAll' even when your afterAll is empty, which confuses people every time.
The important consequence is attribution. Asynchronous work started in spec A but not awaited will very often land during spec B, so the report blames a spec that is entirely innocent, and because spec order is randomised the blamed spec changes between runs. That is one of the two or three most common sources of unexplained CI flakiness in JavaScript suites.
The debugging move is to look at the stack trace rather than the spec name, because the stack points at the real originator. The fixes are all about not leaving work in flight: await or explicitly .catch() every promise you start, cancel timers and intervals in afterEach, unsubscribe from observables, abort in-flight fetches with an AbortController, and make sure a spy configured with and.rejectWith is actually awaited somewhere, since an un-awaited rejected promise from a spy is a classic source of this exact failure. Node's unhandled rejection default of crashing the process also means a bad rejection can kill the runner outright rather than fail a spec.
// Leaves a rejected promise in flight: fails a LATER, unrelated spec
it('fires and forgets', () => {
api.sync(); // returns a promise that rejects 20ms later
expect(true).toBeTrue();
});
// Fix 1: await it and assert the rejection
it('reports a sync failure', async () => {
await expectAsync(api.sync()).toBeRejectedWithError(/socket hang up/);
});
// Fix 2: if the call really is fire-and-forget, own the rejection
it('fires and forgets safely', () => {
const pending = api.sync().catch(() => undefined);
expect(pending).toBeInstanceOf(Promise);
});
// Fix 3: cancel anything still running when the spec ends
let controller;
beforeEach(() => { controller = new AbortController(); });
afterEach(() => { controller.abort(); });
// Reported as, for example:
// Unhandled promise rejection: Error: socket hang up
// (attributed to whichever spec happened to be running)
Key Points
- Jasmine hooks uncaughtException, unhandledRejection, onerror
- The failure lands on the running spec, not the spec that caused it
- Random order makes the blamed spec change between CI runs
- Read the stack trace, then await, catch, cancel or unsubscribe
Q29Why does spyOn fail on ES module exports, and what are the workable alternatives?
AdvancedES Modules
Answer
spyOn works by reassigning a property on an object. A module namespace object is not a normal object: the specification makes its bindings read-only and non-configurable, so spyOn(namespace, 'formatInr') throws 'formatInr is not declared writable or has no setter', and a direct assignment throws 'Cannot assign to read only property'. Transpiled code used to hide this.
Older TypeScript and Babel CommonJS output assigned plain properties onto exports, so import * as utils followed by spyOn happened to work, and thousands of specs were written that way. Modern emitters, including current TypeScript and esbuild, define exports with getters via Object.defineProperty, which restores the read-only behaviour and breaks those specs the day a project switches bundler or moves to native ESM. There is no flag that makes it work again, so the answers are structural.
The cleanest is dependency injection: pass the collaborator into the function or the class constructor, and the spec supplies a spy with no module trickery at all, which is why Angular services almost never hit this problem. Second, export a namespace object deliberately, so the module exports const clock = { now: () => Date.now() } and consumers call clock.now(); the object is yours and spyOn(clock, 'now') is legal. Third, replace the module at load time rather than at runtime: jasmine-browser-runner's importMap can point a bare specifier at a stub file, and under Node you can register a module customization hook that resolves a module to a test double. Fourth, if a project genuinely depends on module mocking everywhere, that is a real argument for Vitest, whose vi.mock intercepts the module graph.
// util/currency.mjs
export function formatInr(paise) {
return 'Rs ' + (paise / 100).toFixed(2);
}
// FAILS under native ESM and modern transpiler output
import * as currency from '../util/currency.mjs';
spyOn(currency, 'formatInr').and.returnValue('Rs 0.00');
// -> formatInr is not declared writable or has no setter
// Option 1: inject the collaborator
export function renderInvoice(invoice, { format = formatInr } = {}) {
return invoice.items.map((i) => format(i.paise)).join('\n');
}
it('formats each line', () => {
const format = jasmine.createSpy('format').and.returnValue('Rs 0.00');
expect(renderInvoice(invoice, { format })).toContain('Rs 0.00');
expect(format).toHaveBeenCalledTimes(invoice.items.length);
});
// Option 2: export an object you own
export const clock = { now: () => Date.now() };
it('stamps the invoice', () => {
spyOn(clock, 'now').and.returnValue(1775000000000);
expect(createInvoice().createdAt).toBe(1775000000000);
});
// Option 3: swap the module in jasmine-browser.json
// "importMap": { "imports": { "./util/currency.mjs": "/spec/stubs/currency.mjs" } }
Key Points
- ESM namespace bindings are read-only and non-configurable by spec
- Old CJS transpiler output made spyOn work only by accident
- Inject collaborators or export a real object you can spy on
- importMap or a Node loader hook swaps modules at load time
Q30What actually happens when you run the Jasmine CLI with --parallel, and what breaks?
AdvancedConcurrency
Answer
Jasmine 5's Node CLI accepts --parallel=N. A controlling process forks N worker processes; each worker loads the config, the requires and the helpers independently, then the controller hands out whole spec files. The unit of parallelism is the file, so one enormous spec file gets no speedup at all and the right first step is usually splitting it.
Because workers are separate processes, there is no shared memory: module-level caches, spies, custom matchers, the fake clock and any singleton all exist once per worker. That isolation is the benefit and the trap. Anything outside the process is shared and will collide.
Two workers binding the same port produce EADDRINUSE, two workers writing the same temp file or snapshot directory race, and two workers hitting one test database truncate each other's rows halfway through a spec. The fixes are to derive the resource name from the worker, for example a port offset or a schema name keyed on process.pid, or to move that resource out of the specs entirely. beforeAll is per worker per file, not once per run, so expensive one-time setup has to live in the npm script that wraps the run rather than in a spec file. Reporters are the other constraint: a reporter that accumulates state across specs receives only the slice its worker saw, so it must be written for parallel execution or run only in serial mode.
Randomisation and seeds apply within a worker, so a parallel run is not byte-for-byte reproducible from a single seed. The practical advice for an interview is to enable it for pure unit specs and keep integration specs serial.
// package.json
// "scripts": {
// "test": "jasmine --parallel=4",
// "test:integration": "jasmine --config=spec/support/integration.json"
// }
// Make every external resource worker-specific
const workerId = process.pid;
const PORT = 4000 + (workerId % 500);
const SCHEMA = 'test_' + workerId;
beforeAll(async () => {
// runs once PER WORKER PER FILE, not once per run
await db.query('CREATE SCHEMA IF NOT EXISTS ' + SCHEMA);
server = await app.listen(PORT);
});
afterAll(async () => {
await server.close();
await db.query('DROP SCHEMA IF EXISTS ' + SCHEMA + ' CASCADE');
});
// Symptoms of shared state under --parallel:
// Error: listen EADDRINUSE: address already in use :::4000
// ENOENT: no such file or directory, open 'tmp/fixture.json'
// rows disappearing mid-spec because another worker truncated the table
Key Points
- Workers are separate processes; the unit of work is a spec file
- beforeAll runs per worker per file, never once for the whole run
- Ports, temp files and test databases must be keyed per worker
- Stateful reporters and single-seed reproducibility both suffer
Q31A browser Jasmine suite runs out of memory after a few thousand specs. How do you find and fix the leak?
AdvancedMemory
Answer
The signature is a run that passes at spec 1500 and dies at 3000 with 'Aborted (OOM)' or a browser disconnect, and it is almost always retention across specs rather than one huge allocation. Start by measuring, not guessing: add a reporter that logs performance.memory.usedJSHeapSize or process.memoryUsage().heapUsed in specDone every hundred specs, and look for a straight upward line rather than a sawtooth. If it climbs, take two heap snapshots several hundred specs apart in Chrome devtools and compare by retained size; the answer is usually detached DOM nodes or a growing array.
Five causes cover most real cases. Fixtures appended to document.body and never removed, which are retained through their event listeners even after the spec ends. Angular fixtures created outside TestBed or components whose destroy path is never exercised, so subscriptions and intervals stay live. jasmine.clock installed without uninstall, leaving queued callbacks holding closures over whole component trees.
Spies stored on a module-level or window-level object, since the calls history keeps every argument object alive for the lifetime of that spy, and saveArgumentsByValue makes it worse by cloning large payloads. And custom reporters that push every specDone result, including failure stacks, into an array for the whole run. Fixes follow directly: remove fixtures in afterEach, destroy fixtures, uninstall the clock, prefer spyOn so restoration is automatic, and cap what reporters retain. If the suite is simply large, run it as several shards or with the parallel CLI so each worker starts from a clean heap, and in Karma raise memory only after the leak is understood.
// spec/helpers/memory-reporter.js
let n = 0;
jasmine.getEnv().addReporter({
specDone() {
if (++n % 100 === 0 && performance.memory) {
const mb = (performance.memory.usedJSHeapSize / 1048576).toFixed(1);
console.log('after ' + n + ' specs: ' + mb + ' MB');
}
},
});
// Leak: fixture never removed, listener keeps the tree alive
beforeEach(() => {
host = document.createElement('div');
document.body.appendChild(host);
host.addEventListener('click', handler);
});
// Fix
afterEach(() => {
host.removeEventListener('click', handler);
host.remove();
host = null;
});
// Leak: clock left installed, queued callbacks retain closures
afterEach(() => jasmine.clock().uninstall());
// Leak: module-level spy holding every argument for the whole run
// window.analytics.track = jasmine.createSpy('track'); // never restored
beforeEach(() => spyOn(window.analytics, 'track')); // restored per spec
Key Points
- Measure heap in a specDone reporter before you theorise
- Detached DOM fixtures and undestroyed component fixtures dominate
- An unrestored spy retains every argument object it ever received
- Sharding or parallel workers reset the heap between chunks
Q32How would you migrate an Angular Karma plus Jasmine suite to the Vitest-based unit test builder without freezing feature work?
AdvancedMigration
Answer
Karma is deprecated and Angular now ships the @angular/build:unit-test builder with Vitest as the runner, so this is a live project at most Angular shops, including large Indian service teams maintaining client codebases. Do it incrementally. First, understand what actually changes.
TestBed, ComponentFixture, HttpTestingController, fakeAsync and tick are Angular APIs and carry over unchanged, which is usually the bulk of a component spec. describe, it, beforeEach and expect exist in both. What changes is the Jasmine-specific surface: spyOn(obj,'m').and.returnValue(v) becomes vi.spyOn(obj,'m').mockReturnValue(v), and.callFake becomes mockImplementation, and.callThrough is the default so it is simply deleted, jasmine.createSpy becomes vi.fn, jasmine.createSpyObj has no direct equivalent and needs a small helper, jasmine.clock().install and tick become vi.useFakeTimers and vi.advanceTimersByTime, jasmine.any and jasmine.objectContaining become expect.any and expect.objectContaining, custom matchers move from jasmine.addMatchers to expect.extend, and fdescribe and fit become describe.only and it.only. Second, sequence the work: switch the builder in angular.json on a branch, remove "jasmine" from the types array in tsconfig.spec.json so the two global typings do not collide, get one small spec file green, then convert file by file behind a codemod for the mechanical rewrites while both builders stay runnable. Third, expect real differences rather than a pure rename: module mocking becomes possible with vi.mock, which changes how teams test collaborators, and the default environment is jsdom rather than a real browser, so anything depending on real layout or genuine browser APIs needs the browser mode or a rewrite.
// angular.json
// "test": {
// "builder": "@angular/build:unit-test",
// "options": { "tsConfig": "tsconfig.spec.json", "runner": "vitest" }
// }
// tsconfig.spec.json: drop the Jasmine globals so typings do not clash
// "compilerOptions": { "types": ["vitest/globals"] }
// Jasmine -> Vitest
// spyOn(o, 'm').and.returnValue(v) -> vi.spyOn(o, 'm').mockReturnValue(v)
// spyOn(o, 'm').and.callFake(fn) -> vi.spyOn(o, 'm').mockImplementation(fn)
// spyOn(o, 'm').and.callThrough() -> vi.spyOn(o, 'm')
// jasmine.createSpy('name') -> vi.fn()
// jasmine.clock().install() -> vi.useFakeTimers()
// jasmine.clock().tick(300) -> vi.advanceTimersByTime(300)
// jasmine.any(Number) -> expect.any(Number)
// jasmine.objectContaining({...}) -> expect.objectContaining({...})
// jasmine.addMatchers({...}) -> expect.extend({...})
// fit / fdescribe -> it.only / describe.only
// createSpyObj has no direct equivalent; a five-line helper covers it
export function spyObj(names) {
return Object.fromEntries(names.map((n) => [n, vi.fn()]));
}
Key Points
- TestBed, fakeAsync and HttpTestingController survive the move untouched
- Only the Jasmine spy, clock and matcher surface needs rewriting
- Remove "jasmine" from tsconfig.spec.json types to avoid global clashes
- jsdom instead of a real browser is the change that surprises teams
Q33How do you write an asynchronous custom matcher with jasmine.addAsyncMatchers?
AdvancedCustom Matchers
Answer
jasmine.addAsyncMatchers registers matchers that are only reachable through expectAsync. The factory looks the same as a synchronous one, but compare is an async function and returns a promise of { pass, message }. Register in a beforeEach, exactly like jasmine.addMatchers, and remember the matchers are cleared between specs.
Calling an async matcher on plain expect throws that the matcher does not exist, which is the first mistake people make, and forgetting the await in front of expectAsync means the matcher resolves after the spec has already been reported, so a genuine failure either disappears or lands on an unrelated spec. Where this earns its keep is polling and settlement assertions that would otherwise be copy-pasted into twenty specs: a queue that should drain within a deadline, a websocket that should reach the open state, a background job that should reach a terminal status. Writing that once as toDrainWithin(ms) makes the specs read like requirements and, more importantly, gives one place to put the timing and the failure message rather than twenty slightly different retry loops.
Two design points matter for production use. Give the matcher its own internal deadline that is comfortably under jasmine.DEFAULT_TIMEOUT_INTERVAL, so a failure reports your message instead of the generic 5000ms timeout, which tells the reader nothing. And build the message from what you actually observed, including the last polled value, because 'Expected the queue to drain within 2000ms, still had 7 jobs' is a bug report while 'Expected false to be true' is not. TypeScript users add the signature to the jasmine.AsyncMatchers interface via declaration merging.
// spec/helpers/async-matchers.js
beforeEach(() => {
jasmine.addAsyncMatchers({
toDrainWithin: () => ({
async compare(queue, budgetMs) {
const deadline = Date.now() + budgetMs;
let depth = await queue.depth();
while (depth > 0 && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 25));
depth = await queue.depth();
}
return {
pass: depth === 0,
message:
depth === 0
? 'Expected the queue not to drain within ' + budgetMs + 'ms'
: 'Expected the queue to drain within ' + budgetMs +
'ms, still had ' + depth + ' jobs',
};
},
}),
});
});
it('drains the notification queue', async () => {
await enqueueBatch(50);
await expectAsync(notificationQueue).toDrainWithin(2000);
});
// TypeScript
declare global {
namespace jasmine {
interface AsyncMatchers<T, U> {
toDrainWithin(budgetMs: number): Promise<void>;
}
}
}
Key Points
- Async matchers exist only on expectAsync, never on expect
- compare is async and resolves to { pass, message }
- Keep the internal deadline below DEFAULT_TIMEOUT_INTERVAL
- Put the observed value in the message so failures are actionable
Q34A Jasmine suite of 4,000 specs takes 22 minutes in CI. Where does the time actually go and how do you cut it?
AdvancedPerformance
Answer
Measure before touching anything. A ten-line reporter that records specDone.duration and prints the fifty slowest specs almost always shows a long tail: a small minority of specs consume most of the wall clock. In Angular suites the usual culprits, in order, are over-broad testing modules, real waiting, and coverage instrumentation.
An over-broad module is a configureTestingModule that imports a whole feature module, which forces Angular to compile every component in it for every spec even though the test touches one. Switching to standalone components and importing only what the component needs, or substituting stub components for heavy children, often removes minutes. Real waiting is any spec that genuinely sleeps: a setTimeout in the code under test with no fake clock, a retry with a backoff delay, a whenStable that waits on a real timer.
Those get replaced with fakeAsync and tick, or jasmine.clock in non-Angular code, converting seconds into microseconds. Coverage instrumentation roughly doubles execution time, so collect it in one dedicated CI job rather than in every pull-request run. Beyond that, structural changes give the biggest wins: split the largest spec files, run the Node CLI with --parallel, or shard the suite across CI jobs so four runners each take a quarter.
Also check the boring things, because they are common: a globally raised DEFAULT_TIMEOUT_INTERVAL that turns each flaky spec into a thirty-second stall, console logging inside a hot beforeEach, and fixtures reading real files or hitting a real network. Locally, developers should use --filter rather than the full run, and the fast feedback loop matters as much as the CI number.
// spec/helpers/slowest.js -> where the 22 minutes actually go
const timings = [];
jasmine.getEnv().addReporter({
specDone: (r) => timings.push([r.duration, r.fullName]),
jasmineDone: () => {
const total = timings.reduce((s, [d]) => s + d, 0);
console.log('total spec time: ' + (total / 1000).toFixed(1) + 's');
timings.sort((a, b) => b[0] - a[0]).slice(0, 50)
.forEach(([d, name]) => console.log(d + 'ms ' + name));
},
});
// SLOW: compiles every component in the feature module, per spec
TestBed.configureTestingModule({ imports: [OrdersFeatureModule] });
// FAST: only what this component needs
TestBed.configureTestingModule({
imports: [OrderRowComponent],
providers: [{ provide: OrderService, useValue: ordersSpy }],
});
// SLOW: two real seconds of wall clock
it('retries', async () => { await retryWithBackoff(fn); });
// FAST: virtual time
it('retries', fakeAsync(() => { retryWithBackoff(fn); flush(); }));
// CI: shard across four runners
// npx jasmine --parallel=4
// ng test --watch=false --code-coverage # coverage in ONE job only
Key Points
- Rank specs by duration first; the tail is where the minutes hide
- Narrow TestBed imports so Angular compiles less per spec
- Replace real sleeps with fakeAsync/tick or jasmine.clock
- Coverage belongs in one job; shard or parallelise the rest
Q35How do you keep date and timezone assertions stable when developers run IST and CI runs UTC?
AdvancedFake Clock
Answer
This bites every Indian team, because laptops are on Asia/Kolkata at UTC+5:30 while the CI container is on UTC, and a five-and-a-half-hour shift crosses a day boundary for anything scheduled in the evening. jasmine.clock().mockDate freezes the instant, which removes the 'test passes until midnight' class of bug, but it does not change the runtime timezone, so any code path that formats or truncates to a local day still behaves differently in the two environments. Three defences, in order of value. First, pin the zone: put TZ=Asia/Kolkata (or TZ=UTC, as long as it is the same everywhere) in front of the test script so Node agrees in both places.
That works for Node; a real browser takes its zone from the operating system, so with jasmine-browser-runner you either set TZ on the container before launching the browser or you avoid depending on the ambient zone at all. Second, assert on unambiguous representations: toISOString or an epoch number rather than toLocaleDateString. Third, when you must assert formatted output, pass explicit locale and options including timeZone, and even then be aware the output is ICU data and moves between runtimes.
The real example people hit is the space before AM and PM in en-US times, which became a narrow no-break space in a newer ICU version and broke a very large number of exact-string assertions the day teams upgraded Node. The India-specific version of the problem is the financial year: it starts on 1 April, so an invoice created at 00:30 IST on 1 April is still 31 March in UTC, and a helper that derives the year from UTC parts assigns it to the wrong FY.
// package.json
// "scripts": { "test": "TZ=Asia/Kolkata jasmine" }
describe('financialYear', () => {
beforeEach(() => jasmine.clock().install());
afterEach(() => jasmine.clock().uninstall());
it('rolls over at 1 April IST, not 1 April UTC', () => {
// 2026-03-31T19:00Z is 2026-04-01T00:30 in Asia/Kolkata
jasmine.clock().mockDate(new Date('2026-03-31T19:00:00Z'));
expect(financialYear()).toBe('2026-27');
});
it('is stable regardless of the runner timezone', () => {
jasmine.clock().mockDate(new Date('2026-04-01T04:00:00Z'));
const invoice = createInvoice({ amount: 118000 });
// Robust: unambiguous instant
expect(invoice.createdAt.toISOString()).toBe('2026-04-01T04:00:00.000Z');
// Robust: explicit zone, not the ambient one
const inIst = new Intl.DateTimeFormat('en-IN', {
timeZone: 'Asia/Kolkata',
dateStyle: 'short',
}).format(invoice.createdAt);
expect(inIst).toBe('1/4/26');
});
// Brittle: depends on the machine's zone AND on bundled ICU data
// expect(invoice.createdAt.toLocaleString()).toBe('01/04/2026, 9:30 am');
});
Key Points
- mockDate freezes the instant, it does not set the timezone
- Pin TZ in the test script so laptops and CI containers agree
- Assert on toISOString or epoch, not on locale-formatted strings
- Indian FY starts 1 April, so IST evening dates land in the wrong year under UTC
Frequently Asked Questions
What salary can I expect with Jasmine on my CV in India?
Jasmine on its own is a supporting skill, not a job title, so the band follows the primary role. Angular developers with solid unit testing habits sit around ₹5-16 LPA in 2026, with freshers and one-year candidates at Infosys, TCS, Wipro, Cognizant and LTIMindtree closer to the bottom of that range and four to seven year Angular engineers in Pune, Bengaluru and Hyderabad product teams nearer the top. SDET and QA automation roles that combine Jasmine with Protractor replacements such as Playwright or Cypress, plus CI ownership, typically land ₹8-20 LPA. What visibly moves the number in interviews is not knowing more matchers, it is being able to explain flaky-test triage, spy discipline and the Karma to Vitest transition, because those are the problems teams are actually paying to have solved.
How long does it take to prepare for a Jasmine interview?
If you already write Angular or Node JavaScript daily, one focused week is enough for the core: the declaration versus execution phases, the matcher set, spies and the three call strategies, the lifecycle hooks, asynchronous specs with expectAsync, and the fake clock. Add a second week if you also need TestBed, HttpTestingController and fakeAsync, since those are where component interviews spend most of their time. Coming in with no testing background at all, budget three to four weeks and write specs for real code rather than reading about them, because interviewers ask you to debug a failing spec far more often than they ask for a definition. The highest-return preparation is deliberately breaking a suite: leave a clock installed, commit an fdescribe, share state in a describe body, then read the errors Jasmine gives you.
What do interviewers ask freshers versus experienced candidates?
Freshers and candidates with up to two years get definitional and mechanical questions: the difference between toBe and toEqual, what beforeEach does, how to spy on a service method, why a spec that uses setTimeout fails. Expect to write a small spec on a shared screen. From three years onward the questions become diagnostic. You are handed a scenario, a suite that fails once in twenty runs, a spec that passes alone and fails in the suite, a timeout with no useful message, and asked how you would narrow it down. Senior and SDET interviews add architecture: how the suite is structured, what runs in CI, how long it takes and why, how doubles are kept honest, and whether you can plan a migration off Karma without stopping feature delivery.
Is Jasmine still worth learning in 2026 when Jest and Vitest exist?
Yes, for a specific and pragmatic reason: the installed base. A very large number of Angular applications in Indian service and product companies are on Jasmine today, and those suites will be maintained for years even as new projects start on Vitest. Interviews follow the code that exists, not the code people wish existed. Beyond employability, the concepts transfer almost completely. Spies, matchers, lifecycle hooks, fake timers, asymmetric matchers and the declaration versus execution distinction all appear in Jest and Vitest with different names, so learning them in Jasmine is not wasted effort. The honest caveat is that you should not learn only Jasmine. Knowing how its concepts map onto Vitest is now part of what a strong candidate demonstrates.
Jasmine versus Jest versus Vitest: how do they actually differ?
Jasmine is self-contained but deliberately minimal: assertions, spies and a fake clock, with no transformer, no module mocker and no snapshots, and you choose the runner separately (the jasmine CLI, jasmine-browser-runner or Karma). Jest bundles everything, including a transformer, jsdom, snapshot testing and module mocking through jest.mock, which is why the React ecosystem standardised on it. Vitest offers a Jest-compatible API on top of Vite's transform pipeline, giving much faster startup and native ES module handling, and it is the runner Angular's newer unit test builder uses. The practical differences that matter in a spec file are module mocking, which Jasmine cannot do, and snapshots, which it does not provide. Everything else is naming: and.returnValue versus mockReturnValue, jasmine.any versus expect.any.
Karma is deprecated. Does that make Jasmine obsolete too?
No, and confusing the two is a common interview slip. Karma is the browser launcher and file server; Jasmine is the framework that defines describe, it, expect and spies. Only Karma was deprecated. Jasmine continues to be maintained and has its own supported browser runner, jasmine-browser-runner, plus the Node CLI with parallel execution. What is genuinely changing is the Angular default: new Angular workspaces now point at the Vitest-based unit test builder rather than Karma, so over time Angular specs will use Vitest APIs. Existing Jasmine suites keep running, and teams migrate on their own schedule. If an interviewer raises this, the answer they want is that you can distinguish runner from framework, and that you have a file-by-file migration plan rather than a rewrite.
Introduction
Jasmine is the behaviour-driven testing framework that ships with almost every Angular codebase, and in 2026 it is still the assertion and spy layer that Indian service companies see most often in their JavaScript estates. Unlike Jest or Vitest, Jasmine has no bundled transformer, no module mocker and no snapshot engine. What it does give you is a self-contained package: describe and it for structure, a large matcher library, first-class spies through spyOn and createSpyObj, a fake clock that patches setTimeout and Date, and three runners (the jasmine CLI for Node, jasmine-browser-runner for real browsers, and karma-jasmine for legacy Angular projects).
Interviews reflect that split. Product teams that write plain Node or browser JavaScript probe the runner itself: random spec ordering and seeds, the declaration versus execution phase, expectAsync, custom matchers, reporters and parallel workers. Angular teams at Infosys, TCS, Cognizant, LTIMindtree and Persistent Systems probe the integration surface instead: TestBed wiring, HttpTestingController, createSpyObj for injected services, why the Karma builder is being retired and what happens to existing specs when a project moves to the Vitest-based unit test builder. Both sides ask about flakiness, because a suite that fails one run in twenty is the single most common complaint on real projects.
This page works through 35 Jasmine interview questions in rising order of difficulty, each answered the way a working engineer would explain it: what the API actually does, the failure mode it produces when misused, and the exact error string you will see in the console. Most questions carry a runnable code example. Start with the basics if you are preparing for a two-year-experience Angular role, and go straight to the intermediate and advanced sections (parallel execution, ES module spying, memory growth, migration strategy) if you are interviewing for a senior or SDET position.
Ready to practice Jasmine interviews?
Don't just read, practice these Jasmine questions live with an AI interviewer that asks follow-ups and scores your answers.