【テスト初心者向け】TDD実践演習~PHPUnitを使ってFizzBuzz問題を実装してみた~

目次

こんにちは!RSSプログラマーのK.A.です。

今回は、テスト初心者向けのテーマとして、「PHPUnitを使ったFizzBuzz問題実装」についてお話しします!
このテーマの記事を書こうと思った背景としては、初めてテスト駆動開発(TDD)に取り組むプロジェクトに参加する機会があり、その体験談を書き記そうと思ったからです。

これまでの開発経験では、実装コードに重点を置くことが多かったですが、TDDのアプローチを学ぶことで、コードの品質向上やバグの早期発見に対する意識が高まりました。
まずは、記事のタイトルについて簡単にご紹介します。

PHPUnitとテスト駆動開発(TDD)の関係

PHPUnitは、PHP用のテストフレームワークであり、テスト駆動開発(TDD)を実践する際に非常に有用なツールです。TDDのプロセスでは、開発者はまずテスト項目を作成し、その後に期待される機能を持つコードを実装します。PHPUnitを使用することで、テストの作成、実行、結果の確認が容易になり、迅速なフィードバックが得られます。これにより、開発者は仕様に沿った高品質なソフトウェアを効率的に作成できます。

テスト駆動開発(TDD)とは

「テスト(RED)→実装(GREEN)→リファクタ(REFACTOR)」というサイクルを繰り返しながら、プロダクトの品質を向上させていく開発手法です。テスト項目は小さい単位で作成することが推奨されていて、これにより高品質なソフトウェアを効率的に作ることができます。

~メリットとデメリット~

[メリット]

・改修のたびにテストを実行することで影響する箇所でエラーを検知できるため、安心して修正ができる
・テストはコードの仕様や動作を可視化するため、仕様がわかりやすくなる
・テスト項目を先に書くことで、コードの設計段階で問題を発見しやすい
・テストで期待通りの動作を保持できることから、リリース後の不具合を減少させ、信頼性が高まる

[デメリット]

・TDDの手法を理解し実践するための初期投資が必要
・テストコードを書くとコード量が大きくなり、メンテナンスを行う際も修正量が多く負担がかかる

~TDDの流れ~

  1. テストファイルの作成
  2. テスト項目を書き出す
  3. テストコードを書く(状態:RED)
  4. テストが成功するような実装コードを書く(状態:GREEN)
  5. 実装コードが完成したら余分なコードを消してより効率的な形に整形する(状態:REFACTOR)
  6. 3~5の繰り返し
  7. 3で書き出したテスト項目すべてが完了したら完成

FizzBuzzとは

今回のテーマにもあるFizzBuzzについて簡単に説明します。
FizzBuzzとはもとは簡単なパーティーゲームで、よくプログラミング問題として出題されます。

– 1から100までの数字を画面に表示する
– 3で割り切れるときは数字の代わりに「Fizz」と表示
– 5で割り切れるときは数字の代わりに「Buzz」と表示
– 15で割り切れるときは数字の代わりに「FizzBuzz」と表示

TDD実践

実際にTDDを実践してみましょう。

1.準備

[今回使用する環境]

・php 8.3
・PHPUnit 11
・laravel 11

[ディレクトリ構成]

project/
├── app/FizzBuzz.php
├── tests/Feature/FizzBuzzTest.php

[Dockerファイル]

FROM php:8.3-apache

# apacheのconf
ADD /apache2/sites-available/000-default.conf /etc/apache2/sites-available/

# composerインストール
COPY --from=composer:2.8.1 /usr/bin/composer /usr/bin/composer

# サーバー側
RUN apt-get update \
&& apt-get install -y \
zip \
unzip \
vim \
libzip-dev
# xdebugインストール
RUN pecl install xdebug
RUN docker-php-ext-enable xdebug
# PHP
RUN docker-php-ext-install zip pdo_mysql mysqli
#mod_rewrite有効化
RUN  a2enmod rewrite

2.テスト項目を書き出す

// --------------------------------------------------
// FizzBuzzテスト項目
// --------------------------------------------------
// 3で割り切れるときはFizzを返す
// 5で割り切れるときはBuzzを返す
// 3でも5でも割り切れるときはFizzBuzzを返す
// 3でも5でも割り切れないときは渡した値を返す

3.1つ目のテスト「3で割り切れるときはFizzを返す」のテストコードを書く

<?php

namespace Tests\Feature;

use Tests\TestCase;

class FizzBuzzTest extends TestCase
{
    /**
    * convertメソッドの確認
    */
    public function test_3で割り切れるときはFizzを返す(): void
    {
        // FizzBuzzクラスをインスタンス化
        $fizz_buzz_class = new FizzBuzz();
        // FizzBuzzクラスのconvertメソッドを呼び出して引数9を渡した戻り値がFizzと等しい場合はテストが正常終了する。
        $this->assertEquals('Fizz', $fizz_buzz_class->convert(9));
    }

}

メソッド名はtest_から始めることで、テストコードであると認識される。
テストコードを書いた時点では、まだFizzBuzzクラスは存在していない状態が正しい。

4.テスト実行

php vendor/bin/phpunit

まだ実装コードがないため、エラーが出ることを想定

5.テスト結果

想定通りのエラーが発生した。

6. エラーになったテストが成功するような実装コードを書

  public function FizzBuzz(int $number): string
    {
        if($number % 3 == 0){
            return 'Fizz';        }
    }

7.テストを再度実行した結果

8.テストが成功したためリファクタリングを行う。

9.これをテスト項目分繰り返す。

完成コード

tests/Feature/FizzBuzzTest.php

<?php

namespace Tests\Feature;

use Tests\TestCase;
use App\FizzBuzz;

class FizzBuzzTest extends TestCase
{
    /**
    * convertメソッドの確認
    */
    public function test_3で割り切れるときはFizzを返す(): void
    {
        $fizz_buzz_class = new FizzBuzz();
        $this->assertEquals('Fizz', $fizz_buzz_class->convert(9));
    }
   
    public function test_5で割り切れるときはBuzzを返す(): void
    {
        $fizz_buzz_class = new FizzBuzz();
        $this->assertEquals('Buzz', $fizz_buzz_class->convert(10));
    }

    public function test_3と5の両方で割り切れるときはFizzBuzzを返す(): void
    {
        $fizzBuzz = new FizzBuzz();
        $this->assertEquals('FizzBuzz', $fizzBuzz->convert(15));
    }

    public function test_3でも5でも割り切れない場合は数字を返す(): void
    {
        $fizzBuzz = new FizzBuzz();
        $this->assertEquals('7', $fizzBuzz->convert(7));
    }

    /**
    * FizzBuzz関数テスト
    */
    public function test_1から15までの数字でFizzBuzzの検証する(): void
    {
        $fizzBuzz = new FizzBuzz();
        $this->assertEquals('12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz', $fizzBuzz->fizzBuzz(15));
    }
}

app/FizzBuzz.php

<?php

namespace App;

class FizzBuzz
{
    /**
    * - 3で割り切れるときは「Fizz」を返す
    * - 5で割り切れるときは「Buzz」を返す
    * - 両方で割り切れるときは「FizzBuzz」を返す
    * - 上記以外の場合、数字を返す
    *
    * @param int $number
    * @return int|string
    */
    public function convert(int $number): int|string
    {
        if ($number % 3 === 0 && $number % 5 === 0) {
            return 'FizzBuzz';
        } elseif ($number % 5 === 0) {
            return 'Buzz';
        } elseif ($number % 3 === 0) {
            return 'Fizz';
        }
        return $number;
    }

    /**
    * 渡された数値までの数字を変換した結果を返す
    *
    * @param int $count
    * @return string
    */
    public function fizzBuzz(int $count): string
    {
        $result = '';
        for ($i = 0; $i < $count; $i++) {
            $result .= $this->convert($i + 1);
        }
        return $result;
    }
}

この状態でテストを実行してみます。

完成しました!

テストが全て通ると気持ちが良いですね~!
続いて、私が実際のプロジェクトでテストコードを書く際に躓いた点を共有します。

躓いた点

・テストの粒度
TDDのサイクルをどれくらい細かく行うべきかが決まらないまま開発を行ってしまい、途中で粒度を変えることで想定工数よりもオーバーしてしまうことがありました。
決めの問題ですが、自分が納得の行く粒度でTDDを行いましょう!

・流れ
「テストコード→実装コード」という流れで書かなければいけないのですが、最初のうちはどうしても実装コードを先に書いてしまうことがありました。
慣れないうちは大変ですが、テストコードから先にかくことで、実装する機能の要件を明確にすることができます。仕様を確認しながら実装を進めることができるため、必ずテストコードから書きましょう!

・工数
実装コードだけを書くよりも、明らかに工数がかかります。
実際に私は、よくあるログイン画面の開発をTDDで行った際、実装コードのみの開発に比べて約3倍の工数がかかってしまいました。最初は実際に掛かった工数を見て落ち込みましたが、バグが少なく、メンテナンス性の高いコードを実装するためには必要なので、初期投資だと考えるようにしています!

おわりに

最後まで読んでくださってありがとうございました!
まだテストコードを書いたことがない新人エンジニアでも、TDD開発手法に挑戦しやすい記事になっていれば幸いです!
これからもRSSのメンバーがブログを投稿していきます。

レイスシステムソリューションズ株式会社のソフトウェア開発や、
採用に関するお問い合わせについては、下記のリンクにてお問い合わせください。

お問い合わせ