Jasmine

テストユニットテストJavaScriptBDDスパイ

GitHub概要

jasmine/jasmine

Simple JavaScript testing framework for browsers and node.js

スター15,816
ウォッチ436
フォーク2,244
作成日:2008年12月2日
言語:JavaScript
ライセンス:MIT License

トピックス

jasminejavascripttddtesting

スター履歴

jasmine/jasmine Star History
データ取得日時: 2025/7/20 02:18

テストツール

Jasmine

概要

JasmineはJavaScript用のBDD(Behavior Driven Development)スタイルのテストフレームワークです。「動作が仕様である」という思想に基づき、自然言語に近い記述でテストを書くことができます。Node.jsとブラウザの両方で動作し、外部依存関係を持たないスタンドアローンな設計により、セットアップが簡単で即座に使い始めることができます。豊富なマッチャーと強力なスパイ機能により、読みやすく保守しやすいテストコードの作成を支援します。

詳細

Jasmineの特徴は、その表現力豊かなBDD構文にあります。describe、it、beforeEach、afterEachなどの関数を使用して、テストの構造を階層的に記述し、テストケースの意図を明確に表現できます。組み込みのマッチャー(toBe、toEqual、toContain、toBeCloseTo等)により、様々な条件を自然な英語に近い形で記述できます。

スパイ機能は特に強力で、関数の呼び出し履歴追跡、戻り値の制御、例外の発生シミュレーションなどが可能です。非同期テストサポートでは、Promise、async/await、コールバックすべてに対応し、タイマー関数のモック機能も提供しています。カスタムマッチャーの作成により、プロジェクト固有の検証ロジックを再利用可能な形で定義できます。

メリット・デメリット

メリット

  • BDD構文: 読みやすく理解しやすいテスト記述
  • スタンドアローン: 外部依存なしで即座に利用開始可能
  • 豊富なマッチャー: 多様な検証パターンに対応
  • 強力なスパイ機能: 関数呼び出しの詳細な監視とモック
  • 非同期サポート: Promise、async/await、コールバック対応
  • ブラウザ・Node.js対応: マルチプラットフォーム実行

デメリット

  • アサーションライブラリ統合: 他のアサーションライブラリとの併用が難しい
  • カスタマイズ制限: フレームワーク全体の動作変更が困難
  • エラーメッセージ: 他フレームワークと比べて改善の余地あり
  • 生態系の規模: Jest等と比べてプラグインやツールが少ない

参考ページ

書き方の例

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!");
  });
});

基本テスト

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

スパイ

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)
        });
      }
    };
    
    // fetchをスパイに置き換え
    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();
  });
});

非同期テスト

describe("Async operations", function() {
  
  // Promiseベースのテスト
  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);
  });
  
  // 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");
  });
  
  // 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));
  });
  
  // タイマーのモック
  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();
  });
});

セットアップ・ティアダウン

describe("Database operations", function() {
  let database;
  let testData;
  
  beforeAll(function(done) {
    // 全テスト開始前の一回だけ実行
    console.log("Setting up test database...");
    database = new TestDatabase();
    database.connect().then(done).catch(done.fail);
  });
  
  afterAll(function(done) {
    // 全テスト終了後の一回だけ実行
    console.log("Cleaning up test database...");
    database.disconnect().then(done).catch(done.fail);
  });
  
  beforeEach(function(done) {
    // 各テスト前に実行
    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) {
    // 各テスト後に実行
    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);
  });
});

高度な機能

describe("Advanced Jasmine features", function() {
  
  // カスタムマッチャー
  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);
  });
  
  // 非対称マッチャー
  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)
    );
  });
  
  // 条件付きテスト
  it("should run conditionally", function() {
    if (typeof window === 'undefined') {
      pending("This test requires browser environment");
    }
    
    expect(window).toBeDefined();
  });
  
  // テストのスキップ
  xit("should be skipped", function() {
    // このテストはスキップされる
    expect(true).toBe(false);
  });
  
  // フォーカステスト(開発時のみ)
  fit("should run only this test", function() {
    expect(true).toBe(true);
  });
  
  // データ駆動テスト
  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);
    });
  });
  
  // ネストしたdescribe
  describe("when user is logged in", function() {
    beforeEach(function() {
      // ログイン状態のセットアップ
    });
    
    describe("and has admin privileges", function() {
      beforeEach(function() {
        // 管理者権限のセットアップ
      });
      
      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);
      });
    });
  });
  
  // スパイの高度な使用法
  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();
  });
});