Jasmine — Complete JavaScript Testing Framework Reference Guide
Learning Path
flowchart LR
A["Jasminejs Overview"] --> B["Core Concepts"]
B --> C["Intermediate Topics"]
C --> D["Advanced Topics"]
D --> E["Practical Applications"]
A --> F["You Are Here"]
style F fill:#f90,color:#fff
Jasmine is a behavior-driven development (BDD) testing framework for JavaScript that doesn’t require a DOM or any other framework — making it ideal for testing both Node.js and browser-based code with clean, readable syntax.
What You’ll Learn
By the end of this tutorial, you’ll write test suites with describe and it blocks, use built-in and custom matchers, spy on functions to verify calls and stub return values, mock timers with the Jasmine clock, test asynchronous code with promises and callbacks, and integrate Jasmine into your build pipeline.
Why Jasmine Matters
Testing isn’t optional — it’s how professional teams ship reliable code. Jasmine’s BDD syntax makes tests readable by non-developers (product managers, QA), bridging the communication gap between technical and non-technical team members. Jasmine is the default test runner for Angular, but works with any JavaScript framework. DodaTech uses Jasmine for unit testing Doda Browser’s extension APIs and DodaZIP’s core file-processing logic.
Security note: Understanding Reference helps build more secure applications — a core principle at DodaTech, where tools like Durga Antivirus Pro and Doda Browser rely on solid implementation practices.
Basic Structure — describe/it
Every Jasmine test is organized into suites (describe) and specs (it):
describe("Calculator", () => {
let calc;
// Runs before each `it` block — fresh setup every time
beforeEach(() => {
calc = new Calculator();
});
it("should add two numbers", () => {
expect(calc.add(2, 3)).toEqual(5);
});
it("should subtract numbers", () => {
expect(calc.subtract(5, 3)).toEqual(2);
});
// Runs after each `it` block — cleanup
afterEach(() => {
// release resources, reset state
});
});Line-by-line explanation:
describe("Calculator", () => { ... })— defines a test suite. The first argument is a name (what are we testing?), the second is a function containing tests.beforeEach(() => { ... })— runs before every single test. Creates a freshCalculatorinstance so tests don’t share state.it("should add two numbers", () => { ... })— an individual test. The string describes what the test verifies. Read it aloud: “it should add two numbers.”expect(calc.add(2, 3)).toEqual(5)— an assertion.expect()returns an expectation object..toEqual(5)is a matcher that checks deep equality.afterEach(() => { ... })— runs after every test for cleanup (closing connections, clearing timers).
Why BDD syntax matters: The test reads like a sentence: “Calculator should add two numbers.” Non-technical stakeholders can read these and understand what the system does.
Matchers — Built-in Assertions
expect(true).toBe(true); // Identity (===) — strict equality
expect(obj).toEqual({ a: 1 }); // Deep equality — checks all properties
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect(1).toBeDefined();
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect([1, 2, 3]).toContain(2); // Array/string inclusion
expect("hello").toMatch(/^hel/); // Regex match
expect(5).toBeGreaterThan(3); // Numeric comparison
expect(5).toBeLessThanOrEqual(10);
expect(fn).toThrow(); // Function must throw an error
expect(fn).toThrowError("specific error"); // Error with specific message
Negation: Add .not before any matcher:
expect(1).not.toBe(2);
expect([1, 2, 3]).not.toContain(4);Custom Matchers
Create reusable assertions for domain-specific checks:
beforeEach(() => {
jasmine.addMatchers({
toBeWithinRange: function(util, customEqualityTesters) {
return {
compare: function(actual, min, max) {
const result = { pass: actual >= min && actual <= max };
if (!result.pass) {
result.message = `Expected ${actual} to be between ${min} and ${max}`;
}
return result;
}
};
}
});
});
// Usage
expect(42).toBeWithinRange(40, 50);Spies — Tracking and Stubbing
Spies replace functions with test doubles that record calls and control behavior:
describe("Spies", () => {
it("should track calls", () => {
const obj = { method: () => {} };
spyOn(obj, "method");
obj.method(1, 2, 3);
expect(obj.method).toHaveBeenCalled();
expect(obj.method).toHaveBeenCalledWith(1, 2, 3);
expect(obj.method.calls.count()).toBe(1);
});
it("should stub a return value", () => {
spyOn(service, "getData").and.returnValue(
Promise.resolve([1, 2, 3])
);
// service.getData() now returns the promise without calling the real method
// Other options:
// spy.and.callThrough() — actually call original function
// spy.and.callFake(fn) — call a fake implementation
// spy.and.throwError() — throw an error
});
it("should create a spy object", () => {
const spy = jasmine.createSpyObj("service", ["get", "post"]);
spy.get.and.returnValue("mock data");
expect(spy.get()).toBe("mock data");
});
});Line-by-line:
spyOn(obj, "method")— replacesobj.methodwith a spy. The original function is NOT called.and.returnValue(value)— the spy returns a specific value when calledand.callThrough()— calls the original function but still tracks callsjasmine.createSpyObj("service", ["get", "post"])— creates a complete mock object with spy methods
Async Testing
describe("Async", () => {
// Modern approach: async/await
it("should handle promises", async () => {
const result = await asyncFunction();
expect(result).toBe("done");
});
// Callback approach: done parameter
it("should use done callback", (done) => {
setTimeout(() => {
expect(true).toBe(true);
done(); // Tell Jasmine this test is complete
}, 1000);
});
// Async with spies
it("should handle promise rejection", async () => {
spyOn(service, "fetchData").and.rejectWith("Network error");
await expectAsync(service.fetchData()).toBeRejectedWith("Network error");
});
});Clock Mocking
Control time-based code without waiting:
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it("should debounce", () => {
const callback = jasmine.createSpy("callback");
debounced(callback, 300);
jasmine.clock().tick(300); // fast-forward 300ms
expect(callback).toHaveBeenCalled();
});Common Mistakes
1. Sharing state between tests
If a test modifies shared state (like an array or object in a describe scope), other tests see the modified version. Always initialize fresh data in beforeEach.
2. Forgetting done() in async callback tests
Without calling done(), Jasmine considers the test passed immediately, not waiting for the async operation. The test always passes — even if the callback fails.
3. Using toBe when you need toEqual
toBe checks reference identity (===). toEqual checks deep equality. expect({a: 1}).toBe({a: 1}) fails (different objects). Use toEqual for object comparison.
4. Not cleaning up after clock mocking
If you install jasmine.clock() in beforeEach but forget uninstall() in afterEach, the clock stays mocked and all subsequent tests have broken timing.
5. Testing implementation details instead of behavior
Test what the code DOES, not how it does it. Testing internal method calls or private state makes tests brittle — they break when you refactor.
Practice Questions
1. What is the difference between toBe and toEqual?
Answer: toBe uses strict equality (===) for primitives and reference identity for objects. toEqual performs deep equality comparison on objects and arrays.
2. How do you test an asynchronous function with Jasmine?
Answer: Use async/await (modern) — await asyncFunction(); expect(result)... — or the done callback (legacy) — call done() when the async operation completes.
3. What is a spy and when would you use one?
Answer: A spy replaces a function with a test double that records calls, arguments, and can stub return values. Use spies to isolate the code under test from its dependencies.
4. How does jasmine.clock() help with testing?
Answer: It allows you to control time programmatically — fast-forwarding through delays, intervals, and timeouts without actually waiting.
Challenge
Write a test suite for a shopping cart module: test adding items, removing items, calculating totals, applying discount codes (with a spy on the discount validation API), and handling empty cart edge cases.
FAQ
Try It Yourself
# 1. Install Jasmine globally or locally
npm install --save-dev jasmine
# 2. Initialize Jasmine
npx jasmine init
# 3. Create a module to test
cat > src/calculator.js << 'EOF'
class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
multiply(a, b) { return a * b; }
divide(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
}
}
module.exports = Calculator;
EOF
# 4. Create a test file
cat > spec/calculator-spec.js << 'EOF'
const Calculator = require("../src/calculator");
describe("Calculator", () => {
let calc;
beforeEach(() => {
calc = new Calculator();
});
it("should add two numbers", () => {
expect(calc.add(2, 3)).toEqual(5);
});
it("should throw on division by zero", () => {
expect(() => calc.divide(1, 0)).toThrowError("Cannot divide by zero");
});
});
EOF
# 5. Run tests
npx jasmineWhat’s Next
Explore more software quality tools:
| Topic | Description |
|---|---|
| https://tutorials.dodatech.com/tools/lodash/ | JavaScript utility library |
| https://tutorials.dodatech.com/tools/rxjs/ | Reactive programming |
Related topics to explore:
- JavaScript Testing
- CI/CD Testing
- Node.js Testing
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro