この記事はCode Polaris Adventcalendar12日目の記事です。
whoami
rinahiraと申します。普段はさくらインターネット株式会社という会社で販売管理システムの開発・運用を担当しています。
Code Polarisと私の出会い
Code Polarisには2023年6月ごろから参加させて頂いています。
テスト駆動開発の勉強したいなーとなんとなくconnpassで「TDD」で検索していたところ、Code PolarisのTDD勉強会キックオフがヒットしまして、女性エンジニアコミュニティというものがあるのか!と驚いたのがきっかけでした。
そのキックオフ会に参加して、雰囲気の良さを感じて本編TDD勉強会への参加を決めました。
また、勉強会で使うGitHubについて、キックオフ会ですごく丁寧な説明がありました。コミュニティ限定で動画が残していただいているそうなので、GitHubに興味がある方でコミュニティ参加済みでしたら動画を見ると非常に勉強になると思います。
Code PolarisでのTDD勉強会
普段の職場は女性エンジニアが少ないため、女子高・女子大出身の私としては久しぶりに女性が多い環境で喋れる!とテンションがあがりました
Kent Beckの「テスト駆動開発」について各自読書をすすめ、書籍に書かれているサンプルコードを各自得意な言語で写経し、Git Hubに上げてPRを作成しオンラインで共有しあう会です。共有後に輪読会のメインメンバーにPRをマージしてもらいます。
私は普段仕事で使っているPHPで書いています。
そして、それぞれ得意な言語で書いているので、普段使ったことがない言語での書き方を学んだり、各言語との違いを体感したりと、非常に勉強になります。
テスト駆動開発について
テスト駆動開発について、書籍を読んだり他の媒体から学んだりしたことを自分なりにまとめます。
※まだ読書会は途中でして、私は13章くらいまでしか読めていないのでその時点までの知識となります
テスト駆動
小さく一歩ずつ作業を確かめながら進めていきます。
- まずはテストを書く
- テストを実行し、テストが失敗することを確認する(レッド)
- テストが通るような変更を行う
- テストが通ることを確認する(グリーン)
- リファクタリングする
といったステップで、都度テストを実行しながらレッド→グリーン→リファクタリングのサイクルをぐるぐると回しながら実装を進めていく設計技法です。
難しい機能をいきなり全部作成するのではなく、実装な必要なことはToDoリストにメモを取っておき、小さくテストを書き、それを実現するコードを書き、そこからリファクタリングを進めることによってだんだんと機能を作り上げていきます。
以下に簡単なFizzBuzzのアプリで例を挙げます。
(PHP 8.2,PHPUnitで書きました)
1. まずはテストを書く
FizzBuzzの要件の一つである 3の倍数を渡すとFizzが返ってくる を検証するテストを書きます。
<?php
declare(strict_types=1);
namespace app;
use PHPUnit\Framework\TestCase;
class FizzBuzzTest extends TestCase
{
public function testFizzBuzz()
{
$fizzBuzz = new FizzBuzz();
$this->assertEquals('Fizz', $fizzBuzz->execute(3));
}
}
2. テストを実行し、テストが失敗することを確認する(レッド)
ここでテストを実行すると、当然呼び出しているFizzBuzzクラスをまだ書いていないので失敗します。
1) app\FizzBuzzTest::testFizzBuzz
Error: Class "app\FizzBuzz" not found
3. テストが通るような変更を行う
テストがグリーンとなるように、FizzBuzz.phpを書いていきます。
まずはテストだけ通ることを考えて、こんな感じに書いてしまいます。
<?php
declare(strict_types=1);
namespace app;
class FizzBuzz
{
public function execute(): string
{
return 'Fizz';
}
}
4. テストが通ることを確認する(グリーン)
OK (1 test, 1 assertion)
5. リファクタリングする
リファクタリングをして重複を無くしていきます。
どんな数字が来ても固定で'Fizz'を返すというひどい状態のコードをリファクタリングして、本来のFizzBuzzの要件である 3の倍数の時にFizzと返す の処理に変更していきます。
<?php
declare(strict_types=1);
namespace app;
class FizzBuzz
{
public function execute(): string
{
if ($int % 3 === 0) {
return 'Fizz';
}
}
}
ここでまた 1. まずはテストを書く に戻り、今度は5の倍数を渡してBuzzが返ってくることを検証するテストを書いてみます。
<?php
declare(strict_types=1);
namespace app;
use PHPUnit\Framework\TestCase;
class FizzBuzzTest extends TestCase
{
public function testFizzBuzz()
{
$fizzBuzz = new FizzBuzz();
$this->assertEquals('Fizz', $fizzBuzz->execute(3));
+ $this->assertEquals('Buzz', $fizzBuzz->execute(10));
}
}
テストを書いて、2. テストを実行し、テストが失敗することを確認する(レッド) を確認したら、3. テストが通るような変更を行う を実行します。
<?php
declare(strict_types=1);
namespace app;
class FizzBuzz
{
public function execute(int $int): string
{
if ($int % 3 === 0) {
return 'Fizz';
+ } elseif ($int % 5 === 0) {
+ return 'Buzz';
+ }
これでまた 4. テストが通ることを確認する(グリーン) でコードが動いていることを確認できました。
またこの後は
- 15の倍数の時にFizzBuzzが返る
- いずれの倍数でもない場合は数字をStringに変換して返す
といった仕様をテストに落とし込んで、1~4のサイクルをぐるぐると回すことで、少しずつ動かしながらFizzBuzzアプリを作成することができます。
(必要な仕様はToDoリストに記載しておき、実装したら取り消し線で消していきます)
実際TDDで書いてみて感じたメリット
早く動くコードが得られる
いきなり難しい機能を全部実装しようとして、実装完了して動かしてみたら全然動かなくて、デバッグにものすごい時間を費やしてしまうことは良くあることだと思います。
テスト駆動開発の場合、小さな歩幅で少しずつプログラムを動かしてはリファクタリングを繰り返していくので、「動かしてみたら全然ダメだった」ということがありません。テストが通らなくなった場合、テストが通った後に行った変更が原因のため、どこを直せばいいのかが比較的明確になります。
テストを書いてから実装するため、自ずとテストしやすい設計になる
テストを先に書くため、引数をどう渡すか、どういう戻り値を返すかの関数のインターフェースが決まります。
先にコードを書き終わっていざテストを書こうと思ったときに書きにくかったり、モックを差し込むためにリファクタリングが必要になったりする経験がある方もいると思います。
テストを先に書くと、その機能を どう呼び出すか が決まってから実装することになります。
実際テストで呼んで使い勝手を確かめながら開発を進めていくので、実際使ってみて「この機能使いにくいな」と思えばリファクタリングしてまたテストして使い勝手を確かめるというプロセスを踏めます。
また、レッド→グリーン→リファクタリングのステップを踏むことで、最初に必ずテストを失敗させることになるので、「このテスト100%通るようになってるやん…意味ない」というミスを防げます。
読んで思ったあった方がよい前提知識
Kent Beckの「テスト駆動開発」を読む前にこの知識があればより理解が深まる、という情報をまとめます。
古典学派、ロンドン学派
単体(Unit)テストではUnitとしてどの単位で隔離するかの考え方で 古典学派(デトロイト学派)とロンドン学派(モック主義者)に分かれるということを認識しておいた方が、本書によりすんなりと入れると思います。
古典学派(デトロイト学派)
- クラスではなく、一単位のふるまいをUnitとする
- クラス内で呼ばれる別クラス(協力者オブジェクト) もそのまま利用する
- 共有依存(DBなど他のテストケースと共有するもの) についてはモックする
- 古典学派の聖典は Kent Beckのテスト駆動開発
ロンドン学派(モック主義者)
- クラスをUnitとする
- クラス内の協力者オブジェクトはモックする
- 不変依存(Enumなどの変わらないオブジェクト)以外の依存すべてをモックする
- テストがテスト対象の詳細なコードに密結合しがち
- ロンドン学派の聖典は 実践テスト駆動開発
Kent Beckのテスト駆動開発は古典学派なので、クラス単位ではなく、一単位のふるまいをテストケースにしています。
職場ではどちらかというとロンドン学派のスタイルで今までテストを書いており、本書の第一章で他国通貨アプリのサンプルプログラムにて Dollar.Java クラスのテストを MoneyTest.java として書き出した段階で戸惑ってしまいました。
(ロンドン学派的なアプローチであれば、クラスのテストを書くのでおそらく DollarTest.java になる)
本書の訳者(t-wadaさん)解説である付録Cや単体テストの考え方/使い方という書籍を読んでいると、より理解が深まると思いますのでお勧めです。
感想
Code PolarisのTDD勉強会を通して、テスト駆動開発を楽しく学べています。
ただ、書籍を読んで写経して、レッド→グリーン→リファクタリングのリズムは身についたけれども、実際現場でどうテスト駆動開発を進めるべきか、まだ分からないというのが本音です。
「どうテストを書けばよいか」「どうリファクタリングを進めていけばいいか」「どういったものが良いテストなのか」についてはこれからも学びながら、試行錯誤していきたいと思います。
また、テスト書くのが楽しくなってテストを書きすぎてしまわないように、というのも気を付けなければいけないと思いました。プロダクションコードと同じくテストコードも 負債 になるので、やたらめったらテストコードを増やしてしまうのはよくないようです。
(テストコードが無駄に大量だと、ソースコード修正して全然テストが動かなくなるというメンテ地獄が待ってます...)
少ないテストコードで高い価値を得ることができるように、良いテストコードの書き方を勉強したいと思います。
ここまでお読みいただきありがとうございました。
まだ学習中のため、誤りなどありましたらコメント頂けると助かります。