Suzna Developer Blog

すずな株式会社の開発者が技術情報を発信します。

テストコードを簡単に書くための方法(PHPUnit)

こんにちは。clonedです。

最近、改めてテストコードのあるプログラムは強い、と感じます。テストされている、変更による破壊を検知できる、というそのままの意味もありますし、テストが書ける程度にコードが整頓されている、つまり実装間違いが起きにくいコードであるという意味もあります。

テストを悪く言う人はあまりいないと思いますが、テストを書けない理由はいろいろあります。その中でも「テストを書いている時間がない」について考えます。

テストを書いている時間は本当にないのか

テストコードを書いて実行するよりも、ブラウザをリロードしたり、アプリを立ち上げ直した方が早く動作確認を行える場合、テストを書くことは “時間がかかること” になります。

大抵の場合、テストコードを書いて実行するよりも、アプリケーションを動作させる方が早いかもしれません(本当はそうでもないと思っていますが一般的な感覚値)。ですが、テストコードにはそもそもメリットがありますので、ほんの少し余分に時間がかかるだけなら(例えば、5分、10分程度)、テストコードを書いて実行する価値を見出しやすくなります。

テストを書いている時間がない、と思う場合、テストを書くと1時間、場合によっては数時間かかりそう、という見込みがあるのではないかと思います。

PHPUnitで時間かけずにテストを書くための工夫を紹介しようと思います。

PHPだけの空間を増やす

PHPの世界から一歩外に出ると途端にテストコードを書くことが難しくなります。つまりテストを書くのに時間がかかります。 データベースを使うテストはfixtureの準備が必要だったり、ファイルシステムを使うテストはMockを使うかテスト用のファイルを準備したりする必要があります。

PHPの世界だけのコードを実行するテストコードを書くのは非常に簡単です。

<?php
class Foo
{
    public function hello(string $name): string
    {
        return 'Hello ' . $name;
    }
}
<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testHello()
    {
        $foo = new Foo();
        $this->assertEquals('Hello Japan', $foo->hello('Japan'));
    }
}

これで、Fooクラスの hello() メソッドを実行(テスト)できます。assertによるテストも重要ですが、実行できることも重要です。なぜなら、プログラマは作ったプログラムをまずは動かしてみたいからです。

次にデータベースを使う場合を考えます。

<?php
// Doctrineのような疑似コード
class Foo
{
    private $entityManager;

    public function __construct($entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function hello(int $id, string $name): string
    {
        $entity = $this
            ->entityManager
            ->getRepository('MyApp:Message')
            ->find($id)
        ;
        return $entity->getMessage() . ' ' . $name;
    }
}

取得したIDでデータベースから取得して、その値を利用するような処理です。こうなると一気にテストコードを書くのが難しくなります。

  • 実際のデータベースを使う場合、messageテーブルにデータを事前に入れておく必要がある(fixtureの準備が必要)
  • PHPUnitのMockを使う場合、 $entityManager という複雑そうなオブジェクトを把握してMockを組み立てる必要がある

このようにPHPの世界を一歩でも出るとテストコードを書く気が一気に萎えます。 解決するには次のようなアプローチがあります。

  • ここでデータベースから値を取得するのをやめる(引数で $message オブジェクトを受け取るようにする)。
  • function getMessage(int $id) などをこのクラスに別途定義して単純なMockを組み立ててテストできるようにする

どちらも他所に押し付けているだけで根本的に解決していないように見えます。が、重要なことがあり、他所に押し付けたことで、 $entity->getMessage() . ' ' . $name のテストが簡単に書けるようになるのです。 このメソッドではデータベースから値を取得している方が派手に見えますが、実際には文字列結合が意図した通りに動作しているかも重要なテスト事項です。

押し付けられた側では結局データベースを使ったテストが必要になるのですが、できるだけ最後まで押し付けていくと、データベースを直接使う処理がまとまっていきます。そこだけ諦めてデータベースを使ったテストを書くことになるかも知れませんが、他の部分で苦労せずに済みます。

PHPだけの空間を増やすことで、テストコードで実行しやすい場所が増え、コードカバレッジも上がります。

入力と出力だけでテストできるメソッドにする

最初の hello(string $name) メソッドの例がそうですが、入力(引数)に対して挙動が確定し、出力(return値)を検証すれば良いコードほど簡単なテストはありません。

別の言い方をすると、インスタンス変数によって挙動が変わったり、戻り値以外で動作検証が必要だとテストを書く手間が増えます。プライベートなインスタンス変数はReflectionPropertyクラスなどを利用して操作できますが、テストコード内に準備のためのコードが多くなり見通しが悪くなります。何より萎えます。

1つのメソッドで複数のことをしない

Aを渡したらBを返す、のテストを書くのは簡単ですが、例えば、Aを渡したらAを元にループが始まり、ループの中で条件によって処理が異なるような場合、テストコードで網羅すべきパターンが増え複雑になります。

<?php
    public function hello(array $params): array
    {
        $result = [];
        foreach ($params as $param) {
            if (isset($param['foo'])) {
                $result[] = $param['a'];
            } elseif (isset($param['bar'])) {
                $result[] = $param['b'];
            } else {
                throw new \Exception('Unknown param');
            }
        }
        return $result;
    }

コードだけ見るとこれくらいは許してよという感じもしますが、テストコードを書く視点に立つと次のように見えます。

  • ループするときとしないときがある
  • ループ内に条件分岐がある
  • ループ内の条件分岐によって戻り値が変化する

1つのメソッドで複合的なパターンを考えなければならないので次のようにメソッドを分けることで、それぞれのテストで考えることを減らすことができます。

<?php
    public function hello(array $params): array
    {
        $result = [];
        foreach ($params as $param) {
            $result[] = $this->helloEach($param);
        }
        return $result;
    }

    private function helloEach($param)
    {
        if (isset($param['foo'])) {
            return $param['a'];
        } elseif (isset($param['bar'])) {
            return $param['b'];
        } else {
            throw new \Exception('Unknown param');
        }
    }

一般的に、1つの複雑なメソッドのテストを書くよりも、2つの単純なメソッドのテストを書くほうが楽です。制御構造の中に制御構造、をできるだけ避けることでテストコードの複雑さはぐっと下がります。

テストしたいところだけをテストする(Mockをうまく利用する)

AクラスがBクラスを、BクラスがCクラスを利用しているような場合、Aクラスをテストしたいだけなのに、Cクラスが動くように準備しないとテストできない、という状況がよくあります。

このような場合にはMockが最適です。特にPHPUnitでは動的にMockが生成できるので、不要なMockクラスを作成する必要はありません。次はFooクラスがUserクラスに依存している場合の例です。

<?php
class Foo
{
    public function hello(): string
    {
        return 'Hello ' . $this->getUserName();
    }

    protected function getUserName(): string
    {
        $user = new User();
        return $user->getName();
    }
}

class User
{
    private $name;

    // $this->nameに値を設定するような処理は割愛

    public function getName(): string
    {
        return $this->name;
    }
}
<?php
use PHPUnit\Framework\TestCase;

class FooTest extends TestCase
{
    public function testHello()
    {
        // FooクラスのMockを生成
        // コンストラクタは呼び出さない
        // getUserName()をMock上で生成
        $foo = $this->getMockBuilder(Foo::class)
            ->disableOriginalConstructor()
            ->setMethods(['getUserName'])
            ->getMock();
        // getUserName()は1度だけ呼び出されて 'John' を返す
        $foo->expects($this->once())
            ->method('getUserName')
            ->willReturn('John');
        // Mockの挙動を使ってテスト
        $this->assertEquals('Hello John', $foo->hello());
    }
}

コード例が長くなるので単純化していますが、FooクラスのテストコードではUserのことを考えなくて済みます。useすら必要ありません。

もちろん、Userクラスが意図した動作をしない場合、このテストでは問題を検知できません。ですが、ユニットテストだからと割り切ってあまり気負いせずに書きやすくテストコードを書いた方が良いと思います。

データベースのところでは $entityManager のMock化をデメリットとして書きましたが、フレームワーク/ライブラリのオブジェクトをMock化するにはそのクラスをよく把握する必要があり簡単とは言えません。ですが、今回のように自分がまさに開発しているコードであれば動作もよく把握しているのでMockの組み立てを簡単に行うことができます。

簡単とは言え、Mockを使わずに済むならその方がより簡単です。Web APIを使うような常にMockが必要な場合にPHPUnitの動的なMockを常に組み立てていると手間が多いので、指定したレスポンスを返すMockクラスを作った方が良い場合もあります。

終わりに

思いのほか長くなってしまったので、他の方法はまた機会があれば書こうと思います。

テストコードの簡単な書き方!ではなく、テストコードを簡単に書くためのアプリケーションコードの書き方ばかりになってしまいました(確信犯ですが)。テストコードのためにアプリケーションコードを変更するのは本質的ではない、という意見もあると思いますが、テストしやすいコードというのは、つまり呼び出し側にとって簡単に扱える優しいコードだと思いますので、結果問題ないと考えています。

昔、コードカバレッジ100%を謳う発表をしたことがありますが、目標は高く、というだけで、コードカバレッジのみを目的にする必要はありません。実行したいところをPHPUnitから簡単に実行できる、というスタート地点の克服が大切に思います。

ビデオ会議をハングアウトからappear.inにしたら快適になった

こんにちは。clonedです。

リモート業務が多いためビデオ会議が必須なのですが、今まではあまり検討せずにGoogle ハングアウトを使っていました。

Google ハングアウトでもどうしても困るほどではありませんでしたが、次の点で不満がありました。

  • Googleアカウントが必要
  • 通信が安定しない

Googleアカウントについては持っていても利用しているブラウザがログインしていないとすぐ使えない、アカウントを持っていない人を招待できない、など致命的ではないものの不満に感じていました。

そこで、検討した結果、appear.inとzoomの二つに絞り込みました。

appear.in zoom.us

zoom は品質面でしっかりしていそうに思いましたが、無料で使える appear.in を利用したところGoogle ハングアウトよりも随分快適でした。

f:id:cloned:20161205163359p:plain

https://appear.in に訪れるとルーム名を入力するだけでビデオ会議を開始できます。

2014年に書かれた記事 スカイプ、ハングアウトを超えうるビデオ会議サービス『Appear.in』 | ライフハッカー[日本版] に当時の解説がありますが、まさに不満だった2点が解決したのに加えて、快適になりました。

  • アカウントが不要
  • 通信が安定する
  • 音質/画質が良い
  • 接続速度が速い(ルームに入るまでの時間)

通信はたまに切れることがありますし(appear.inが悪いのかその他の通信環境が悪いのかは不明)、信じられないほどの品質になったということではありませんが、基本的なところで抱えていた不満がなくなり、とても快適になりました。

Slackとの連携もアプリ ( appear.in | Slack )を入れれば /appear チャンネル名 ですぐにルームのリンクを作成できて便利です。

MacのSafariで使えないのが難点ですが、それを考慮してもGoogle ハングアウトよりも良い体験になりました。

Swiftで自作structをArrayやDictionaryのように使えるようにしてみる

初めまして、Swiftがバージョンアップされる度に産まれたての子羊かのごとくガクブルしているKayです。
まぁ、容赦なくバージョンアップした後に泣いたり、感動したりと忙しい日々を送っております。
※ 現在のバージョンはSwift 3.0です。

Arrayっぽくしてみる

for...inで回してみる

ここでは好みで全てstructにしておりますが、classでも同じ事が出来るはずです!

struct MyStrings: Sequence {
    private var _array: [String] = ["string1", "string2", "string3"]
    func makeIterator() -> IndexingIterator<[String]> {
        return self._array.makeIterator()
    }
}

let strings = MyStrings()
for string in strings {     // string1
    print(string)           // string2
}                           // string3

Iteratorまで自作してもいいのですが、
実際にarrayを内包してるパターンが多いかとは思いますので少々手を抜きました(・・。)ゞ
必要メソッドはmakeIteratorだけなので、for...inで回すだけならこれで簡単に出来ます。

でもでも!
どうせなら、isEmptyとか、firstとか・・・使いたいですよね・・・?

Collectionプロトコルを実装してみる

プロトコルをSequenceではなく、Collectionにしておけば、

struct MyStrings: Collection {
    // fileprivateにしてるのは、後でextensionするからです
    fileprivate var _array: [String] = ["string1", "string2", "string3"]

    var startIndex: Int { return self._array.startIndex }
    var endIndex  : Int { return self._array.endIndex }

    func index(after i: Int) -> Int {
        return self._array.index(after: i)
    }

    subscript(index: Int) -> String {
        return self._array[index]
    }
}

let strings = MyStrings()
for string in strings {     // string1
    print(string)           // string2
}                           // string3

print(strings.isEmpty)              // false
print(strings.first)                // Optional("string1")
print(strings.contains("string1"))  // true

回せるだけでなく便利メソッドがいろいろ使えるようになるので、個人的にはこっちのほうが好きです( ̄▽ ̄)b

更に、必要な実装は増えますが需要に合わせてCollectionタイプを変えればよりスマートですね☆
実際に、本物のArrayはもっと他のCollectionを実装してますが、面倒くさいので省略します( '∇')

ちなみに、SequenceやCollection達の関係性はこんな感じ↓

                     +--------+
                     |Sequence|
                     +---+----+
                         |
                    +----+-----+
                    |Collection|
                    +----+-----+
                         |
          +--------------+-------------+
          |              |             |
          |     +--------+--------+    |
          |     |MutableCollection|    |
          |     +-----------------+    |
          |                            |
+---------+-------------+    +---------+----------------+
|BidirectionalCollection|    |RangeReplaceableCollection|
+---------+-------------+    +--------------------------+
          |
 +--------+-------------+
 |RandomAccessCollection|
 +----------------------+

Array風に書けるようにする

extension MyStrings: ExpressibleByArrayLiteral {
    init(arrayLiteral elements: String...) {
        self._array = elements
    }
}

let strings: MyStrings = ["string1", "string2", "string3"]
for string in strings {     // string1
    print(string)           // string2
}                           // string3

もう一手間加えると、更にArrayっぽくなります☆

Dictionary風にしてみる

for...inで回してみる

struct MyDictionary: Sequence {
    private var _dictionary: [String: Any] = ["a": "1", "b": 2, "c": "3"]
    func makeIterator() -> DictionaryIterator<String, Any> {
        return self._dictionary.makeIterator()
    }
}

let dictionary = MyDictionary()
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

これまたDictionaryを内包してる前提の手抜きコードですが、Arrayの時とIteratorが違うだけですね。
当然Dictionaryなので、順不同に出てきます。

Collectionを実装

struct MyDictionary: Collection {
    typealias Element = (key: String, value: Any)
    typealias Index   = DictionaryIndex<String, Any>

    // fileprivateにしてるのは、後々extensionするからです
    fileprivate var _dictionary: [String: Any] = ["a": "1", "b": 2, "c": "3"]

    var startIndex: Index { return self._dictionary.startIndex }
    var endIndex  : Index { return self._dictionary.endIndex }

    func index(after i: Index) -> Index {
        return self._dictionary.index(after: i)
    }

    subscript(index: Index) -> Element {
        return self._dictionary[index]
    }
}

let dictionary = MyDictionary()
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

print(dictionary.isEmpty)   // false
print(dictionary.count)     // 3
print(dictionary["aaa"])    // Error: Cannot subscript a value of type 'MyDictionary' with an index of type 'String'

IndexとElementが少々複雑になりましたが、これも実装的にはArrayとほぼ変わりません。 ただ、これだけだと回せてもあまりDictionaryらしい使い方は出来てないですね・・・

その他必要そうなメソッドを追加する

extension MyDictionary {
    subscript(key: String) -> Any? {
        return self._dictionary[key]
    }
}

print(dictionary["a"])      // Optional("1")
print(dictionary["d"])      // nil

これがなければ始まりませんよね。 後は地道にほしいメソッドを手動で追加してく方法しかないのかな・・・?(たぶん)

Dictionaryらしく書けるようにする

extension MyDictionary: ExpressibleByDictionaryLiteral {
    init(dictionaryLiteral elements: (String, Any)...) {
        self._dictionary = [:]
        for (key, value) in elements {
            self._dictionary[key] = value
        }
    }
}

let dictionary: MyDictionary = ["a": "1", "b": 2, "c": "3"]
for (key, value) in dictionary {    // b: 2
    print("\(key): \(value)")       // a: 1
}                                   // c: 3

一気にDictionaryっぽくなりました〜(*゚▽゚ノノ゙☆パチパチ

以上でした! これであなたもArrayやDictionaryのように扱える自作structを作ってみませんか〜?

SymfonyのセッションにDynamoDBを利用する

こんにちは。clonedです。今日からすずな株式会社の開発者ブログを開始することになりました。今後ともよろしくお願いします。

SymfonyのセッションストレージにDynamoDBを使うというアイデアは目新しくないと思いますが、情報が少ないように思ったので書いてみます。

Symfony Session DynamoDB で検索すると GitHub - gwkunze/dynamo-session-bundle: DynamoDb Session Handler for Symfony 2 が上位に出てきます。しかし、更新が止まっており最新の aws/aws-sdk-php が利用できないようです。

また、AWS公式の aws/aws-sdk-phpAws\DynamoDb\SessionHandler があるのにわざわざ他のライブラリを被せるのがスマートではありません。

そういうわけで、Symfonyの設定を楽にする aws/aws-sdk-php-symfony (こちらもAWS公式)を使って設定だけでDynamoDBをセッションに利用する方法を紹介します。

DynamoDBにテーブルを作成

f:id:cloned:20161007234420p:plain

paramters.ymlでテーブル名、プライマリキーの名称はどちらも設定可能なのでこの名前でなくても大丈夫です。

Symfonyを準備

$ symfony new my_project

aws/aws-sdk-php と aws/aws-sdk-php-symfony をインストール

$ cd my_project
$ composer require aws/aws-sdk-php aws/aws-sdk-php-symfony

app/AppKernel.php にAwsBundleを追加

<?php
...
    public function registerBundles()
    {
        $bundles = [
...
            new Aws\Symfony\AwsBundle(),

DynamoDBの設定をする

app/config/parameters.yml

app/config/paramters.yml にdynamodbの設定を追記する

parameters:
    dynamodb:
        table_name: session
        session_lifetime: 7776000

table_name にはDynamoDBで作成したテーブル名を設定します。 session_lifetime にはセッション保存期間を秒で設定します。

app/config/services.yml

services:
    dynamodb_session_connection:
        class: Aws\DynamoDb\StandardSessionConnection
        arguments: ["@aws.dynamodb", "%dynamodb%"]
    dynamodb_session_handler:
        class: Aws\DynamoDb\SessionHandler
        arguments: ["@dynamodb_session_connection"]

DynamoDBクライアントとparameters.ymlで設定したセッションに関する設定値を元にStandardSessionConnectionインスタンスが生成されます。 そして、そのStandardSessionConnectionインスタンスを元にSessionHandlerが生成されます。

app/config/config.yml

セッションのハンドラーをservices.ymlで追加した dynamodb_session_handler に設定します。

framework:
    session:
        handler_id: dynamodb_session_handler
        name: my_project
        cookie_lifetime: 7776000

そして、AWSユーザーであればおなじみの設定を追記します。

aws:
    version: "latest"
    region:  "ap-northeast-1"
    credentials:
        key:    "AWSのkey"
        secret: "AWSのsecret"

動作確認

サイトにアクセスしてみた後にDynamoDBでテーブルを確認するとセッションが保存されています。

f:id:cloned:20161007235219p:plain f:id:cloned:20161007235228p:plain

まとめ

AWS公式ライブラリだけで対処できて素敵です。

動作環境
  • Amazon Linux
  • PHP 5.6.25
  • Symfony 3.1.5
  • aws/aws-sdk-php 3.19.13
  • aws/aws-sdk-php-symfony 1.2.0