AIにJavaScriptを直してもらった後、「見た目はすっきりしたけれど、本当に前と同じ動きなのか」と迷うことがあります。そんなときは、修正前に入力と期待する出力をテストとして保存し、修正後に同じテストを実行します。この記事では、架空の学習記録の集計機能を使い、追加ライブラリなしで確かめる方法を紹介します。
「読みやすくなった」と「正しく動く」は別の確認
リファクタリングとは、利用者から見える動作を保ちながら内部のコード構造を見直すことです。画面の機能追加や計算結果の変更まで含める場合は、単なる内部整理とは区別します。
ここで確認するのは、実行に成功したかだけではありません。「完了済みの学習記録だけを集計する」という決めごとが、書き換え後も守られているかです。テストは入力例ごとに期待結果を明示できるため、勘ではなく比較で判断できます。
用語解説:回帰テスト
修正で過去に動いていた機能が壊れていないかを確かめるテスト。すべての不具合を見つけられる保証ではなく、用意した条件の範囲で異常を検知します。
(リファクタリングの意味については『Martin FowlerによるRefactoringの解説』をご参照ください)
練習する機能と、変えてはいけない約束
架空の学習メモを考えます。1件のデータには「学習時間」と「完了したかどうか」が入っています。`done`が真偽値の`true`になっている記録だけを数え、その時間を合計します。ここでは`minutes`が0以上の数値で渡されることを前提にします。
| 入力の状態 | 数える件数 | 合計時間 |
|---|---|---|
| 25分と15分が完了、50分は未完了 | 2件 | 40分 |
| 未完了だけ | 0件 | 0分 |
| 記録がない | 0件 | 0分 |
| 完了した記録の時間が0 | 1件 | 0分 |
| `done`が文字列の`true` | 対象外 | 対象外 |
この約束を後から変更するなら、期待結果も見直す必要があります。AIが気を利かせて「`done`が文字列でも完了と扱う」よう変更すると、今回は元の仕様から外れます。
準備:Node.jsで動く小さな関数を保存する
Node.jsはJavaScriptをブラウザ以外でも実行できる環境です。Node.js 22以降を用意し、ターミナルで`node –version`を実行して確認してください。仕事用のファイルを直接書き換えず、空の練習フォルダに`study-summary.cjs`を作ります。
function summarizeStudy(records) {
let minutes = 0;
let completed = 0;
for (const record of records) {
if (record.done === true) {
completed += 1;
minutes += record.minutes;
}
}
return { completed, minutes };
}
module.exports = { summarizeStudy };
この処理は配列を先頭から読み、`done === true`のときだけ件数と時間を増やします。戻り値は`{ completed, minutes }`という2つの数字を持つオブジェクトです。
テストを書く:最初に期待する答えを固定する
同じフォルダに`study-summary.test.cjs`を作り、次のコードを保存します。`node:test`はNode.jsに組み込まれたテスト機能で、`node:assert/strict`は実際の値と期待値が一致するか確かめるための機能です。別途`npm install`する必要はありません。
const test = require('node:test');
const assert = require('node:assert/strict');
const { summarizeStudy } = require('./study-summary.cjs');
test('完了済みだけを合計する', () => {
const records = [
{ done: true, minutes: 25 },
{ done: false, minutes: 50 },
{ done: true, minutes: 15 },
];
assert.deepStrictEqual(summarizeStudy(records), { completed: 2, minutes: 40 });
});
test('すべて未完了なら0件・0分', () => {
assert.deepStrictEqual(summarizeStudy([{ done: false, minutes: 20 }]),
{ completed: 0, minutes: 0 });
});
test('空の配列なら0件・0分', () => {
assert.deepStrictEqual(summarizeStudy([]), { completed: 0, minutes: 0 });
});
test('完了0分も1件として数える', () => {
assert.deepStrictEqual(summarizeStudy([{ done: true, minutes: 0 }]),
{ completed: 1, minutes: 0 });
});
test('文字列のdoneは完了とみなさない', () => {
assert.deepStrictEqual(summarizeStudy([
{ done: 'true', minutes: 99 }, { done: true, minutes: 10 },
]), { completed: 1, minutes: 10 });
});
ターミナルで次を実行します。テスト対象とテストファイルは同じフォルダに置いてください。
node --test study-summary.test.cjs
このサンプルでは5件のテストがPASSすることを確認済みです。画面上の表示形式はNode.jsのバージョンで多少異なります。重要なのは、失敗したテストがないことと、対象の入力・期待結果を自分で読めることです。
(テストランナーの構文については『Node.js公式Test runner』をご参照ください)
内部を書き換え、同じテストをもう一度動かす
`study-summary.cjs`の内容だけを、次のコードに置き換えます。完了済みの記録を`filter`で選び、`reduce`で分数を合計する実装に変更しました。テストファイルと期待結果は変えません。
function summarizeStudy(records) {
const finished = records.filter((record) => record.done === true);
return {
completed: finished.length,
minutes: finished.reduce((total, record) => total + record.minutes, 0),
};
}
module.exports = { summarizeStudy };
`reduce`の最後にある`0`は合計の初期値です。これがあるため、完了した記録が0件でも合計を0として返せます。ここを省くと空の配列に対する処理で失敗するため、見た目だけで削除しないでください。
node --test study-summary.test.cjs
この書き換え後も、用意した5件のテストがPASSすることを実行確認しました。これは「今回の5条件で結果が一致した」という証拠であり、あらゆる入力の動作を保証するものではありません。
AIの変更が原因でテストが落ちたらどうする?
例えば、`record.done === true`を`record.done`に変えると、真偽値ではない`"true"`という文字列も完了として数えられることがあります。見た目はわずかな違いでも、集計件数が変わります。
const finished = records.filter((record) => record.done);
本例の「文字列のdoneは完了とみなさない」というテストは、そのような変更を検出します。意図的にこの誤った条件にした際、5件中1件がFAILすることも実行確認しました。
- まず失敗したテスト名と期待値・実際の値を読む。
- どの条件を変えたか、該当するコード行を見つける。
- 仕様変更の依頼がなければ、もとの意味を保つよう修正して再実行する。
仕様そのものを変える必要がある場合は、変更理由と新しい期待結果を別に検討します。「テストが落ちたから期待値を書き換える」だけでは、問題の見落としにつながります。
通った後に見ることと、テストだけでは分からないこと
- 同じ入力で同じ出力になったか。
- 入力配列を途中で書き換えていないか(この例の契約では入力は読み取り専用)。
- 既存の画面やAPIから呼び出した場合も問題ないか。
- 意図しないログ出力や性能劣化がないか。
テストを実施する前に期待値が誤っていた場合、PASSでも正しさは証明されません。今回のコードは入力の型チェックや巨大なデータの性能まで検証していません。そこは実際に必要な仕様が明確になった段階で、別のテストを追加してください。
(一般的なテスト設計の考え方については『Route Zeroの記事:テストケース作成の基本』をご参照ください)
よくある疑問:最小限のテストでも意味はある?
テストを5件通せば、AIの変更は安全ですか?
今回の5条件に対する正しさを確かめたことになります。別の値や利用環境で問題が起きない保証はないため、実際に使う条件に応じてテストを増やします。
AIに修正を依頼するより先にテストを書く必要がありますか?
必ずしも絶対条件ではありませんが、変更前に期待値を固定すると、何を守るべきかを共有しやすくなります。実際の仕様と元の動作を混同しないことが重要です。
次に試すこと:学習記録の表示に広げる
まず自分でテストの「25分」を別の値に変更し、期待値もそれに合わせてみましょう。次に「未完了の記録が1件混ざる」パターンを追加すると、1つの関数を改修する際の確認が習慣になります。
AIの提案を採用する基準は、コードの短さではなく必要な動作が説明でき、確かめられることです。テストが通った小さな改修から始めると、何が変わったのかを追いやすくなります。