Jestの基本的な書き進め方
はじめに
こんにちは、アノテーションのなかたです。
今回は、Jestでテストについて学んでいるため、Jestにおけるテストコードの書き進め方をアウトプットしてみました。
このブログで書いていること
- テストコードの書き進め方
- テストスイート、マッチャー
このブログで書いていないこと
- セットアップやテストコマンドの実行方法
- モック
- テストに対する考え方
これらは後々、別のブログとして書こうと思います。
テスト対象とするソースコード
まず、テストする対象として銀行のアカウントを表す BankAccount クラスを定義します。
ソースコードは、各セクションでも書いているので、こちらを毎回参照する必要はありません。
BankAccount
export class BankAccount {
private balance: number; // 残高
private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴
public constructor(
public accountHolder: string,
initialBalance = 0,
) {
if (initialBalance < 0) {
throw new Error('Initial balance cannot be negative');
}
this.balance = initialBalance;
this.transactions = [];
}
// 入金処理
public deposit(amount: number): number {
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
this.balance += amount;
this.transactions.push({ type: 'deposit', amount, date: new Date() });
return this.balance;
}
// 出金処理
public withdraw(amount: number): number {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
if (amount > this.balance) {
throw new Error('Insufficient funds');
}
this.balance -= amount;
this.transactions.push({ type: 'withdraw', amount, date: new Date() });
return this.balance;
}
private printGetBalance(): void {
console.log('getBalance called');
}
// 残高を取得
public getBalance(): number {
this.printGetBalance();
return this.balance;
}
}
簡単なテストコードを書いてみる
BankAccount をテストするにはインスタンスを作成する必要がありそうです。
以下のように書いてみました。
import { BankAccount } from '#/bank';
const bankAccount = new BankAccount('user1', 1000);
こちらで実行してみると、エラーが発生します。
FAIL __tests__/bank.test.ts
● Test suite failed to run
Your test suite must contain at least one test.
at onResult (node_modules/@jest/core/build/TestScheduler.js:133:18)
at node_modules/@jest/core/build/TestScheduler.js:254:19
at node_modules/emittery/index.js:363:13
at Array.map (<anonymous>)
at Emittery.emit (node_modules/emittery/index.js:361:23)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 9.307 s
Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "BankAccount Test".
Your test suite must contain at least one test.
とありますので、少なくとも1つのテストコードを書く必要がありそうです。
test
(もしくはit
)というテストスイートに入れてあげることでテストが通る様になります。
import { BankAccount } from '#/bank';
test('BankAccount', () => {
const bankAccount = new BankAccount('user1', 1000);
});
// テスト結果
// PASS __tests__/bank.test.ts (6.626 s)
// ✓ BankAccount (3 ms)
// Test Suites: 1 passed, 1 total
// Tests: 1 passed, 1 total
// Snapshots: 0 total
// Time: 7.198 s, estimated 9 s
// Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "bankAccount$".
expect
+ toBe
による単純なテスト
では、簡単なテストを書いてみます。
getBalance 関数が簡単にテストできそうなので、こちらをテスト対象とします。
// ソースコード
// export class BankAccount {
// private balance: number; // 残高
// private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴
// public constructor(
// public accountHolder: string,
// initialBalance = 0,
// ) {
// if (initialBalance < 0) {
// throw new Error('Initial balance cannot be negative');
// }
// this.balance = initialBalance;
// this.transactions = [];
// }
// }
import { BankAccount } from '#/bank';
test('BankAccount', () => {
const bankAccount = new BankAccount('user1', 1000);
expect(bankAccount.getBalance()).toBe(1000);
});
// テスト結果
// PASS __tests__/bank.test.ts (9.702 s)
// ✓ BankAccount (5 ms)
// Test Suites: 1 passed, 1 total
// Tests: 1 passed, 1 total
// Snapshots: 0 total
// Time: 10.291 s
// Ran all test suites matching /\/home\/user\/dev\/__tests__\/bank\.test\.ts/i with tests matching "bankAccount$"
expect
と toBe
による組み合わせでテストできます。
toBe
は Jest における最も単純なマッチャーで、expect
に渡した値と照合するだけです。
Jestのマッチャー
Jest はマッチャーが豊富であるため、値によって適切なマッチャーを選ぶとシンプルで適切なテストコードが書けます。
マッチャー | 説明 | 例 |
---|---|---|
toBe(value) |
厳密な等価性をテストします(=== )。 |
expect(2 + 2).toBe(4); |
toEqual(value) |
オブジェクトや配列の内容が等しいかをテストします。 | expect({a: 1}).toEqual({a: 1}); |
toBeTruthy() |
値がtrue と評価されるかをテストします。 |
expect(1).toBeTruthy(); |
toContain(item) |
配列や文字列が特定の要素を含むかをテストします。 | expect([1, 2, 3]).toContain(2); |
toThrow() |
関数がエラーをスローするかをテストします。 | expect(() => { throw new Error(); }).toThrow(); |
など |
toThrow
による例外テスト
次に、コンストラクタでは例外が発生することをテストします。
toThrow
にエラーやエラーメッセージを渡すことでテストが可能です。
また、expectには() =>
などを用いて、関数を渡す必要があります。
// ソースコード
// export class BankAccount {
// private balance: number; // 残高
// private transactions: Array<{ type: string; amount: number; date: Date }>; // 取引履歴
// public constructor(
// public accountHolder: string,
// initialBalance = 0,
// ) {
// if (initialBalance < 0) {
// throw new Error('Initial balance cannot be negative');
// }
// this.balance = initialBalance;
// this.transactions = [];
// }
// }
import { BankAccount } from '#/bank';
test('BankAccount initialBalance greater than 0', () => {
expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
// expect(() => new BankAccount('user1', -1)).toThrow(Error); # Errorでも可
});
describe
で test
をまとめる
BankAccount に対するテストが増えてしまったため、describe
でまとめてみます。
import { BankAccount } from '#/bank';
// // Before: BankAccountに対するテスト群
// test('BankAccount getBalance', () => {
// const bankAccount = new BankAccount('user1', 1000);
// expect(bankAccount.getBalance()).toBe(1000);
// });
// test('BankAccount initialBalance greater than 0', () => {
// expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
// });
describe('BankAccount', () => {
test('getBalance', () => {
const bankAccount = new BankAccount('user1', 1000);
expect(bankAccount.getBalance()).toBe(1000);
});
test('initialBalance greater than 0', () => {
expect(() => new BankAccount('user1', -1)).toThrow('Initial balance cannot be negative');
});
});
describe
スコープ内でオブジェクトを使い回す
また、describe
はスコープを共有するため、オブジェクトを使い回せます。
試しに コンストラクタに渡す accountHolder を使い回してみます。
import { BankAccount } from '#/bank';
describe('BankAccount', () => {
const accountHolder = 'user1'; // describeスコープに変数を宣言
test('getBalance', () => {
const bankAccount = new BankAccount(accountHolder, 1000);
expect(bankAccount.getBalance()).toBe(1000);
});
test('initialBalance greater than 0', () => {
expect(() => new BankAccount(accountHolder, -1)).toThrow('Initial balance cannot be negative');
});
});
モックデータが重複してしまい、コードが長くなってしまう際に便利です。
しかし、他テストによりオブジェクトの状態が変化してしまう可能性を考慮する必要があります。
参考
アノテーション株式会社について
アノテーション株式会社はクラスメソッドグループのオペレーション専門特化企業です。サポート・運用・開発保守・情シス・バックオフィスの専門チームが、最新 IT テクノロジー、高い技術力、蓄積されたノウハウをフル活用し、お客様の課題解決を行っています。当社は様々な職種でメンバーを募集しています。「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、アノテーション株式会社 採用サイトをぜひご覧ください。