今回やること
この記事では、計算処理と副作用のある処理を分けます。
DB保存やメール送信を計算ロジックから分けると、テストと変更がしやすくなります。
前提条件
- 関数とasync/awaitが読める
- DB保存やメール送信が外部への影響を持つことを理解している
- テストしやすいコードを書きたい
Step 1: 副作用が混ざったコードを見る
async function checkout(cart: Cart) {
let total = 0;
for (const item of cart.items) {
total += item.price * item.quantity;
}
await orderRepository.save({
userId: cart.userId,
total,
});
await mailer.send(cart.userEmail, `Total: ${total}`);
return total;
}
合計計算、DB保存、メール送信が1つの関数に入っています。
Step 2: 計算処理を切り出す
function calculateCartTotal(items: CartItem[]) {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
この関数は外部へ影響しません。同じ入力なら同じ結果になります。
Step 3: 副作用を外側に残す
async function checkout(cart: Cart) {
const total = calculateCartTotal(cart.items);
await orderRepository.save({
userId: cart.userId,
total,
});
await mailer.send(cart.userEmail, `Total: ${total}`);
return total;
}
合計計算は分離できました。DB保存とメール送信は、外部I/Oとして外側の流れに残しています。
Step 4: テストしやすさを確認する
calculateCartTotal は、DBやメールを用意しなくてもテストできます。
const total = calculateCartTotal([
{ price: 1000, quantity: 2 },
{ price: 500, quantity: 1 },
]);
console.log(total); // 2500
計算ロジックの確認が簡単になりました。
Step 5: 副作用の名前を明確にする
副作用を含む関数には、動作が分かる名前を付けます。
| 名前 | 分かること |
|---|---|
saveOrder | DBなどへ保存する |
sendReceiptMail | メールを送る |
writeAuditLog | ログを書く |
fetchUser | 外部から取得する |
名前で副作用が分かると、呼び出し側が注意できます。
よくあるエラー
| 状況 | 原因 | 対応 |
|---|---|---|
| テストで本物のDBが必要 | 計算と保存が混ざっている | 計算関数を切り出す |
| 同じ入力で結果が変わる | 外部状態に依存している | 依存を引数にする |
| メールが誤送信される | テストと本番処理が分離されていない | モックやFakeを使う |
| 関数名から副作用が分からない | 名前が曖昧 | save / send / fetch を入れる |
まとめ
副作用のある処理は必要ですが、計算ロジックと混ざるとテストしにくくなります。まず純粋な計算を切り出し、外部I/Oは分かる名前で閉じ込めましょう。