サバンナ便り ~ソフトウェア開発の荒野を生き抜く~

第1回学習用テスト ~学びを自動テストとして書く

こんにちは、今回からコラムを書かせていただく和田(t_wada)と申します。

現代のソフトウェア開発の対象領域は、広く複雑で不確実なものになりました。この連載では、自動テストAutomated Testに関わるトピックを中心に、ソフトウェア開発の荒野を生き抜いていくためのプログラミングやソフトウェアエンジニアリングの考え方を書いていきたいと考えています。

初回のテーマは、学習や調査が目的のテストコードを書くテクニック「学習用テスト」Learning Testです。では、よろしくお願いします。

二兎を追わない

プログラミングのコツに、⁠一度に2つ以上のものを相手にしないこと」があります。

未知の技術を使って問題を解決するコードを書こうとするとき、私たちは2つのものと同時に戦うことになります。未知の技術そのものと、その技術を使った問題解決の2つです。2つ以上のものを同時に取り扱おうとすると、プログラミングは暗中模索の状態に陥りがちです。

このようなとき取るべき手段は、大きいタスクを小さいタスクに分割し、1つずつ倒すことです。つまり、未知の技術、たとえばこれまで使ったことがないライブラリやフレームワークの使い方を学ぶという学習タスクと、その知見を使って自分の解きたい問題を解決するタスクに分けるのが王道です。

即時性と再現性─⁠─ 学びを支える2つの性質

未知の技術を学ぶときは、公式のドキュメントを読むだけでなく、実際に手を動かしてみることが効果的です。学びの効果がさらに高まるのは、即時性のあるフィードバックが得られるときです。手の動かしやすさと即時フィードバックが得られるという観点では、REPLRead-eval-print loopやデバッガ、開発者コンソールなどの対話的なUIUser Interfaceがまず候補に挙がるでしょう。

学びという観点でもう一つ大事なのが再現性です。再現性とは、学習内容をどのくらい簡単に再現できるかどうかです。この観点では、対話的環境で動かしただけでは十分とは言えません。過去の学習内容を手もとで再現するのがやや面倒だからです。

そこで、学びを自動テストの形で残すという手法が有力な候補になってきます。そういった学習目的の自動テストを「学習用テスト」と言います。学習用テストでは、実際に取り組む問題を解決するテストコードを書くのではなく、未知のライブラリやフレームワークなどを学ぶことに特化したテストコードを書きます。学習用テストは、動作するサンプルコードを伴う詳細なドキュメントとして機能します。自動テストは即時性という観点では対話的環境にはかないませんが、実行は一瞬です。つまり、学習用テストでも即時フィードバックが働きます。

対話的環境で学んだ内容を学習用テストに移植するのもお勧めです。再現性に問題を抱える対話的環境の弱点を学習用テストで補い、効率良く着実に学べます。

では実際の学習用テストの例を見てみましょう。リスト1は、PHPの日付関係の標準ライブラリを調べているときに私が実際に書いたテストコードです。

リスト1 DateTimeImmutableクラスの不変性に関する学習用テスト(PHP)
/**
 * @test
 * @group learning
 */
public function addメソッドは自分の状態を変更する代わりに計算後の状態を伴う新しいインスタンスを返す(): void
{
    $newYearsDay2022 = new DateTimeImmutable('2022-01-01');
    $added = $newYearsDay2022->add(DateInterval::createFromDateString('1 year'));
    $this->assertNotSame($newYearsDay2022, $added);
    $this->assertEquals('2022-01-01', $newYearsDay2022->format('Y-m-d'));
    $this->assertEquals('2023-01-01', $added->format('Y-m-d'));
}

不変オブジェクトDateTimeImmutableの使い方を調査し、addメソッドに副作用がないことを実際にテストを書いて確認しています。PHPの公式ドキュメントから読み取れることを、実際に手もとで検証しようとした記録であるとも言えます。

学習用テストを書いて未知のライブラリの使い方を学んだら、次にその学んだ内容を使って本来自分の解きたかった問題を解決するコードを書いていけばよいのです。

疑念、疑問をテストにする

学習用テストでは、頭に浮かんだ疑念や疑問をテストコードとして書くことも効果的です。たとえば、リスト1に出てきたDateTimeImmutableはタイムゾーンの情報も持っています。では、タイムゾーンが異なるものの、同じ瞬間を指しているDateTimeImmutableのインスタンスどうしは等価とみなされるのでしょうか。設計としては、等価であるとみなされても、等価ではないとみなされても、どちらもそれなりの説得力がありそうに思えます。

こういう場合は実際にテストを書いて動かしてみるのが早いですね。リスト2のテストは成功します。つまり、DateTimeImmutableはタイムゾーンが異なっても同じ瞬間を指していれば等価とみなすことがわかりました。たとえば、この振る舞いが公式ドキュメントにない暗黙の仕様である場合、将来予告なく変更されることもありえます。暗黙の仕様の学習用テストを残しておくと、そのテストの失敗が、過去学んだ振る舞いの変更を教えてくれます。

リスト2 タイムゾーンと等価性に関する学習用テスト(PHP)
/**
 * @test
 * @group learning
 */
public function 同じ時刻を指している場合はタイムゾーンが異なっても等価とみなされる(): void
{
    $utc = new DateTimeImmutable('2021-12-24T15:00:00', new DateTimeZone('UTC'));
    $jst = new DateTimeImmutable('2021-12-25T00:00:00', new DateTimeZone('Asia/Tokyo'));
    $this->assertTrue($utc == $jst);
}

印を付けて見分ける

学び終わったテストが不要になることもあるでしょう。学習用テストはほかのテストと同一のディレクトリに作成することが多いですが、時間が経つと見分けることが難しく、不要になった学習用テストが残ってしまいがちです。

そこで、学習用テストには印を付けておくことをお勧めします。テスティングフレームワークによってはテストにマークやタグを付けられるものがあります。リスト1、2では、PHPUnitの@groupアノテーションを使っています。タグ機能がない場合は、テストメソッドの命名規則などで学習用テストと伝わる名前を使うのがよいでしょう。タグやテストの名前をヒントに、それが学習用テストならば消すという判断を後日行えます。

まとめ

今回は、学びをテストコードの形にする学習用テストを紹介しました。学習用テストには2つの長所があります。すぐに学びの結果が得られることと、学んだ内容の再現性が高いことです。皆様も、ぜひ学習用テストを書いてみてください。

おすすめ記事

記事・ニュース一覧