BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

TDDのTips

f:id:kmotoki:20201211181511p:plain

前置き

この記事はBASE Advent Calendar 2020 13日目の記事です。 devblog.thebase.in

こんにちは、BASE株式会社 Product Dev Division でバックエンドエンジニアを務めている元木です。

以前、社内で同僚のエンジニアと話していたとき、
「TDDって頭では分かっているけど、テストから書くってなかなか難しいよね」
という話がありました。

そこで、自分がTDDでプログラムを書くときに行なっているTips的なものを紹介してみたいと思います。

あくまで
「自分はこういう感じで実践している」
というものであり、
「これが正しいTDDだ!」
と主張するものではありませんので、軽い気持ちで読んでいただけたら幸いです。

そもそも、TDDとは?

テスト駆動開発 (Test Driven Development) のことです。いいね?

本題

前置きが長くなりましたが、自分が実践しているやりかたは
「テストコードを書く前に、メソッドコメントを書く」
です。

テストを書こうとしても手が進まない場合、
「実装しようとしているメソッドの仕様が、曖昧なままだから」
ということが原因の一つにあるように思います。

そのため、まずは実装しようとしているメソッドの仕様をメソッドコメント(と、メソッドのシグニチャ)という形で明確にしてみると、テストケースの洗い出しがしやすくなるかも知れません。

以下、サンプルコードを例に説明します。


今、PHPで電話帳アプリを開発していて、
「氏名の一部を入力したら、電話帳に登録されている人の中から氏名が一致する人の電話番号を返す」
というメソッド (PhoneBook::search()) を実装しようとしているとします。

何もない状態からいきなりテストを書こうとすると、

<?php
class PhoneBookTest extends TestCase
{
    public function test_氏名の一部を入力したら、一致する人の電話番号を返す()
    {
    }
}

という 1つくらいしかテストケースが思い浮かばなかったりします。

そこでまず、メソッドのシグニチャと大まかなメソッドコメントだけ先に書いてみます。

<?php
class PhoneBook
{
    /**
     * 氏名の一部を入力したら、電話帳に登録されている人の中から氏名が一致する人の電話番号を返す。
     *
     * @param string $name
     * @return array
     */
    public function search(string $name): array
    {
    }
}

このメソッドコメントを充実させていくことが、テストケースを洗い出すことにつながるわけです。

例えば、

<?php
class PhoneBook
{
    /**
     * 氏名の一部を入力したら、電話帳に登録されている人の中から氏名が一致する人の電話番号を返す。
     *
     * @param string $name 電話番号を検索したい人の氏名の一部。
     *     漢字・カナのどちらでも良い。
     * @return array
     */
    public function search(string $name): array
    {
    }
}

と書けば、テストケースは

<?php
class PhoneBookTest extends TestCase
{
    public function testSearch_漢字で検索()
    {
    }

    public function testSearch_カナで検索()
    {
    }
}

の 2つになります。

次に、戻り値に

<?php
class PhoneBook
{
    /**
     * 氏名の一部を入力したら、電話帳に登録されている人の中から氏名が一致する人の電話番号を返す。
     *
     * @param string $name 電話番号を検索したい人の氏名の一部。
     *     漢字・カナのどちらでも良い。
     * @return array 入力した氏名の一部に一致する人の電話番号の配列。
     *     一致する人がいない場合、空の配列を返す。
     */
    public function search(string $name): array
    {
    }
}

と書けば、テストケースも

<?php
class PhoneBookTest extends TestCase
{
    public function testSearch_漢字で検索()
    {
    }

    public function testSearch_カナで検索()
    {
    }

    public function testSearch_一致する人がいない()
    {
    }
}

となります。

さて、ここで
「引数$nameが空文字列だった場合は、どうしようか?」
と思いつきました。
何も考慮せずに実装すると、電話帳に登録されているすべての人の電話番号を返してしまいます。

そこで、以下のような仕様に決めてみました。

<?php
class PhoneBook
{
    /**
     * 氏名の一部を入力したら、一致する人の電話番号を返す。
     *
     * @param string $name 電話番号を検索したい人の氏名の一部。
     *     漢字・カナのどちらでも良い。
     *
     * @return array 入力した氏名の一部に一致する人の電話番号の配列。
     *     一致する人がいない場合、空の配列を返す。
     *
     *     引数 $name が空文字列の場合も、空の配列を返す。
     */
    public function search(string $name): array
    {
    }
}

テストケースも 1つ、追加されました。

<?php
class PhoneBookTest extends TestCase
{
    public function testSearch_漢字で検索()
    {
    }

    public function testSearch_カナで検索()
    {
    }

    public function testSearch_一致する人がいない()
    {
    }

    public function testSearch_渡された氏名が空文字列()
    {
    }
}

ところでこの電話帳アプリ、データは CSVファイルで管理するのですが、後日、データベースに載せ替えことになるかもしれません。 そのため、メソッドの呼び出し元にはメソッド内でファイルを操作していることを隠蔽しておきたいと思いました。

そこで、CSVファイル操作時にエラーが発生した場合は独自に定義した例外を投げるように仕様を決めておきます。

<?php
class PhoneBook
{
    /**
     * 氏名の一部を入力したら、一致する人の電話番号を返す。
     *
     * @param string $name 電話番号を検索したい人の氏名の一部。
     *     漢字・カナのどちらでも良い。
     *
     * @return array 入力した氏名の一部に一致する人の電話番号の配列。
     *     一致する人がいない場合、空の配列を返す。
     *
     *     引数 $name が空文字列の場合も、空の配列を返す。
     *
     * @throws PhoneBookException 検索中にエラーが発生した場合。
     */
    public function search(string $name): array
    {
    }
}

例外のテストケースも追加されました。

<?php
class PhoneBookTest extends TestCase
{
    public function testSearch_漢字で検索()
    {
    }

    public function testSearch_カナで検索()
    {
    }

    public function testSearch_一致する人がいない()
    {
    }

    public function testSearch_渡された氏名が空文字列()
    {
    }

    /**
     * @expectedException PhoneBookException
     */
    public function testSearch_エラーが発生()
    {
    }
}

いかがでしたでしょうか?

TDDではテストコードという形で仕様を記述しますが、何も決まっていない状態からいきなりテストコードを書くのは、なかなか難しいです。 やはり、自然言語で仕様を記述してからテストコードを書いたほうが、アイデアをまとめやすいと思います。

仕様書を書くのと何が違うの?

メソッドの仕様を自然言語で書いていては、
「仕様書を書くのと何が違うの?」
と思うかもしれません。 実際、これほど詳細にメソッドコメントを書くのと仕様書を書くのでは、かかるコストは同じくらいかもしれません。 しかし、仕様を(仕様書ではなく)メソッドコメントという形で記述することには、以下のようなメリットがあると考えます。

  • そのメソッドの仕様が、そのまま他の人にも読める形でソースコード内に残る。
  • ある程度、決まった書式に沿って書くことになるので、考えを整理しやすい。

また、コメントの書式はphpDocumentorなどのドキュメンテーションツールのそれに合わせておくことをお勧めします。 そうすれば、人間だけでなくIDEにも解釈可能になり、プログラミング作業を助けてくれる可能性が高くなるからです。

最後に

今回は、TDDでプログラミングする際に自分が実践しているTIPSをご紹介しました。 皆さんのプログラミング作業の一助になりましたら、幸いです。

明日はデザインチームの北村さん(id:lllitchi)です。 お楽しみに!