unshiu

最近の更新履歴

テストコードの書き方


テストコードのいろいろをまとめます。

目次

  1. 1 コミット手順
  2. 2 テスト対象スコープ
    1. 2.1 メソッド内にコメントを書かなければ理解できないようなメソッド
    2. 2.2 2回以上修正したメソッド
    3. 2.3 外部アプリケーションと連携する部分
  3. 3 基本的なこと
    1. 3.1 他のテストに依存したテストコードはかかない
    2. 3.2 メソッドを修正したらテストが通らなかったのでテストをコメントアウトしてコミットしない
    3. 3.3 メソッドにバグが発見されたときには、そのバグを再現するテストコードを書いた上で修正作業をする
    4. 3.4 1テストメソッドでテストすることは1つのみ
    5. 3.5 環境へ依存するコードはかかない
    6. 3.6 fixtures のデータに極力依存しないようにする
    7. 3.7 テストに実装をあわせない
    8. 3.8 テスト用にコードを書き換えるなければテストが通らない実装にはしない
  4. 4 ディレクトリルール
  5. 5 modelの単体テスト
    1. 5.1 トランザクションをはさむ場合
  6. 6 controllerの機能テスト
    1. 6.1 ログイン状態にする
    2. 6.2 画像が関係するcontrollerのテストをする
    3. 6.3 携帯画面のテストをする
    4. 6.4 携帯でのform内容はキャリアごとに変換がおこなわれることを忘れない
    5. 6.5 1テストの中でpost/getするのは1つ
  7. 7 SeleniumのViewテスト
  8. 8 テストコードテクニック
    1. 8.1 日本語でテストメソッドを定義する
    2. 8.2 find_by_idを利用する
    3. 8.3 デフォルトで生成されるテストデータ .yml ファイルはつかわないほうがいい

コミット手順

  1. svn up
  2. rake test
  3. svn commit

テスト対象スコープ

理想をいえば全ての可能性を網羅することが最適ですが、事実上不可能です。 大事なのは重要もしくはミスが起こりやすいところが網羅されているかです。

少なくとも以下のポイントに当てはまるところは重点的にテストコードを書くべきです。

メソッド内にコメントを書かなければ理解できないようなメソッド

ロジックが複雑なためにどうしてもコメントで補記しないとわからないメソッドはどうしても存在してしまいます。 こういったメソッドはテストを書くことでさらに仕様を明確にできます。

2回以上修正したメソッド

なんのミスもなく、書いた後に1度も修正しないようなメソッドよりも、書いた後に拡張した、なんらかのミスで修正したメソッド に重点的にテストは書くべきです。

外部アプリケーションと連携する部分

こちらが何を想定して何が想定外なのが明確になります。 外部アプリケーションの仕様が先方都合や提供もとの都合で変更される可能性も十分あるためそのときメンテナンスコストをふくめて も十分書く価値があります。 なお、その場合、モック・スタブを使い外部アプリケーションに依存せずにテストが通ることは必須です。

基本的なこと

他のテストに依存したテストコードはかかない

×hogehogeのテストはhogeのテストが終わった後でないと成功しない

メソッドを修正したらテストが通らなかったのでテストをコメントアウトしてコミットしない

  • 「あとでやろうと思った」→大概一生やらない。
  • テストが失敗している状態で一時的にコミットしておけば忘れることはないのでどうしても一時的に失敗する場合はそのままにしておく。

メソッドにバグが発見されたときには、そのバグを再現するテストコードを書いた上で修正作業をする

デグレ防止。 メソッドのバグだけ直すのは直したとみなしません。

1テストメソッドでテストすることは1つのみ

状態が変わる場合や、前提が変わる場合などは必ず、メソッドをわけてください。 特にコントローラ系で複数のことをしようとすると正常にテスト結果がかえらない場合さえあります。

環境へ依存するコードはかかない

Windowsでも、MacでもLinuxでも動くようにする

fixtures のデータに極力依存しないようにする

例えば特別な条件のユーザ数を出力するメソッドがあった場合、単純に数をテストするテストコードを書いた場合、テストデータを増やすたびに変更が必要なります。 テストの数がふえてくると足枷になりかねないので、極力依存しないほうがベストです。

ただこういう場合の多くは、本来テストしたいのは数ではなく、「その条件にはてまっているか?」であるので、この場合、かえってきたデータ全てがその条件をみたしているかをチェックするべきです。

なおこの場合、複数の条件があればそれを全て包括するだけのテストが書かれていることが条件です。

テストに実装をあわせない

テストは本番用のデータをシュミレートして再現しますが、 テストが増えてくると実際の運用上ありえない状態を再現してしまうことがあります。 例えば A テーブル と B テーブルのあるカラムの情報はつねに一緒であるはずなのに、テストデータ上は一緒でないために本番とは違う状態になるなど。 この場合テストが通らないからといって A テーブル と B テーブルの情報が違う場合に対応できるように書き換えるアプローチはNGです。

テスト用にコードを書き換えるなければテストが通らない実装にはしない

rubyは動的にメソッドを書き換える機構があったり、モック、スタブも使えるのでそれを利用しましょう。

ディレクトリルール

以下のルールに従ってtestファイルを作成してください。

test/file                  # テストで利用するcsvなどのファイル
/fixures # テストデータymlファイル
/file_column
/xxx_image # ymlファイルに定義されている画像実ファイルなど
/functional # controller系などの機能テスト
/script # /script以下におかれるバッチ作成などを行うスクリプトのテスト
/integration
/mocks # モック
/tmp # テスト用の一時ディレクトリ。この以下にコミットが必要なファイルは置いてはいけない
/unit # model系などの単体テスト
/lib # /lib などに置かれるライブラリのテスト
/worker # backgroundrb の worker のテスト

modelの単体テスト

トランザクションをはさむ場合

modelのテストが行われる場合、以下のような手順がふまれます。

1. fixtureのデータをロード
2. テストを実行
 データはcommitしない
3. ロールバックして1と同じ状態に戻す

そのため、テスト中でトランザクションをはさむような場合は、明示的にコミットされてしまうためにロールバックできず、次のテストに影響をあたえてしまいます。
それを防ぐために、トランザクションをはさむテストの場合、以下のように use_transactional_fixtures を追加してください。

class UnshiuTest < Test::Unit::TestCase
self.use_transactional_fixtures = false

fixtures :base_users

def test_mikan
#transactionを含むテストを実行
end
end

これによりfixtureの更新処理にはロールバックではなくDBへのinsert/deleteが行われるようになります。
ただしfixturesでロードしているデータ以外は元に戻らないのでテスト中で処理した全てのファイルをfixturesで設定してください。

controllerの機能テスト

共通系のメソッドをmoduleでくくりだしているので、それを利用してください。

利用例)

class Hoge < Test::Unit::TestCase
include TestUtil::Base::PcControllerTest
end

詳細は以下を確認してださい

lib/test_util.rb

ログイン状態にする

TestUtil::Base::PcControllerTest, TestUtil::Base::MobileControllerTest等を利用している場合はmodule側で AuthenticatedTestHelper をincludeしているので 個別クラスでincludeする必要はありません。

以下はそれを利用しない一般的なacts_as_authenticatedを使った場合の例です。

1. AuthenticatedTestHelperをインクルードする

include AuthenticatedTestHelper

2. login_as の引数にloginのカラム値を渡してあげる。

login_as :quentin

このメソッドが呼ばれたあとはログイン状態とみなします。 テスト全体でログイン状態でいたい場合は、setupに書いておけば問題ありません。

画像が関係するcontrollerのテストをする

以下の説明は file_column でデータをファイルとして保持している場合です。

画像をファイルデータとして保持している場合、テストデータとして画像の実ファイルが必要になります。
テストとしてアップロード処理をすると test/tmp/file_column/ などへアップロードされます。それと同じ状態をつくるために以下のようにsetupコマンドで テストデータファイルをテスト用ディレクトリにセットアップする必要があります。

def setup
# 通常のsetup内容
setup_fixture_files
end

テスト後は必要ないので以下のようにteardownでファイルを消します

def teardown
teardown_files
end

テスト画像ファイルデータは下記ディレクトリ以下に格納して下さい。

test/fixture/file_column/#{model_name}/

携帯画面のテストをする

agentが通常のままだとテンプレートの選択などに失敗するので偽装が必要です。 以下のようにMobileControllerTest?を継承しておくとそのへんを勝手にやってくれます。

class TestClass < Test::Unit::TestCase
include TestUtil::Base::MobileControllerTest
end

ただし setup メソッドで事前にリクエストにユーザエージェントを設定しているので以下のようにmoduleで定義してある上位の setupメソッドを呼び出してください。

def setup
super
end

携帯でのform内容はキャリアごとに変換がおこなわれることを忘れない

日本語で何かをsubmitした場合などキャリアごとに処理方法(文字コードなど)が違います。 アプリケーション側はjpmobileがうまいことやっているので開発者はキャリアの差を意識する必要はありません。
ただしテストコードもこの同じ仕組みを使いますが、携帯が行っている文字コード変換まではエミュレートしないため、テストコードで日本語の内容をsubmitすると
DBに文字化けした状態で挿入される場合があります。
このこと自体は例外さえおこらなければ特に問題ありませんが、テストとして、処理後にDBへ正しい値がはいったことを確かめたりなんかするとうまくいきません。
その際は、日本語を含まない形でsubmitする、内容ではなくレコード数が増えたかなどをチェックするなどをしてテストとしてください。

1テストの中でpost/getするのは1つ

画面の遷移が必要な場合はintegrationテストになります。複数のpost/getをするのは意味的には1のリクエストの中で複数のpost/getをしていることになるので、テストコードして正しくありません。

SeleniumのViewテスト

テストコードテクニック

日本語でテストメソッドを定義する

正常系だけなら test_xxx などといった形でもあまり問題はありませんが、テストパターンが増え複雑なテストになると、無駄にメソッド名が長くなったり 本来の意味がわからなくなってしまいます。そんな場合におすすめな日本語でテストメソッドを書く方法です。

define_method('test: hogehogeのテストをする')
assert true
end

setupやfixturesも何の問題もなく利用できます。

find_by_idを利用する

find(1) はもし id が存在しない場合やそのレコードが削除済みの場合、例外をなげてしまいます。
一方、 find_by_id の場合、存在しない場合やレコードが削除済みの場合、nilオブジェクトを返すだけで例外は返しません。
テストコード内でテスト後のレコードチェックをする場合などはいちいち例外を補足するのもうざったいので、find_by_id で検索して 返ってきたオブジェクトをassertするのが楽です。

デフォルトで生成されるテストデータ .yml ファイルはつかわないほうがいい

rails よりgenerateされたテストデータ用の fixtures ymlファイルは以下のような構文です。

one:
id: 1
two:
id: 2

これにそって one, two, three と増やしていくと10をこえたあたりで面倒になります。ついでにスペルミスする可能性も増えます。

1:
id: 1
2:
id: 2

a:
id: 1
b:
id: 2

でも何の問題もないので無理に英語にする必要はありません。
構造的にはファイル内でユニークでないとあとからロードした値を上書きしてしまうため、ファイル内でユニークである必要ありますが、それ以外に制約はありません。ただしその値をキーにテストコードから値を指定できるのでテストデータに意味付けしない場合などは適切な名前を付けると便利です。