開発の現場では、既存のテストコードから仕様を読み解く機会がよく訪れます。そのようなとき、テスト対象の仕様やテストの意図を読み解きやすいテストとそうではないテストがあることに気付きます。今回はテストコードの読み解きやすさに寄与する要素を考えます。
認知資源と認知負荷
人間は何かを読み解くときに脳のリソース
人間が何かを読み解くときに認知資源が何にどのくらい割かれているかという概念を
これらの概念を自動テストの文脈に当てはめると、テストコードの認知負荷とは、テストコードを読み解く際に読み手の認知資源が何にどのくらい割かれているかであると言えます。読み手がテスト対象の設計やテストの意図を読み解くことに認知資源を割けるようにしたいものです。そのためには、テスト対象とは本来関係ない要素に読み手が認知資源を割かなければならないような書き方を避けましょう。そこで重要なのがテストの名前、構造、情報量です。
アンチパターン1:情報が少なすぎる
実際に例を挙げてみましょう。みなさんは、コンビニのレシート発行システムの開発にあとから参加したばかりのソフトウェアエンジニアだとします。軽減税率が関わる税額の計算という複雑な対象領域の理解に取り組む過程で、既存のテストコードを読むときを想像してみてください。
test('税額計算結果が正しいこと', () => {
addTea(2);
addBeer(3);
const { reduced, standard } = inv.total().tax;
assert.equal(reduced, 19);
assert.equal(standard, 40);
});
このコードからは、inv
変数はどこから来たのか、addTea
やaddBeer
関数は何をしているのか、検証の期待値19
や40
の狙いなどが読み取れません。事前状態の準備と検証内容の因果関係を読み解けないのは、準備の一部がテスト関数の外で行われているからです。テスト関数内から得られる情報が少ないため、認知負荷が高いのです。
アンチパターン2:情報が多すぎる
では、テスト関数外で行われている準備をすべてテスト関数の中に書けばよいかというと、そうでもありません。テストの意図とは直接関係ない要素がテスト関数の中に登場すると、読み手の認知資源がそれら不要な要素にも割かれてしまうからです。
次のコードでは、事前状態の準備のところにも結果の検証のところにも、税額計算と関係なさそうな要素が入っています。何がこのテストにとって大事なのか、読み手が取捨選択しなければなりません。しかもよく見てみると、テストの意図や結果の妥当性という肝心なところの情報は足りません。これでは読み手の認知負荷が不必要に高まってしまいます。
test('税額計算結果が正しいこと', () => {
const inv = new SimplifiedInvoice({
confirmationNumber: 'XXXX-XXXXX-XXXXX',
paymentMethod: 'cash',
paymentDate: new Date()
});
inv.add(new Item('技評茶', 130, {
type: 'beverage',
name: '飲料',
tax_type: 'reduced'
}), 2);
inv.add(new Item('技評酒', 150, {
type: 'liquor',
name: '酒類',
tax_type: 'standard'
}), 3);
const res = inv.total();
assert.equal(res.total, 710);
assert.equal(res.sub_total.reduced, 260);
assert.equal(res.sub_total.standard, 450);
assert.equal(res.tax_included, true);
assert.equal(res.tax.total, 59);
assert.equal(res.tax.reduced, 19);
assert.equal(res.tax.standard, 40);
});
テストの名前、構造、情報量
ここまでの問題を踏まえて、テストの名前、構造、情報量に改善を加えてみましょう。
テストコードを書く際には、テストの名前が最も重要です。意図を込めて説明的に書きましょう。そうすれば、テスト実行結果のレポートは仕様の一覧となり、テスト失敗時には何が失敗しているか一目瞭然となり、テストコードを読む際には、対象のテスト関数を探し、その中身まで読みに行くべきかどうかの判断材料となります。
テスト名を
テスト関数の中を読みに行くべきと判断したら、次に構造を見ます。読み手はテスト関数の中を準備/
describe('適格簡易請求書の消費税額計算', () => {
test('税込価額を税率ごとに区分して合計した金額に対して税額を計算し、端数は切り捨てること', () => {
// 準備(Arrange, Given)
// 軽減税率の商品(飲料)と標準税率の商品(酒類)を
// それぞれ複数購入している事前状態において
const inv = createSimplifiedInvoice();
inv.add(new Item('技評茶', 130, 飲料), 2);
inv.add(new Item('技評酒', 150, 酒類), 3);
// 実行(Act, When)
// 合計、小計、消費税額などの計算を行った場合
const total = inv.total();
// 検証(Assert, Then)
// 税率ごとの合計に対して1回ずつ切り捨てられること
assert.deepEqual(total.tax, {
reduced: 19, // (130*2)*(8/108) => 19.25円
standard: 40, // (150*3)*(10/110) => 40.90円
total: 59 // 消費税額は合算するだけ
});
});
});
テスト関数内の情報量は、createSimplifiedInvoice
)、テストの意図を伝えるために必要な要素はテスト関数内に必ず登場させます。期待値の計算のところは、計算式をコメントで添えています。期待値の計算をコードで行ってしまうと
おわりに
テストの名前、構造、情報量を工夫し、読み手が対象の理解に集中できるようにしましょう。