Jasmine is a JavaScript testing framework that runs unit tests in Node.js or in a browser and ships a test runner, the expect() assertions, spies for fake dependencies and a mock clock for timers in one package. Jasmine unit testing follows the behavior-driven style. A describe() block names the code under test, and each it() block inside it states one behavior that its expect() calls check.
We use Jasmine to test JavaScript and TypeScript code such as Node.js services and Angular components, and Karma with Jasmine was the default test setup of Angular CLI projects for many years. For example, a shop’s cart module gets one spec per pricing rule, so a broken discount fails the CI build before a customer sees a wrong total.
The following example is a quick reference for Jasmine 7 on Node.js 22. It tests a small shopping cart module in TypeScript with matchers, a spy object, an async expectation and the mock clock.
describe("Shopping cart", () => {
let cart: Cart;
// 1. Fresh cart before each spec
beforeEach(() => {
cart = new Cart();
cart.add("apple", 2);
cart.add("banana", 1, 3);
});
// 2. Matchers
it("calculates the total", () => {
expect(cart.total()).toBe(5); // 5 === 5
expect(cart.items[0]).toEqual({ name: "apple", price: 2, qty: 1 }); // deep equality
expect(cart.total(percentOff(10))).toBeCloseTo(4.5, 2); // 4.5
expect(cart.items.map((item) => item.name)).toContain("banana");
expect(() => cart.add("apple", -1)).toThrowError(RangeError);
});
// 3. Spy object and async expectation
it("checks the stock", async () => {
const stock = jasmine.createSpyObj<StockService>("StockService", ["available"]);
stock.available.and.returnValue(Promise.resolve(0));
const shop = new Cart(stock);
await expectAsync(shop.addIfInStock("cherry", 3)).toBeRejectedWithError("Out of stock: cherry");
expect(stock.available).toHaveBeenCalledWith("cherry");
});
// 4. Mock clock for timers
it("expires after 15 minutes", () => {
jasmine.clock().install();
cart.expireAfter(15 * 60 * 1000);
jasmine.clock().tick(15 * 60 * 1000);
expect(cart.count).toBe(0);
jasmine.clock().uninstall();
});
});
Notice that the async spec awaits expectAsync(), and the timer spec moves the mock clock forward by 15 minutes instead of waiting.
Next, we set up Jasmine with TypeScript and go through matchers, hooks, spies, async specs and timers on the same cart. After that, we cover focused and skipped specs and the test report, and compare Jasmine with Jest, Vitest and the built-in node:test runner.
1. What Is Jasmine?
Jasmine is an open source testing framework for JavaScript and TypeScript code. A Java developer can think of Jasmine as JUnit and Mockito in one library. The matchers replace assertEquals(), and the spies replace mock objects. Jasmine depends on no other framework, not even on a browser or the DOM.
Jasmine uses its own names for the parts of a test, and the same names appear in the test report and in the configuration.
| Term | Jasmine function | Meaning |
|---|---|---|
| Suite | describe() | A named group of specs, for example all tests of the Cart class. Suites can be nested. |
| Spec | it() | One test case. It passes when all its expectations pass. |
| Expectation | expect(actual) | One check of an actual value. |
| Matcher | toBe(), toEqual(), … | The comparison that an expectation applies to the actual value. |
| Hook | beforeEach(), afterEach(), beforeAll(), afterAll() | Setup and cleanup code that runs around the specs. |
| Spy | spyOn(), jasmine.createSpy() | A fake function that records its calls and can return fixed values. |
A spec file nests these parts. When an expectation fails, Jasmine marks the spec as failed, but by default it still runs the remaining expectations in that spec, so a single run reports every broken check.

2. Setting Up Jasmine With TypeScript
Jasmine runs JavaScript, so for TypeScript specs we have two working setups on Node.js 22. Both use the same project and the same spec files.
- Compile the specs and the source with tsc, then run the compiled JavaScript with Jasmine. This setup also type-checks every spec.
- Run the .ts files without a build step and let Node.js strip the types at load time. This setup needs no build step but does not type-check.
Jasmine supports both setups for TypeScript, and each setup needs its own Jasmine config file.
2.1. Installing Jasmine
We create a project, mark it as an ES module package, and install Jasmine with its TypeScript type definitions. The @types/jasmine package declares the global functions describe(), it(), expect() and the rest, so the compiler knows them without imports.
mkdir jasmine-unit-testing && cd jasmine-unit-testing
npm init -y
npm pkg set type=module
npm install --save-dev jasmine@7.0.0 @types/jasmine@7.0.0 typescript@7.0.2 @types/node@26.6.4
npx jasmine init
In a fresh Jasmine 7.0.0 project, the npx jasmine init command creates spec/support/jasmine.mjs with the default settings.
export default {
spec_dir: "spec",
spec_files: [
"**/*[sS]pec.?(m)js"
],
helpers: [
"helpers/**/*.?(m)js"
],
env: {
stopSpecOnExpectationFailure: false,
random: true,
forbidDuplicateNames: true
}
}
Each key controls one part of the run.
- The spec_dir key is the folder that Jasmine searches, relative to the project root.
- The spec_files key lists glob patterns for the spec files, relative to spec_dir. By default, a spec file name ends with spec.js or spec.mjs.
- The helpers key lists files that Jasmine loads before any spec, for example custom matchers.
- The setting random: true runs the specs in random order to find specs that depend on each other. The report prints the seed, so a failing order can be repeated.
- The setting forbidDuplicateNames: true reports an error when two specs or suites in the same parent have the same name.
2.2. Running Compiled Specs With tsc
In this setup, tsc writes the compiled files to dist, and Jasmine reads the specs from dist/spec. We change only the first three keys of the generated config.
export default {
spec_dir: "dist/spec",
spec_files: [
"**/*[sS]pec.js"
],
helpers: [
"helpers/**/*.js"
],
env: {
stopSpecOnExpectationFailure: false,
random: true,
forbidDuplicateNames: true
}
}
The project’s tsconfig.json compiles the src, spec and demo folders and loads the Jasmine and Node.js types. Two options let the same files work in both setups. The rewriteRelativeImportExtensions option allows import … from “../src/cart.ts” and rewrites it to cart.js in the output. The erasableSyntaxOnly option reports an error for syntax that Node.js cannot strip, such as enums and constructor parameter properties.
{
"compilerOptions": {
"target": "es2024",
"lib": ["es2024"],
"module": "nodenext",
"strict": true,
"rootDir": ".",
"outDir": "dist",
"types": ["node"],
"skipLibCheck": true
},
"include": ["src"]
}
The test script compiles first and runs Jasmine only when the compiler reports no errors.
"scripts": {
"build": "tsc",
"test": "tsc && jasmine",
"test:ts": "jasmine --config=spec/support/jasmine-ts.mjs"
}
2.3. Running .ts Specs With Node.js Type Stripping
Since Node.js 22.18, Node.js runs .ts files by default, and it replaces the type annotations with spaces without checking the types. Jasmine loads spec files with a dynamic import(), so it can load .ts specs when the config points to them. On Node.js 22.6 to 22.17, the same works when Node.js starts with the –experimental-strip-types flag, for example through the NODE_OPTIONS environment variable.
export default {
spec_dir: "spec",
spec_files: [
"**/*[sS]pec.ts"
],
helpers: [
"helpers/**/*.ts"
],
env: {
stopSpecOnExpectationFailure: false,
random: true,
forbidDuplicateNames: true
}
}
The two setups differ in a few practical points.
| npm test (tsc, then Jasmine) | npm run test:ts (type stripping) | |
|---|---|---|
| Type errors in specs | Stop the run | Ignored, so only the editor shows them |
| Build step | Yes, writes dist/ | No |
| Stack traces point to | dist/spec/…js | spec/…ts |
| Node.js version | Any Node.js 22 | 22.18 or newer (22.6+ with a flag) |
We use npm test in CI, because it fails the build on a type error, and npm run test:ts while writing specs.
3. Jasmine Unit Testing Example With a Shopping Cart
The module under test is a small shopping cart in src/cart.ts. It has no framework code, and its constructor takes a stock service, so the tests can replace the service with a spy.
| Member | What it does |
|---|---|
| add(name, price, qty = 1) | Adds an item or increases the quantity of an existing item. Throws a RangeError for a negative price. |
| items, count | The item list and the total number of units. |
| subtotal() | Sum of price times quantity. |
| total(…discounts) | Subtotal minus all discount rules, never below 0. |
| percentOff(10), flatOffOver(5, 1) | Discount rules, either 10% off or 1 off when the subtotal is at least 5. |
| addIfInStock(name, price, qty) | Async. Asks the StockService for the available quantity and throws “Out of stock: name” when it is too low. |
| expireAfter(ms, onExpire) | Empties the cart after a timeout. |
A spec file imports the code under test and calls describe() with a name and a function, and inside that function each it() call is one spec. Jasmine joins the suite name and the spec name into the full name in the report, for example “Cart add() counts every unit”, so we write them to read as a sentence.
import { Cart, flatOffOver, percentOff } from "../src/cart.ts";
describe("Cart", () => {
let cart: Cart;
beforeEach(() => {
cart = new Cart();
cart.add("apple", 2);
cart.add("banana", 1, 3);
});
describe("add()", () => {
it("counts every unit", () => {
expect(cart.count).toBe(4);
});
});
});
We declare the let cart variable in the describe() function, and beforeEach() assigns a new Cart before every spec. Each spec gets its own cart, so a spec that adds items cannot change the result of another spec. When we run npm test, Jasmine prints one dot per passing spec.
Randomized with seed 86575
Started
.....................
21 specs, 0 failures
Finished in 0.017 seconds
Randomized with seed 86575 (jasmine --random=true --seed=86575)
4. Jasmine Matchers
A matcher compares the actual value from expect() with an expected value, and we can invert every matcher with .not, as in expect(names).not.toContain(“cherry”). Jasmine has many more matchers, but the ones in the table cover most specs.
| Matcher | Passes when |
|---|---|
| toBe(x) | actual === x (same primitive value or same object reference) |
| toEqual(x) | actual and x have the same content (deep equality) |
| toBeCloseTo(x, digits) | The numbers are equal to digits decimal places (default 2) |
| toContain(x) | An array has the element x, or a string has the substring x |
| toThrowError(type, message) | The function throws an error of that type and message |
| toBeTruthy(), toBeFalsy() | The value is truthy or falsy |
| toBeUndefined(), toBeNull(), toBeDefined() | The value is undefined or null, or not undefined |
| toBeGreaterThan(x), toBeLessThan(x) | Number comparisons |
| toMatch(regex) | A string matches a regular expression |
| toHaveSize(n) | An array, string, Set or Map has n elements |
4.1. toBe() vs toEqual()
The toBe() matcher uses the === operator, so for numbers, strings and booleans it compares the values, whereas for objects and arrays it checks whether both sides are the same object in memory. To compare the content of objects or arrays, we use toEqual().
expect(cart.total()).toBe(5); // passes
expect(cart.items[0]).not.toBe({ name: "apple", price: 2, qty: 1 }); // different object
expect(cart.items[0]).toEqual({ name: "apple", price: 2, qty: 1 }); // same content
When toBe() fails on two objects with the same content, Jasmine adds a hint to the message.
Expected Object({ name: 'apple', qty: 1 }) to be Object({ name: 'apple', qty: 1 }). Tip: To check for deep equality, use .toEqual() instead of .toBe().
4.2. Decimal Numbers, Arrays and Truthy Values
Prices are decimal numbers, and JavaScript stores them as binary floating point values. The sum 0.1 + 0.2 is 0.30000000000000004, so toBe(0.3) fails. For decimal prices, we use toBeCloseTo(), which compares the numbers rounded to a given number of decimal places.
// 1. Decimal prices
const small = new Cart();
small.add("pen", 0.1);
small.add("clip", 0.2);
expect(small.total()).not.toBe(0.3); // 0.30000000000000004
expect(small.total()).toBeCloseTo(0.3, 2); // passes
// 2. Array elements
const names = cart.items.map((item) => item.name);
expect(names).toContain("banana");
expect(names).not.toContain("cherry");
// 3. Truthy and falsy values
expect(cart.total() > 0).toBeTruthy();
expect(new Cart().count).toBeFalsy(); // 0 is falsy
4.3. Testing Exceptions With toThrowError()
To test that code throws, we pass a function to expect(), not the result of a call. Jasmine calls the function and catches the error. If we wrote expect(cart.add(“apple”, -1)), the error would be thrown before expect() runs, and the spec would fail with that error. The cart throws a RangeError, which is one of the standard error classes in TypeScript.
expect(() => cart.add("apple", -1)).toThrowError(RangeError, "Price must not be negative");
expect(() => cart.add("apple", -1)).toThrowError(/negative/);
The first form checks the error class and the exact message, whereas the second form checks only that the message matches a regular expression. The toThrow() matcher without “Error” also exists. It compares the thrown value with toEqual() semantics and fits code that throws something other than an Error.
4.4. Partial Matching With jasmine.any() and jasmine.objectContaining()
Sometimes only some fields matter, or a value is not known in advance, such as a generated ID or a timestamp. For example, a spec for an order service checks the ordered items and that the saved order has some numeric id, without knowing which id the database generated. Jasmine has asymmetric matchers for partial matching, and we can use them anywhere an expected value goes, including inside toEqual() and toHaveBeenCalledWith().
- The matcher jasmine.any(Number) matches any value of that type.
- The matcher jasmine.objectContaining({ … }) matches an object that has at least these properties.
- The matcher jasmine.anything() matches everything except null and undefined.
- The matchers jasmine.arrayContaining([ … ]) and jasmine.stringContaining(“…”) match part of an array or string.
expect(cart.items[1]).toEqual(jasmine.objectContaining({ name: "banana" }));
expect(cart.items[1]).toEqual({ name: "banana", price: jasmine.any(Number), qty: 3 });
4.5. Writing a Custom Matcher
A custom matcher turns a repeated check into one readable line. We register it with jasmine.addMatchers() inside a beforeEach() in a helper file, so every spec can use it. The matcher factory returns a compare() function that receives the actual value and the arguments, and returns pass and a failure message. The declare global block adds the method to the jasmine.Matchers interface, so TypeScript accepts expect(cart).toHaveItem(“apple”).
declare global {
namespace jasmine {
interface Matchers<T> {
toHaveItem(name: string): boolean;
}
}
}
beforeEach(() => {
jasmine.addMatchers({
toHaveItem: () => ({
compare(actual: Cart, name: string) {
const pass = actual.items.some((item) => item.name === name);
return {
pass,
message: "Expected cart " + (pass ? "not " : "") + "to have item " + name,
};
},
}),
});
});
expect(cart).toHaveItem("apple");
expect(cart).not.toHaveItem("banana");
Jasmine also supports custom equality testers and async matchers, which we register in the same way.
5. Setup and Teardown Hooks
Hooks remove repeated setup code from specs. Jasmine has four of them, and each one applies to all specs in the describe() where it is declared, including nested suites.
- The beforeEach() hook runs before every spec, so this is where we create fresh test data.
- The afterEach() hook runs after every spec, also after a failed one, so this is where we undo global changes, for example by uninstalling the mock clock.
- The beforeAll() hook runs once before the first spec of the suite. It fits expensive setup that specs only read, such as loading a large product catalog fixture.
- The afterAll() hook runs once after the last spec of the suite.
Data created in beforeAll() is shared by all specs, so a spec that changes it affects the specs after it. With random order turned on, a shared-data bug appears only for some seeds, so we keep changing data in beforeEach().
The demo spec demo/lifecycle.spec.ts logs from every hook in two nested suites.
describe("outer", () => {
beforeAll(() => console.log("beforeAll outer"));
afterAll(() => console.log("afterAll outer"));
beforeEach(() => console.log(" beforeEach outer"));
afterEach(() => console.log(" afterEach outer"));
describe("inner", () => {
beforeEach(() => console.log(" beforeEach inner"));
afterEach(() => console.log(" afterEach inner"));
it("spec 1", () => {
console.log(" it spec 1");
expect(true).toBeTrue();
});
it("spec 2", () => {
console.log(" it spec 2");
expect(true).toBeTrue();
});
});
});
With random order turned off, npm run demo:hooks prints the order in which the hooks run.
Started
beforeAll outer
beforeEach outer
beforeEach inner
it spec 1
afterEach inner
afterEach outer
. beforeEach outer
beforeEach inner
it spec 2
afterEach inner
afterEach outer
.afterAll outer
2 specs, 0 failures
Finished in 0.006 seconds
The beforeEach() hooks run from the outer suite to the inner suite, and the afterEach() hooks run in reverse order.

6. Jasmine Spies
A spy is a test double, which means a function that records every call and its arguments and returns whatever the test tells it to return. We use spies to replace slow or unpredictable dependencies, such as a stock service that calls a remote API, and to check that our code calls them correctly. Jasmine removes every spy after the spec that created it, so a spy never leaks into the next spec.
6.1. spyOn() With and.returnValue()
The function spyOn(object, “method”) replaces an existing method of an object with a spy. By default, the spy does not call the original method and returns undefined. The and property configures what the spy does instead, and and.returnValue() sets a fixed return value. In the next spec, the cart calls the spy instead of the real service.
const stock: StockService = { available: async () => 0 };
const spy = spyOn(stock, "available").and.returnValue(Promise.resolve(10));
const cart = new Cart(stock);
const count = await cart.addIfInStock("apple", 2, 3); // count = 3
expect(spy).toHaveBeenCalledWith("apple");
expect(spy).toHaveBeenCalledTimes(1);
TypeScript also checks the spy setup, so a misspelled method name and a return value of the wrong type are both compile errors.
error TS2345: Argument of type '"availble"' is not assignable to parameter of type '"available"'.
error TS2345: Argument of type 'number' is not assignable to parameter of type 'Promise<number>'.
The and property has a few more strategies.
| Strategy | The spy… |
|---|---|
| and.returnValue(v) | returns v |
| and.returnValues(a, b) | returns a on the first call, b on the second |
| and.callFake(fn) | runs fn with the same arguments |
| and.callThrough() | calls the original method (the call is still recorded) |
| and.throwError(e) | throws e |
| and.resolveTo(v), and.rejectWith(e) | returns a promise that resolves or rejects |
6.2. Fake Implementations With and.callFake()
When the return value depends on the arguments, and.callFake() runs our own function in place of the real method. In the next spec, the fake stock service has 5 apples and no bananas.
const stock = jasmine.createSpyObj<StockService>("StockService", ["available"]);
stock.available.and.callFake(async (name) => (name === "apple" ? 5 : 0));
const cart = new Cart(stock);
await expectAsync(cart.addIfInStock("apple", 2)).toBeResolvedTo(1);
await expectAsync(cart.addIfInStock("banana", 1)).toBeRejectedWithError("Out of stock: banana");
6.3. Spy Objects With jasmine.createSpyObj()
The function spyOn() needs a real object first, whereas jasmine.createSpyObj() creates a new object whose methods are all spies, which suits an interface such as StockService that has no implementation in the test. The generic type argument gives the result the type jasmine.SpyObj<StockService>, so it can be passed to the Cart constructor. The function createSpyObj() takes the method names in two forms.
// 1. Method names only; configure each spy later
const stock1 = jasmine.createSpyObj<StockService>("StockService", ["available"]);
// 2. Method names with return values
const stock2 = jasmine.createSpyObj<StockService>("StockService", {
available: Promise.resolve(1),
});
For a single callback function, jasmine.createSpy(“name”) creates one standalone spy, and the cart expiry test in section 7.2 uses one as the onExpire callback.
6.4. Checking How a Spy Was Called
Spy matchers check the recorded calls, and the calls property gives direct access to them. After the two addIfInStock() calls from section 6.2, the spy has recorded “apple” and then “banana”.
expect(stock.available).toHaveBeenCalled(); // at least once
expect(stock.available).toHaveBeenCalledWith("apple"); // in any call
expect(stock.available).toHaveBeenCalledTimes(2);
const total = stock.available.calls.count(); // total = 2
const second = stock.available.calls.argsFor(1); // second = ["banana"]
The matcher toHaveBeenCalledOnceWith(“apple”) is stricter, because it passes only when the spy was called one time in total, with these arguments and no others. Other useful members of calls are allArgs(), mostRecent(), first() and reset().
7. Testing Async Code and Timers
Node.js code often uses promises and timers. For example, the cart checks stock through a service that returns a promise, and it empties itself after 15 minutes with a timer. Jasmine handles both without extra libraries, with async specs and expectAsync() for promises and jasmine.clock() for setTimeout() and setInterval().
7.1. Async Specs and expectAsync()
When the function passed to it() is async, Jasmine waits for its promise before it finishes the spec. Inside, we can await the code under test and use normal matchers on the result. For checks on the promise itself, expectAsync() has async matchers.
// 1. await the result, then use a normal matcher
const count = await cart.addIfInStock("apple", 2, 3); // count = 3
expect(count).toBe(3);
// 2. The promise resolves to a value
await expectAsync(cart.addIfInStock("apple", 2)).toBeResolvedTo(1);
// 3. The promise rejects with an Error
await expectAsync(cart.addIfInStock("banana", 1)).toBeRejectedWithError("Out of stock: banana");
await expectAsync(cart.addIfInStock("apple", 2, 2)).toBeRejectedWithError(Error, /Out of stock/);
An expectAsync() matcher returns a promise, so we always await it. Without await, the spec ends before the check runs, and Jasmine reports an error in the suite instead of a clean failure.
Spec "missing await forgets await" ran a "toBeResolvedTo" expectation after it finished.
Message: "Expected a promise to be resolved to 1 but it was rejected with Error: No stock service."
1. Did you forget to return or await the result of expectAsync?
An async spec fails when it takes longer than 5 seconds, the default value of jasmine.DEFAULT_TIMEOUT_INTERVAL. A third argument to it() sets a longer timeout in milliseconds for one spec. The older callback style, it(“…”, (done) => { …; done(); }), still works, but async and await are shorter and fail with clearer messages.
7.2. Testing Timers With jasmine.clock()
The cart empties itself 15 minutes after we call expireAfter(), and a real test cannot wait that long. The call jasmine.clock().install() replaces setTimeout(), setInterval() and their clear functions with fake versions, and tick(ms) moves the fake time forward and runs every timer that is due. The call uninstall() restores the real functions, so we put it in afterEach(), where it runs even when the spec fails.
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it("empties the cart after 15 minutes", () => {
const cart = new Cart();
const onExpire = jasmine.createSpy("onExpire");
cart.add("apple", 2);
cart.expireAfter(15 * 60 * 1000, onExpire);
jasmine.clock().tick(15 * 60 * 1000 - 1);
expect(cart.count).toBe(1); // 1 ms too early
expect(onExpire).not.toHaveBeenCalled();
jasmine.clock().tick(1);
expect(cart.count).toBe(0);
expect(onExpire).toHaveBeenCalled();
});
Because the time is fake, the spec finishes in milliseconds. For code that reads the current Date, the mock clock has mockDate(). The call jasmine.clock().mockDate(new Date(2026, 0, 1)) fixes new Date() and Date.now() to a chosen moment, and tick() moves them forward.
8. Running, Focusing and Skipping Specs
While we work on one failing spec, running the whole suite slows us down. Jasmine has four prefixed variants of describe() and it() for focusing and skipping specs, plus a –filter option.
| Function | Effect |
|---|---|
| fdescribe(), fit() | Focused, so when any focused suite or spec exists, Jasmine runs only the focused ones. |
| xdescribe(), xit() | Excluded, so the specs are reported as pending and do not run. |
| pending(“reason”) | Called inside a spec, marks it as pending with a reason. |
| npx jasmine –filter=”decimal” | Runs only specs whose full name matches the regular expression. |
describe("Cart", () => {
fit("runs because of fit", () => {
expect(1 + 1).toBe(2);
});
it("is skipped while a focused spec exists", () => {
expect(1 + 1).toBe(2);
});
xdescribe("discounts", () => {
it("is disabled by xdescribe", () => {
expect(1 + 1).toBe(2);
});
});
});
Randomized with seed 24164
Started
.
Ran 1 of 3 specs
1 spec, 0 failures
Finished in 0.004 seconds
Incomplete: fit() or fdescribe() was found
Randomized with seed 24164 (jasmine --random=true --seed=24164)
A forgotten fit() would let a CI build pass while most specs never run. Jasmine prevents a forgotten fit() from passing, because the run ends as “Incomplete” with exit code 2, so the build fails.
8.1. Reading the Test Report
The default console reporter prints one character per spec while the run is in progress. Each pass prints a dot and each failure prints F, whereas *** marks a pending spec. After the run, the reporter lists each failure with the full spec name, the matcher message and the stack trace, and after the failures each pending spec with its reason. The demo spec in demo/failing.spec.ts has two wrong expectations and one xit().
it("adds two decimal prices", () => {
const cart = new Cart();
cart.add("pen", 0.1);
cart.add("clip", 0.2);
expect(cart.total()).toBe(0.3);
});
it("stores the item", () => {
const cart = new Cart();
cart.add("pen", 0.1);
expect(cart.items[0]).toEqual({ name: "pen", price: 0.1, qty: 2 });
});
xit("applies a coupon code", () => {
expect(true).toBeTrue();
});
Running npm run demo:failing prints the report, with the project path shortened.
Started
FF*
Failures:
1) Cart total adds two decimal prices
Message:
Expected 0.30000000000000004 to be 0.3.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///.../jasmine-unit-testing/demo/failing.spec.ts:8:26)
at <Jasmine>
2) Cart total stores the item
Message:
Expected $.qty = 1 to equal 2.
Stack:
at <Jasmine>
at UserContext.<anonymous> (file:///.../jasmine-unit-testing/demo/failing.spec.ts:14:27)
at <Jasmine>
Pending:
1) Cart total applies a coupon code
Temporarily disabled with xit
3 specs, 2 failures, 1 pending spec
Finished in 0.007 seconds
Three details help when reading a report.
- For toEqual(), the message names the property that differs, where $.qty is the qty property of the actual object, and the message shows the actual value 1 and the expected value 2.
- The stack line gives the file, line and column of the failed expect(). With type stripping, the stack line points to the .ts file, whereas with npm test it points to the compiled file in dist.
- With random order, the report starts and ends with the seed. The command npx jasmine –random=true –seed=86575 repeats that order.
The process exit code tells CI the result. The code is either 0 when all specs pass, 3 when a spec fails, or 2 for an incomplete run, such as a focused spec or no specs found.
9. Jasmine vs Jest vs Vitest vs node:test
All four tools run unit tests on Node.js, and the spec syntax of Jasmine, Jest and Vitest is very similar (describe(), it(), expect()). Early Jest versions ran their tests on top of Jasmine. The differences are in what comes built in and how each tool handles TypeScript and ES modules. The table compares Jasmine 7.0, Jest 30.5, Vitest 5.0 and node:test on Node.js 22, based on each project’s documentation.
| Feature | Jasmine | Jest | Vitest | node:test |
|---|---|---|---|---|
| Installation | npm package jasmine | npm package jest | npm package vitest | Built into Node.js |
| Assertions | Built-in matchers | Built-in expect() | Built-in, Jest-compatible expect() | Separate node:assert module |
| Spies and mocks | spyOn(), createSpyObj() | jest.fn(), jest.spyOn() | vi.fn(), vi.spyOn() | mock.fn(), mock.method() |
| Module mocking | Not built in | jest.mock() | vi.mock() | mock.module(), behind a flag |
| Fake timers | jasmine.clock() | jest.useFakeTimers() | vi.useFakeTimers() | mock.timers (experimental) |
| Snapshot tests | No | Yes | Yes | Yes (Node.js 22.3+) |
| Code coverage | External tool | –coverage | @vitest/coverage-v8 | –experimental-test-coverage |
| Watch mode | No | –watch | Default in an interactive terminal | –watch (experimental) |
| TypeScript | Compile with tsc, or Node.js type stripping | Babel (no type check) or ts-jest | Built in, no type check | Node.js type stripping |
| ES modules | Supported | Experimental, needs –experimental-vm-modules | Supported | Supported |
| Real browser | jasmine-browser-runner | No (jsdom simulation) | Browser Mode | No |
Jasmine fits projects that want one small dependency that includes spies and a mock clock, or specs that also run in a real browser. Jest and Vitest add snapshots, coverage, watch mode and module mocking. New Angular CLI projects use Vitest by default, while Karma with Jasmine remains supported. The node:test runner needs no installation, but its assertions and mocks are more verbose.
10. Jasmine FAQs
Developers ask these questions often in searches and on forums when they combine Jasmine with TypeScript.
10.1. Why Does spyOn() Fail on an ES Module Import?
Because an ES module namespace object, as in import as cartModule from “../src/cart.ts”*, has read-only exports. The call spyOn(cartModule, “percentOff”) tries to replace the export and throws a TypeError.
TypeError: Cannot assign to read only property 'percentOff' of object '[object Module]'
In our test run with Jasmine 7.0.0, the run stopped without a report and with exit code 4. Jasmine recommends dependency injection instead. We pass the dependency in, as Cart does with StockService, and spy on the object that the test creates.
10.2. Do I Still Need @types/jasmine?
Yes, because the jasmine package contains no TypeScript declarations. Without @types/jasmine, TypeScript 7.0.2 reports error TS2593 for every spec file.
x.ts(1,1): error TS2593: Cannot find name 'describe'. Do you need to install type definitions for a test runner? Try `npm i --save-dev @types/jest` or `npm i --save-dev @types/mocha` and then add 'jest' or 'mocha' to the types field in your tsconfig.
The hint names Jest and Mocha, but for Jasmine the fix is npm install –save-dev @types/jasmine. The “types”: [“node”, “jasmine”] entry in tsconfig.json loads the globals. We keep the major version of @types/jasmine equal to the version of jasmine.
10.3. How Do I Run a Single Spec File?
We pass the file path after the command. The file arguments replace spec_files from the config, while the helpers still load.
npx jasmine dist/spec/cart.spec.js
npx jasmine --config=spec/support/jasmine-ts.mjs spec/cart.spec.ts
10.4. Can Jasmine Run Specs in Parallel?
Yes, since Jasmine 5.0, with npx jasmine –parallel=4 or –parallel=auto. A CI job with a few thousand specs can split them across the CPU cores this way, but parallel mode has a few limits.
- The functions fit() and fdescribe() are not supported.
- Random order cannot be turned off.
- A beforeEach() at the top level of a spec file is not allowed.
11. Conclusion
Jasmine gives a TypeScript project a complete unit testing setup with one npm package and its type definitions. We group specs with describe() and write each spec with it(), where expect() and a matcher check the result. Hooks prepare fresh data, spies replace dependencies, expectAsync() checks promises and jasmine.clock() makes timers instant. On Node.js 22, the specs run either after tsc with type checking or without a build step through type stripping.
Three rules prevent the most common Jasmine mistakes.
- Use toEqual() to compare objects.
- Use toBeCloseTo() for decimal numbers.
- Write await before every expectAsync().
The complete project, with the cart module, all specs and the demo scripts, is in the GitHub repository. After cloning it, npm install and npm test run all 21 specs.
12. References
- Jasmine documentation
- Jasmine: Setting up Jasmine with Node.js
- Jasmine: Your first suite
- Jasmine: Testing TypeScript
- Jasmine: Matchers API
- Jasmine: Async matchers API
- Jasmine: Spy API
- Jasmine: Clock API
- Node.js: Running TypeScript natively
- Node.js: Test runner
- Jest: ECMAScript modules
- Vitest guide
- Angular: Testing
Happy Learning !!
Excellent work! I wish that the people that develop these frameworks and such would take the effort to give meaningful examples like you have done to explain the ways their libraries can be used. (not to mention keeping them up to date) Seeing examples where they just writes a trivial block of javascript code inside an it() function without demonstrating how best to structure a test around an actual Javascript function/class you want tested isn’t very encouraging. Nice work outlining how the Jasmine test features can be set up around the stuff I’d expect to test.
nice
Thanks for this, its nice to see a demo in pure javascript.
I’m curios how you would run the tests in the command line without the help of the html script tags, is it still possible to run the jasmine command in the commandline? How does it import the javascript files without the html?
Hi, if i want to stop execution of suits on any one test case fails, or if first case fails then i want to stop execution of spec means i do not want remaining test to run. What is the way to achieve that?
throwFailures=true
Add that to the runner page’s URL.
https://github.com/jasmine/jasmine/blob/master/release_notes/2.3.0.md#pull-requests–issues
#577
This is extremely helpful!
I currently have an issue where I’m trying to test whether internal methods of a void method have been called. You briefly mention that you can use spies to verify the calls for internal methods, but can you give an example of how to do this? My spy for the external method passes, but when I try to call the internal methods I get an error:
“Error: Expected a spy, but got undefined.”
it('expects submitLogin() to have been called', function() { spyOn(component, 'submitLogin').and.callThrough(); //make the call to submitLogin component.submitLogin(); //check internal function expect(component.submitLogin).toHaveBeenCalled(); expect(component.apiService.login).toHaveBeenCalled(); })