Jasmine

TestingUnit TestingJavaScriptBDDSpy

GitHub Overview

jasmine/jasmine

Simple JavaScript testing framework for browsers and node.js

Stars15,816
Watchers436
Forks2,244
Created:December 2, 2008
Language:JavaScript
License:MIT License

Topics

jasminejavascripttddtesting

Star History

jasmine/jasmine Star History
Data as of: 7/20/2025, 02:18 AM

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();
  });
});