Jasmine
GitHub Overview
jasmine/jasmine
Simple JavaScript testing framework for browsers and node.js
Topics
Star History
Testing Tool
Jasmine
Overview
Jasmine is a BDD (Behavior Driven Development) style testing framework for JavaScript. Based on the philosophy that "behavior is specification," it allows you to write tests in descriptions close to natural language. It runs on both Node.js and browsers with a standalone design that has no external dependencies, making it easy to set up and start using immediately. Rich matchers and powerful spy functionality support the creation of readable and maintainable test code.
Details
Jasmine's strength lies in its expressive BDD syntax. Using functions like describe, it, beforeEach, and afterEach, you can hierarchically describe test structure and clearly express test case intentions. Built-in matchers (toBe, toEqual, toContain, toBeCloseTo, etc.) allow you to describe various conditions in forms close to natural English.
The spy functionality is particularly powerful, enabling function call history tracking, return value control, and exception simulation. Asynchronous test support covers Promises, async/await, and callbacks, also providing timer function mocking capabilities. Custom matcher creation allows you to define project-specific validation logic in reusable forms.
Pros and Cons
Pros
- BDD Syntax: Readable and understandable test descriptions
- Standalone: Ready to use immediately without external dependencies
- Rich Matchers: Support for diverse validation patterns
- Powerful Spy Functionality: Detailed monitoring and mocking of function calls
- Async Support: Support for Promises, async/await, and callbacks
- Cross-Platform: Multi-platform execution on browsers and Node.js
Cons
- Assertion Library Integration: Difficult to use with other assertion libraries
- Customization Limitations: Hard to modify overall framework behavior
- Error Messages: Room for improvement compared to other frameworks
- Ecosystem Size: Fewer plugins and tools compared to Jest and others
Reference Pages
Code Examples
Hello World
// package.json
{
"scripts": {
"test": "jasmine"
},
"devDependencies": {
"jasmine": "^5.0.0"
}
}
// spec/hello-world.spec.js
describe("Hello World", function() {
it("should return greeting message", function() {
function greet(name) {
return `Hello, ${name}!`;
}
expect(greet("Jasmine")).toBe("Hello, Jasmine!");
});
});
Basic Testing
describe("Calculator", function() {
let calculator;
beforeEach(function() {
calculator = {
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; },
multiply: function(a, b) { return a * b; },
divide: function(a, b) {
if (b === 0) throw new Error("Division by zero");
return a / b;
}
};
});
describe("when adding numbers", function() {
it("should add positive numbers correctly", function() {
expect(calculator.add(2, 3)).toBe(5);
});
it("should handle negative numbers", function() {
expect(calculator.add(-1, 1)).toBe(0);
});
it("should handle floating point precision", function() {
expect(calculator.add(0.1, 0.2)).toBeCloseTo(0.3, 2);
});
});
describe("when dividing numbers", function() {
it("should divide numbers correctly", function() {
expect(calculator.divide(10, 2)).toBe(5);
});
it("should throw error for division by zero", function() {
expect(function() {
calculator.divide(10, 0);
}).toThrow(new Error("Division by zero"));
});
});
afterEach(function() {
calculator = null;
});
});
Spies
describe("Spy functionality", function() {
let userService;
let apiSpy;
beforeEach(function() {
userService = {
fetchUser: function(id) {
return fetch(`/api/users/${id}`);
},
updateUser: function(user) {
return fetch('/api/users', {
method: 'PUT',
body: JSON.stringify(user)
});
}
};
// Replace fetch with spy
apiSpy = spyOn(window, 'fetch');
});
it("should call correct API endpoint", function() {
const mockResponse = new Response(JSON.stringify({id: 1, name: "John"}));
apiSpy.and.returnValue(Promise.resolve(mockResponse));
userService.fetchUser(1);
expect(apiSpy).toHaveBeenCalledWith('/api/users/1');
expect(apiSpy).toHaveBeenCalledTimes(1);
});
it("should track function calls with arguments", function() {
const user = {id: 1, name: "Jane", email: "[email protected]"};
apiSpy.and.returnValue(Promise.resolve(new Response()));
userService.updateUser(user);
expect(apiSpy).toHaveBeenCalledWith('/api/users', {
method: 'PUT',
body: JSON.stringify(user)
});
});
it("should create standalone spy", function() {
const callback = jasmine.createSpy('callback');
callback('arg1', 'arg2');
callback('arg3');
expect(callback).toHaveBeenCalledWith('arg1', 'arg2');
expect(callback).toHaveBeenCalledWith('arg3');
expect(callback.calls.count()).toBe(2);
expect(callback.calls.argsFor(0)).toEqual(['arg1', 'arg2']);
});
it("should create spy object with multiple methods", function() {
const mockService = jasmine.createSpyObj('UserService',
['getUser', 'createUser', 'deleteUser']);
mockService.getUser.and.returnValue({id: 1, name: "Test"});
const result = mockService.getUser(1);
expect(result).toEqual({id: 1, name: "Test"});
expect(mockService.getUser).toHaveBeenCalledWith(1);
expect(mockService.createUser).not.toHaveBeenCalled();
});
});
Async Testing
describe("Async operations", function() {
// Promise-based testing
it("should handle promises", function(done) {
const asyncFunction = function() {
return new Promise(function(resolve) {
setTimeout(function() {
resolve("async result");
}, 100);
});
};
asyncFunction().then(function(result) {
expect(result).toBe("async result");
done();
}).catch(done.fail);
});
// Testing with async/await
it("should handle async/await", async function() {
const asyncFunction = async function() {
return new Promise(resolve => {
setTimeout(() => resolve("async result"), 100);
});
};
const result = await asyncFunction();
expect(result).toBe("async result");
});
// Testing with expectAsync
it("should use expectAsync for promises", async function() {
const promise = Promise.resolve("resolved value");
await expectAsync(promise).toBeResolvedTo("resolved value");
});
it("should test promise rejections", async function() {
const rejectingPromise = Promise.reject(new Error("Something went wrong"));
await expectAsync(rejectingPromise).toBeRejectedWith(jasmine.any(Error));
});
// Timer mocking
it("should mock timers", function() {
jasmine.clock().install();
let callback = jasmine.createSpy("callback");
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
jasmine.clock().tick(1000);
expect(callback).toHaveBeenCalled();
jasmine.clock().uninstall();
});
});
Setup and Teardown
describe("Database operations", function() {
let database;
let testData;
beforeAll(function(done) {
// Execute once before all tests
console.log("Setting up test database...");
database = new TestDatabase();
database.connect().then(done).catch(done.fail);
});
afterAll(function(done) {
// Execute once after all tests
console.log("Cleaning up test database...");
database.disconnect().then(done).catch(done.fail);
});
beforeEach(function(done) {
// Execute before each test
testData = {
users: [
{id: 1, name: "User 1", email: "[email protected]"},
{id: 2, name: "User 2", email: "[email protected]"}
]
};
database.seed(testData).then(done).catch(done.fail);
});
afterEach(function(done) {
// Execute after each test
database.clear().then(done).catch(done.fail);
});
it("should create new user", function(done) {
const newUser = {name: "New User", email: "[email protected]"};
database.createUser(newUser).then(function(createdUser) {
expect(createdUser.id).toBeDefined();
expect(createdUser.name).toBe("New User");
expect(createdUser.email).toBe("[email protected]");
done();
}).catch(done.fail);
});
it("should find existing users", function(done) {
database.findUsers().then(function(users) {
expect(users.length).toBe(2);
expect(users[0].name).toBe("User 1");
expect(users[1].name).toBe("User 2");
done();
}).catch(done.fail);
});
it("should update user information", async function() {
const updatedData = {name: "Updated User"};
const result = await database.updateUser(1, updatedData);
expect(result.name).toBe("Updated User");
expect(result.id).toBe(1);
});
});
Advanced Features
describe("Advanced Jasmine features", function() {
// Custom matchers
beforeEach(function() {
jasmine.addMatchers({
toBeWithinRange: function() {
return {
compare: function(actual, floor, ceiling) {
const result = {};
result.pass = actual >= floor && actual <= ceiling;
if (result.pass) {
result.message = `Expected ${actual} not to be within range ${floor} - ${ceiling}`;
} else {
result.message = `Expected ${actual} to be within range ${floor} - ${ceiling}`;
}
return result;
}
};
}
});
});
it("should use custom matcher", function() {
expect(15).toBeWithinRange(10, 20);
expect(25).not.toBeWithinRange(10, 20);
});
// Asymmetric matchers
it("should use asymmetric matchers", function() {
expect("Hello World").toEqual(jasmine.stringMatching(/World/));
expect({name: "John", age: 30}).toEqual(jasmine.objectContaining({
name: jasmine.any(String)
}));
const spy = jasmine.createSpy();
spy(12, function() { return true; });
expect(spy).toHaveBeenCalledWith(
jasmine.any(Number),
jasmine.any(Function)
);
});
// Conditional testing
it("should run conditionally", function() {
if (typeof window === 'undefined') {
pending("This test requires browser environment");
}
expect(window).toBeDefined();
});
// Skipping tests
xit("should be skipped", function() {
// This test will be skipped
expect(true).toBe(false);
});
// Focused tests (development only)
fit("should run only this test", function() {
expect(true).toBe(true);
});
// Data-driven testing
const testCases = [
{input: "hello", expected: 5},
{input: "world", expected: 5},
{input: "test", expected: 4}
];
testCases.forEach(function(testCase) {
it(`should calculate length of "${testCase.input}"`, function() {
expect(testCase.input.length).toBe(testCase.expected);
});
});
// Nested describe blocks
describe("when user is logged in", function() {
beforeEach(function() {
// Login state setup
});
describe("and has admin privileges", function() {
beforeEach(function() {
// Admin privileges setup
});
it("should allow admin operations", function() {
expect(true).toBe(true);
});
});
describe("and has regular privileges", function() {
it("should allow regular operations", function() {
expect(true).toBe(true);
});
});
});
// Advanced spy usage
it("should track property access", function() {
const obj = {
get currentValue() {
return this._value;
},
set currentValue(val) {
this._value = val;
}
};
spyOnProperty(obj, 'currentValue', 'get').and.returnValue(10);
spyOnProperty(obj, 'currentValue', 'set');
obj.currentValue = 5;
const value = obj.currentValue;
expect(obj.currentValue).toBe(10);
expect(obj.currentValue).toHaveBeenCalled();
});
});