Closed11

Jest再入門

水無瀬水無瀬

導入

yarn + typescriptの前提。
それぞれの導入方法については割愛。

# jest導入
$ yarn add -D jest ts-jest @types/jest

# 設定ファイル作成
$ yarn ts-jest config:init
水無瀬水無瀬

テスト作成

テスト対象の関数作成

引数で数値を2つ受け取って足し算する関数

export function sum(a: number, b: number) {
  return a + b;
}

テストコード作成

describe('sum', () => {
  it('2 + 3 = 5', () => {
    const result = sum(2, 3);
    // 関数の結果と一致することを確認
    expect(result).toBe(5);
  });
});
水無瀬水無瀬

エラーになることのテスト

実装

export function division(dividend: number, divisor: number) {
  if (divisor === 0) {
    throw new Error('Do not divide by 0.');
  }
  return dividend / divisor;
}

テストコード

describe('division', () => {
  it('6 / 0 = error!', () => {
    // エラーになることの確認。
    // そのまま実行するとエラーで落ちるので関数でラップしてあげる
    // ThrowErrorの引数にErrorを渡して上げると内容が同じか比較してくれる
    expect(() => division(6, 0)).toThrowError(new Error('Do not divide by 0.'));
  });
});
水無瀬水無瀬

オブジェクトの比較

実装

export function getUserById(userId: string) {
  return {id: userId, name: 'Taro', age: 20};
}

テストコード

describe('getUserById', () => {
  it('取得できる場合', () => {
    const result = getUserById('user1');
    // Objectの値の確認はtoEqualを使う
    expect(result).toEqual({id: 'user1', name: 'Taro', age: 20});
    // toBeで比較するとObjec.isでの比較になるため↓は同値にならずテストがコケる
    // expect(result).toBe({id: 'user1', name: 'Taro', age: 20});
  });
});
水無瀬水無瀬

その他のmatcher

describe('matcher sample.', () => {
  it('真偽値', () => {
    // nullであることの確認
    expect(null).toBeNull();
    // null以外だと失敗する
    // expect(undefined).toBeNull();
    
    // undefinedでの確認
    expect(undefined).toBeUndefined();
    // 同じくundefined以外だと失敗する
    // expect(null).toBeUndefined();

    // toBeUndefinedの反対
    expect('hoge').toBeDefined();
    // toBeUndefinedの反対なのでnullは通る
    expect(null).toBeDefined();
    // undefinedは失敗する
    // expect(undefined).toBeDefined();

    // truthyな値であることの確認
    expect('hoge').toBeTruthy();
    expect(1).toBeTruthy();
    // 空文字,0,nullなどfalthyなものは通らない
    // expect('').toBeTruthy();
    // expect(0).toBeTruthy();
    // expect(null).toBeTruthy();

    // falthyな値であることの確認
    expect(0).toBeFalsy();
    expect('').toBeFalsy();
    expect(null).toBeFalsy();
    expect(undefined).toBeFalsy();
    // truthyな値は通らない
    // expect('hoge').toBeFalsy();
  });
  it('数値', () => {
    // 3より大きい
    expect(4).toBeGreaterThan(3);
    // 3.5以上
    expect(4).toBeGreaterThanOrEqual(3.5);
    // 4以上
    expect(4).toBeGreaterThanOrEqual(4);
    // 5より小さい
    expect(4).toBeLessThan(5);
    // 4.5以下
    expect(4).toBeLessThanOrEqual(4.5);
    // 4以下
    expect(4).toBeLessThanOrEqual(4);
    // 数値においてはtoEqualとtoBeは同じ
    expect(4).toEqual(4);
    expect(4).toBe(4);

    const sum = 0.1 + 0.2;
    // 浮動小数点の場合、丸め誤差があり一致しない
    // expect(sum).toBe(0.3);
    // 浮動小数点の確認はtoBeCloseToを使う
    expect(sum).toBeCloseTo(0.3);
  });
  it('文字列', () => {
    // 文字列は正規表現で確認ができる
    expect('HelloWorld').toMatch(/^Hello.+$/);
    // デフォだと部分一致する
    expect('HelloWorld').toMatch(/orl/);
  });

  it('配列、反復可能オブジェクト', () => {
    const list = ['hoge', 'huga', 'piyo'];
    expect(list).toContain('hoge');
    expect(new Set(list)).toContain('huga');
  });

  it('マッチしない場合', () => {
    // notを挟めばできる
    expect('hoge').not.toBe('huga');
    expect('hoge').not.toBeFalsy();
  });
});

他のMatcherは↓参照。
Expect · Jest

水無瀬水無瀬

非同期処理のテスト

実装

function asyncResolveClient(): Promise<string> {
  return Promise.resolve('OK');
}

function asyncRejectClient(): Promise<string>{
  return Promise.reject('Error');
}

export async function resolveSample(): Promise<string> {
  return await asyncResolveClient();
}

export async function rejectSample(): Promise<string> {
  return await asyncRejectClient();
}

テストコード

describe('async function', () => {

  it('非同期の確認', () => {
    // resolvesをつけることでPromiseがresolveされるまで待つ
    expect(resolveSample()).resolves.toBe('OK');
    // 何もつけないとPromiseが返ってきてすぐ比較されるので一致しない
    // expect(resolveSample()).toBe('OK');
  
    // rejectsをつけることでPromiseがrejectされるまで待つ
    expect(rejectSample()).rejects.toBe('Error');
  });
  it('asyncを使った場合', async () => {
    // 結果を待ってから取得した値でチェックする
    const result = await resolveSample();
    expect(result).toBe('OK');
  });
  it('asyncを使った場合(エラー時)', async () => {
    // テストが間違ってエラーにならない場合、テストが通ってしまうため想定した数assertionが呼ばれることのチェック
    expect.assertions(1);
    // rejectされてErrorになるためcatchしてあげる必要がある
    try {
      await rejectSample();
    } catch (e) {
      expect(e).toMatch('Error');
    }
  });
});
水無瀬水無瀬

SetupとTeardown

テストの最初と最後に何らかの処理をはさみたい場合。
モック化、そのリセットなどでやりたいことがあると思う。

beforeEach(() => {
  console.log('このファイル内のテストケースの前に実行される');
});

describe('setup and teardown sample.', () => {
  // describeの中に書くとこのdescribe内でのスコープになる(このdescribe内のテストが実行される時に実行される)
  beforeAll(() => {
    console.log('全テストケースの前に実行される');
  });
  afterAll(() => {
    console.log('全テストケースの後に実行される');
  });
  beforeEach(() => {
    console.log('各テストケースの前に実行される');
  });
  afterEach(() => {
    console.log('各テストケースの後に実行される');
  });

  it('テスト1', () => {
    console.log('テスト1');  
  });
  it('テスト2', () => {
    console.log('テスト2');  
  });

  it('テスト3', () => {
    console.log('テスト3');  
  });

});
水無瀬水無瀬

exportした関数のmock化

実装と別ファイルに定義した関数をimportして使う場合

実装

export function multiplication(a: number, b: number) {
  return a * b;
}
// ↑のファイルをimport
import { multiplication } from './sample';
export function twice(a: number) {
  return multiplication(a, 2);
}

テストコード

jest.mock('multiplicationがあるファイルまでのパス', () => {
  // 一部のみモック化したいので元の実装を持っておく
  const original = jest.requireActual('multiplicationがあるファイルまでのパス');
  return {
    ...original,
    // テストで使う関数のみ固定値が返るようにする
    multiplication: jest.fn().mockReturnValue(10)
  }
});
describe('twice', () => {
  it('test', () => {
    const result = twice(5);
    expect(result).toBe(10);
  });
});

水無瀬水無瀬

Classのmock化

実装

export class Calcurator {
  public sum(a: number, b: number) {
    return a + b;
  }
}
// テスト対象
export function add2(a: number) {
  return new Calcurator().sum(a, 2);
}

テストコード

import { Calcurator } from './パス;
// mockImplementationがエラー吐くので必要
import { mocked } from 'ts-jest/utils';

jest.mock('../../../src/functions/CalcuratorClass');
describe('sum', () => {
  it('test', () => {
    mocked(Calcurator).mockImplementation(() => {
      return {
        sum: () => {
          return 5;
        },
      };
    });
    const result = add2(3);
    expect(result).toBe(5)
  });
});
水無瀬水無瀬

snapshotsテスト

create-react-appの初期状態からスタート。
ただし、デフォだとjest落ちるので色々修正する。

svgのimportをやめる

ライブラリを使えば解決できるがそこまでsvgのimportに拘らないので普通にcomponent化する。
基本はsvgファイルの中身コピペ。CSSを当てる都合classNameだけ追加。

import React from 'react';
export const Logo: React.FC = () => (<svg className="App-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="長いので割愛。初期値のコピペ" /><circle cx="420.9" cy="296.5" r="45.7" /><path d="M520.5 78.1z" /></g></svg>)

App.tsxから↑のファイルを読み込むようにすればOK。

import React from 'react';
import { Logo } from './Logo';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Logo/>
        {/* 以下初期値と同じ */}
      </header>
    </div>
  );
}
export default App;

cssをimportできるようにする

こちらもデフォだと落ちるのでなんとかする。
公式サイトにあったので同じ様に対応する。

ライブラリ追加

$ yarn add -D identity-obj-proxy 

jest.config.js修正

module.exports = {
  // ここを追加
  moduleNameMapper: {
    '\\.(css|less)$': 'identity-obj-proxy'
  }
};

テストコード

snapshotsテスト用にライブラリを追加。

$ yarn add -D react-test-renderer @types/react-test-renderer

テストコードは下記の通り。

import React from 'react';
import renderer from 'react-test-renderer';
import App from '../../src/App';

test('renders learn react link', () => {
  const result = renderer.create(<App />).toJSON();
  expect(result).toMatchSnapshot();
});

テストを実行して__snapshots__フォルダにテストコードファイル.snapが作成されていればOK。

参考リンク

このスクラップは2021/02/11にクローズされました