中野's workspace

  • Profile
  • Privacy
  • Contact

2019/07/24

PHPUnitでテストを書きながらfizzbuzzを実装する

  • #PHP
  • #PHPUnit
  • #UnitTest

目次

はじめに

PHPのテストツールであるPHPUnitの環境構築を行ったので、テストファーストでプログラムを組んでみたくなりました。

そこで今回はPHPUnitでテストを実装しながら実コードを書き、変更のしやすいプログラムを組むまでの一連の流れを記事にしてみます。

トライ&エラーを繰り返しながらfizzbuzzを実装していきます。

この記事は

  • PHPUnitの使い方を学びたい人
  • テストファーストで実装してみたい人

が対象となることを想定しています。

環境構築に関してはPHPUnitでテストコードを書いて実行するまでを参考にしてください。

テストコードを書く前に仕様の整理

テストコードを実装していく前に、今回実装するプログラムの仕様を整理しておきます。

今回実装するプログラムは以下の仕様を満たすことが出来れば、完成とします。

1. 1 ~ 100までの数字が与えられる
2. 与えられた数字が3で割り切れる場合は`fizz`を返す
3. 与えられた数字が5で割り切れる場合は`buzz`を返す
4. 与えられた数字が3と5で割り切れる場合は`fizzbuzz`を返す
5. それ以外の場合は与えられた数字をそのまま返す

上記に加えて、以下の制限を課します。

  • 1 ~ 100以外の値が与えられることはないものとする。
  • 値が渡されないといったパターンは存在しないものとする。

では、やっていきます。

phpunitと実コードの実装

今回実装するプログラムのディレクトリ構造は以下のようにしました。

root/
 ├ src/
 │  └ FizzBuzz.php
 ├ test/
 │  └ FizzBuzzTest.php
 ├ vendor/
 └ composer.json

またこの段階では、FizzBuzz.phpFizzBuzzTest.phpの中身は以下のように何も書いていない状態です。

FizzBuzz.php
<?php
declare(strict_types=1);
namespace App;

class FizzBuzz
{
}
FizzBuzzTest.php
<?php

use PHPUnit\Framework\TestCase;
use App\FizzBuzz;

class FizzBuzzTest extends TestCase
{
}

また、通常は

プログラムを実装 -> テストを書く

の順で実装していくと思うのですが、今回はテストファーストなので

テストを書く -> プログラムを書く

の順番で実装していきたいと思います。

数字を返す

順番が前後しますが、まずは

  1. それ以外の場合は与えられた数字をそのまま返す

をやっていきます。

数字が3でも5でも割り切れない場合なので、例えば1を与えられたら1を返すことが確認出来たら良さそうです。

テストコードは以下のようにします。

FizzBuzzTest.php
public function testReturnOne()
{
    $fizzbuzz = new FizzBuzz();
    $result = $fizzbuzz->answer();

    $this->assertEquals(1, $result);
}

上記テストを実行してみると失敗すると思います。

$ vendor/bin/phpunit E 1 / 1 (100%)

Time: 837 ms, Memory: 4.00 MB

There was 1 error:

  1. FizzBuzzTest::testReturnOne

Error: Call to undefined method App\FizzBuzz::answer()

php-fizzbuzz\test\FizzBuzzTest.php:11

answerメソッドを実装していないのが原因ですね。

早速実装しましょう!

以下のように1を返すメソッドを実装しても良いですが

FizzBuzz.php
public function answer() {
    return 1;
}

他の数字が渡されるパターンに対応できません。
そのため以下のように引数をそのまま返すメソッドを実装することにします。

FizzBuzz.php
public function answer($number) {
    return $number;
}

テストも引数を渡すように修正します。

FizzBuzzTest.php
public function testReturnOne()
{
    $fizzbuzz = new FizzBuzz();
    $result = $fizzbuzz->answer(1);

    $this->assertEquals(1, $result);
}

テストを実行すると無事通ります。

OK (1 tests, 1 assertions)

この実装の場合、数字の2が渡された場合のテストも通ります。

FizzBuzzTest.php
public function testReturnTwo()
{
    $fizzbuzz = new FizzBuzz();
    $result = $fizzbuzz->answer(2);

    $this->assertEquals(2, $result);
}

3の倍数の時

では3の倍数の場合のテストを書いていきましょう。

FizzBuzzTest.php
public function testReturnFizz()
{
    $fizzbuzz = new FizzBuzz();
    $result1 = $fizzbuzz->answer(3);

    $this->assertEquals("fizz", $result1);

    $result2 = $fizzbuzz->answer(6);

    $this->assertEquals("fizz", $result2);
}

3の倍数、3と6が渡された場合にfizzを返すようなテストを書きました。

$this->assertEquals を一つのテストケースに複数書くのは駄目というところもあります。
(どの箇所でテストが失敗したか分かりにくくなるため)
今回はわかりやすさ重視でテストを分けるといったことはしませんが、プロジェクトの方針によって決めてください。

単純にこの仕様を満たすテストを書く場合

FizzBuzz.php
public function answer($number) {
    return "fizz";
}

こんな感じのテストで良さそうですが、これでは

  1. それ以外の場合は与えられた数字をそのまま返す

を満たせなくなります。

なので、3の倍数であるかの判定を追加する必要がありそうです。

FizzBuzz.php
public function answer($number) {
    if ($number%3 === 0) {
        return "fizz";
    }
    return $number;
}

これで追加したテストも含め、テストが成功することが確認できます。

5の倍数の時

3の場合と同様に考えます。

FizzBuzzTest.php
public function testReturnBuzz()
{
    $fizzbuzz = new FizzBuzz();
    $result1 = $fizzbuzz->answer(5);

    $this->assertEquals("buzz", $result1);

    $result2 = $fizzbuzz->answer(10);

    $this->assertEquals("buzz", $result2);
}

実装は以下のようにします。

FizzBuzz.php
public function answer($number) {
    if ($number%3 === 0) {
        return "fizz";
    }
    if ($number%5 === 0) {
        return "buzz";
    }
    return $number;
}

似たパターンをやっていたので簡単ですね。

3でも5でも割り切れる時

今度は3でも5でも割り切れる場合なので、テストは以下のようにします。

FizzBuzzTest.php
public function testReturnFizzBuzz()
{
    $fizzbuzz = new FizzBuzz();
    $result1 = $fizzbuzz->answer(15);

    $this->assertEquals("fizzbuzz", $result1);

    $result2 = $fizzbuzz->answer(30);

    $this->assertEquals("fizzbuzz", $result2);
}

実装も3の場合や5の場合と同様にやっていけば良いだけなのですが、

FizzBuzz.php
public function answer($number) {
    if (($number%3 === 0) && ($number%5 === 0)) {
        return "fizzbuzz";
    }
    if ($number%3 === 0) {
        return "fizz";
    }
    if ($number%5 === 0) {
        return "buzz";
    }
    return $number;
}

これでは数字を返す時に数字を3で割る処理と5で割る処理がそれぞれ2回ずつ走ることになっちゃいます。
なので少しリファクタしましょう。

FizzBuzz.php
public function answer($number)
{
    $is_multiple_of_three = $number%3 === 0;
    $is_multiple_of_five = $number%5 === 0;
    if ($is_multiple_of_three && $is_multiple_of_five) {
        return "fizzbuzz";
    }
    if ($is_multiple_of_three) {
        return "fizz";
    }
    if ($is_multiple_of_five) {
        return "buzz";
    }
    return $number;
}

結構大胆に変更しましたが、テストを実行すると全件パスすることが確認できたので、デグレを起こしていないことが分かります。

安心感を持ちつつコードを実装でき、リファクタもスムーズに完了しました。

実装後に要件が変わった時

悲しいですが要件はよく変わります。

ですがテストをしっかり書いていると変更にも強いです。

例として、要件が

与えられた数字が3で割り切れる場合はfizzを返す

与えられた数字が3で割り切れる場合はahoを返す

に変わったとしましょう。(懐かしのナベアツですね)

まずテストを改修します。
3で割ったときなので

FizzBuzzTest.php
public function testReturnFizz()
{
    $fizzbuzz = new FizzBuzz();
    $result1 = $fizzbuzz->answer(3);

    $this->assertEquals("fizz", $result1);

    $result2 = $fizzbuzz->answer(6);

    $this->assertEquals("fizz", $result2);
}

これを

FizzBuzzTest.php
public function testReturnAho()
{
    $fizzbuzz = new FizzBuzz();
    $result1 = $fizzbuzz->answer(3);

    $this->assertEquals("aho", $result1);

    $result2 = $fizzbuzz->answer(6);

    $this->assertEquals("aho", $result2);
}

とします。
メソッド名も実態とそぐわなくなるので修正しておきましょう。

実装も以下のように修正します。

FizzBuzz.php
public function answer($number)
{
    $is_multiple_of_three = $number%3 === 0;
    $is_multiple_of_five = $number%5 === 0;
    if ($is_multiple_of_three && $is_multiple_of_five) {
        return "fizzbuzz";
    }
    if ($is_multiple_of_three) {
        return "aho";
    }
    if ($is_multiple_of_five) {
        return "buzz";
    }
    return $number;
}

通常、要件変更などで既存のコードを修正する場合、影響調査が大変です。
しかしながらテストを十分にしておけば既存の箇所に影響なく修正内容が期待値通りであるかが簡単に求められます。

結果としてデグレの発生しにくい、変更に強いプログラムができます。

まとめ

テストファーストで実装する場合

  1. テストから書く
  2. そのテストに対する正しい答えを返せるように実装する
  3. リファクタする

のサイクルでやっていくとスムーズに進みます。

ここら辺はTDD(テスト駆動開発)の思想にも繋がるところかなとは思います。
TDDに関しては勉強不足なので本を読んでおきます。

テストコードを書くのは正直つらいし面倒です。
ですが一度書き慣れてしまうとテストコードがない状態でプログラムを書くのがとても怖くなります。

テストを書いて、変更しやすくバグの少ない世界に飛び込んでみませんか?

このエントリーをはてなブックマークに追加
  • Copyright © 2019. Makoto Nakano
  • ALL Tags