Jobeetをやってみる 8日目

単体テスト(ユニットテスト)について学ぶ

symfonyにおけるテスト

symfonyではと単体テストと機能テストを自動生成する
それぞれ test/unit, test/functional に生成される

今回は単体テストのみ対象で、テストにはlimeテストフレームワークを利用する

テスト用メソッド

値比較用
メソッド名 説明
ok($test) $test == true
is($value1, $value2) $value1 == $value2
isnt($value1, $value2) $value1 != $value2
like($string, $regexp) preg_match($regexp, $string) != 0
unlike($string, $regexp) preg_match($regexp, $string) == 0
is_deeply($array1, $array2) $array1 == $array2
その他の便利なメソッド
メソッド名 説明
fail($msg) 常にfailになる(Exceptionのテストに便利)
pass($msg) 常にpassになる(Exceptionのテストに便利)
skip($msg, $nb_tests) $nb_tests数分スキップする(条件付きテストに便利)
todo($msg) テストがまだ未実装の場合用
comment($msg) テストを実行した場合のコメント

テストしてみる

<?php
// test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(1, new lime_output_color());
$t->pass('This test always passes.');
$ ./symfony test:unit Jobeet

slugifyのテスト

以下のようなテスト

入力値 期待する出力
Sensio Labs sensio-labs
Paris, France paris-france
<?php
// test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(6, new lime_output_color());
 
$t->is(Jobeet::slugify('Sensio'), 'sensio');
$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs');
$t->is(Jobeet::slugify('sensio   labs'), 'sensio-labs');
$t->is(Jobeet::slugify('paris,france'), 'paris-france');
$t->is(Jobeet::slugify('  sensio'), 'sensio');
$t->is(Jobeet::slugify('sensio  '), 'sensio');
$ ./symfony test:unit Jobeet

エラーが起こったときはどうなるかというと、最後の文がFailed testとなり、エラーに関する情報を出力してくれる。
みたい場合はどれか$t->fail()を追加してみる

テストに何をしたのかさっぱりなので、テストの説明を出力してみる

<?php
// test/unit/JobeetTest.php
require_once dirname(__FILE__).'/../bootstrap/unit.php';
 
$t = new lime_test(6, new lime_output_color());
 
$t->comment('::slugify()');
$t->is(Jobeet::slugify('Sensio'), 'sensio', '::slugify() converts all characters to lower case');
$t->is(Jobeet::slugify('sensio labs'), 'sensio-labs', '::slugify() replaces a white space by a -');
$t->is(Jobeet::slugify('sensio   labs'), 'sensio-labs', '::slugify() replaces several white spaces by a single -');
$t->is(Jobeet::slugify('  sensio'), 'sensio', '::slugify() removes - at the beginning of a string');
$t->is(Jobeet::slugify('sensio  '), 'sensio', '::slugify() removes - at the end of a string');
$t->is(Jobeet::slugify('paris,france'), 'paris-france', '::slugify() replaces non-ASCII characters by a -');

コードカバレッジ

テストされていない行を検出するための機能

$ ./symfony test:coverage test/unit/JobeetTest.php lib/Jobeet.class.php

実行されていない行数を知りたい場合は

$ ./symfony test:coverage --detailed test/unit/JobeetTest.php lib/Jobeet.class.php


Jobeetクラスはslugify()しか持っていないので、適当にfoo()メソッドでも追加してから実行すると分かりやすい

新しい要素のためのテストの追加

先にテストを追加することで、これから実装するコードが既に実装されているかどうか判別できると共に、実装後に期待通りの結果になっていることに自信をもてる

以下のテスト項目を追加

<?php
// test/unit/JobeetTest.php
$t = new lime_test(7, new lime_output_color());
// ...
$t->is(Jobeet::slugify(''), 'n-a', '::slugify() converts the empty string to n-a');

Jobeet::slugify()の始めに以下のコードを追加

<?php
// lib/Jobeet.class.php
public static function slugify($text)
{
  if (empty($text))
  {
    return 'n-a';
  }
 
  // ...
}

実行してもテストには成功する

$ ./symfony test:coverage --detailed test/unit/JobeetTest.php lib/Jobeet.class.php

バグのためのテスト

先程追加した処理では完全に空文字を防ぐことができていないことが、以下のテストで判明する

<?php
// test/unit/JobeetTest.php
$t = new lime_test(8, new lime_output_color());
// ...
$t->is(Jobeet::slugify(' - '), 'n-a', '::slugify() converts a string that only contains non-ASCII characters to n-a');

このテストを実行するとFailedになる。
原因は先に空文字チェックを行い、その後で非アスキー文字の除去を行っているから。
解決するには、空文字チェックを非アスキー文字の除去後に行えばよい。

<?php
// lib/Jobeet.class.php
public static function slugify($text)
{
  // ...

  if (empty($text))
  {
    return 'n-a';
  }
}

実際にslugify()をより良くするためにはどうすればいいかが記述されているが、日本語を扱うのがほとんどなので、ちょっとした参考程度でいいかなと。

Doctrineを使った単体テスト

DBの設定
# config/databases.yml
dev:
  doctrine:
    class: sfDoctrineDatabase
 
test:
  doctrine:
    class: sfDoctrineDatabase
    param:
      dsn: 'mysql:host=localhost;dbname=jobeet_test'
 
all:
  doctrine:
    class: sfDoctrineDatabase
    param:
      dsn: 'mysql:host=localhost;dbname=jobeet'
      username: root
      password: null

devは作成されないが、コマンドでやると以下の手順

$ ./symfony configure:database --name=doctrine --class=sfDoctrineDatabase --env=test "mysql:host=localhost;dbname=jobeet_test" root root
$ mysqladmin -uroot -p create jobeet_test
$ ./symfony doctrine:insert-sql --env=test
テストデータの作成

今回は本番用のデータを利用するので、data/fixtures からコピーする

$ cp -ap data/fixtures test/

そして、Doctrineの単体テスト用のファイルを作成する

<?php
// test/bootstrap/Doctrine.php
include(dirname(__FILE__).'/unit.php');
// frontendアプリケーションのtest用の設定を取得 
$configuration = ProjectConfiguration::getApplicationConfiguration( 'frontend', 'test', true);
// DatabaseManagerの作成により、databases.ymlを読み込む 
new sfDatabaseManager($configuration);
// テスト用のデータを読み込むために、doctrine:data-loadタスク
// でも利用されている、Doctrine::loadData()を利用する
Doctrine::loadData(sfConfig::get('sf_test_dir').'/fixtures');
JobeetJobクラスのテスト
<?php
// test/unit/model/JobeetJobTest.php
require dirname(__FILE__).'/../bootstrap/Doctrine.php';
 
$t = new lime_test(3, new lime_output_color());
$t->comment('->getCompanySlug()');
$job = Doctrine::getTable('JobeetJob')->createQuery()->fetchOne();
$t->is($job->getCompanySlug(), Jobeet::slugify($job->getCompany()), '->getCompanySlug() return the slug for the company');

$t->comment('->save()');
$job = create_job();
$job->save();
$expiresAt = date('Y-m-d', time() + 86400 * sfConfig::get('app_active_days'));
$t->is(date('Y-m-d', strtotime($job->getExpiresAt())), $expiresAt, '->save() updates expires_at if not set');
 
$job = create_job(array('expires_at' => '2008-08-08'));
$job->save();
$t->is(date('Y-m-d', strtotime($job->getExpiresAt())), '2008-08-08', '->save() does not update expires_at if set');
 
function create_job($defaults = array())
{
  static $category = null;
 
  if (is_null($category))
  {
    $category = Doctrine::getTable('JobeetCategory')
      ->createQuery()
      ->limit(1)
      ->fetchOne();
  }
 
  $job = new JobeetJob();
  $job->fromArray(array_merge(array(
    'category_id'  => $category->getId(),
    'company'      => 'Sensio Labs',
    'position'     => 'Senior Tester',
    'location'     => 'Paris, France',
    'description'  => 'Testing is fun',
    'how_to_apply' => 'Send e-Mail',
    'email'        => 'job@example.com',
    'token'        => rand(1111, 9999),
    'is_activated' => true,
  ), $defaults));

  return $job;
}

もし ok ではなく dubious が出たら、全てのテストが実行される前にスクリプトが終了している可能性があるので、チェックすること