Ruby on Railsのfixturesを,Excelから生成しよう (テストデータを管理しやすくするためのマクロ)
〜書き途中〜
Ruby on Railsのテストの書き方 (モデルの単体テストと,コントローラの機能テスト)
http://language-and-engineering.hatenablog.jp/entry/20091023/p1
テストデータやテストケースを作る際,下記のような要望が生じる。
- yaml形式のfixturesではなく,もっと管理しやすい+編集しやすいファイル形式でテストデータを作りたい。
- →Excelでテストデータを書きたい。
- Excelならテーブル内のカラム追加・削除・名称変更が容易になる。yamlだと大変。
- Excelなら類似したレコードを大量に作るのが簡単。オートフィル機能を使えるから。
- →Excelでテストデータを書きたい。
- テストデータとして同じデータを繰り返し書かなくてすむように,テストデータ間の include を行ないたい。
- →テストデータのDRY化。変更発生時のメンテも容易にしたい。
- 1つのモデル/コントローラのテストのために,複数パターンのテストデータを利用したい。
- →テストデータを分割し,テストデータのセットを複数持ちたい。
- もしDBの状態を1通りに限定してしまったら,十分なテストはできない。状態を網羅できない。
- チーム作業時に,もしテストデータが1セットだけではコンフリクトが多発し,管理不能になる。
- →テストデータを分割し,テストデータのセットを複数持ちたい。
- テストデータにマイグレーションを利用したい。
- →テストの事前セットアップとして,動的なテーブル生成+カラム操作が可能になる。
↓
- Excelからfixturesを生成する
- Excelから他のExcelを include 可能(⇒同じデータを繰り返し書かなくてよい。テストデータのDRY)
- 生のfixturesが扱いづらい
- 1モデルに1データではなく,複数のテストデータに分割したい
- 分割したfixturesを,サブディレクトリごとに管理したい
レールに乗っていゆくには
http://d.hatena.ne.jp/authorNari/2009...
fixture は使わない。もうこりごりです。 fixture。
テストデータを分割したい
http://www.pen-chan.jp/pen-chan/20080...
set_fixture_class :yaml名 => モデル名 で,1モデルに対し複数のyamlを作成できる
任意のファイル名とディレクトリ名の fixturesが読み込めるようになった
http://blog.dio.jp/2008/7/24/loading-...
Rails 2.2 あたりでは:
特定のテストケースでのみ使いたいようなテストデータはサブディレクトリに置いとけば良いじゃん、というのがトレンドになりそう。
テストデータは,下記のような区分を持つ。
- テスト対象アプリケーション名 (=プロジェクト名)
- テスト種別 (= "unit" もしくは "functional")
- テスト対象機能名 (=コントローラ名とか)
- テスト対象DB状態名 (= 初期状態(init) とか 通常運用状態(normal) とか)
- テストデータのテーブル
- テストデータのレコード
- テストデータのカラム
上の1〜4の区分は,テストケースのソースコードにも当てはまる。
モジュール下のモデルのフィクスチャを扱うには
http://everyleaf.com/tech/ror_tips/fi...
set_fixture_class :group_members => Group::Member のようにする
Ruby on Railsのマイグレーションで,テストデータやサンプルデータをうまく管理する方法
http://language-and-engineering.hatenablog.jp/entry/20091211/p1
利用方法
全テスト実行バッチ
#!/bin/sh # ディレクトリ情報 rails_root_dir="/root/hoge" # Railsルート(developmentモード) migrate_exec_dir="${rails_root_dir}/db/migrate" # マイグレーション実行ディレクトリ migrate_basic_dir="${rails_root_dir}/db/migrate_basic" # 基本マイグレーションのあるディレクトリ migrate_taihi_dir="${rails_root_dir}/db/migrate_taihi" # test前のマイグレーションの一時退避先フォルダ fixture_base_dir="${rails_root_dir}/test/fixtures/unit" # フィクスチャのルートフォルダ test_db_name="hoge_test" # テスト用DB名 clear echo "テスト開始" # 現在のdb/migrate以下のファイルを退避 rm -rf $migrate_taihi_dir/*.rb cp -p $migrate_exec_dir/*.rb $migrate_taihi_dir/ # 1ファイル分のテストを実行する関数 # 引数:test/unit以下のテストケースコードのパス Exec_test(){ ## テストDBを作り直して基本マイグレーションを実行 echo "db setup..." echo cd $rails_root_dir # DB su - postgres -c "dropdb ${test_db_name}" && echo "dropdb success!" echo su - postgres -c "createdb ${test_db_name}" && echo "createdb success!" echo # スキーマ(基本マイグレーション) rm -rf $migrate_exec_dir/* cp -p $migrate_basic_dir/*.rb $migrate_exec_dir/ rake environment RAILS_ENV=test db:migrate > /dev/null 2>&1 ## このテストケース用の追加マイグレーションを収集 # 基点となる番号を取得 basic_files=$(ls -1 ${migrate_basic_dir/* | wc -l}) counter_num=`expr ${basic_files} + 1` # 基本マイグレーションの次から番号を振り始める # 追加マイグレーションを取得 fixture_dir=$fixture_base_dir/${1/_test\.rb/} additional_migrations=$(find $fixture_dir -name *.rb | sort) # 実行すべき追加マイグレーション # 追加マイグレーションをリネーム for old_full_path in $additional_migrations do # フルパスからファイル名だけを取り出す old_name=`echo ${old_full_path/*\//}` # マイグレーション番号を0パディングする counter_str=$( echo ${counter_num} | perl -nle 'printf(qq{%03d},$_);' ) # 番号を振りなおしたファイル名を取得 new_name=$( echo "${old_name},${counter_str}" | perl -nle '($name,$counter_str)=split/,/;$name=~s/^\d{3}/$counter_str/g;print qq{$name};' ) # 新ファイル名でコピー cp -p $fixture_fir/$old_name $migrate_exec_dir/$new_name # 番号を増やす counter_num=`expr ${counter_num} + 1` done ## このテストケース用の追加マイグレーションを実行 rake environment RAILS_ENV=test db:migrate > /dev/null 2>&1 # テストデータが整った ## このテストケースのテストを実行 ruby $1 echo } cd $rails_root_dir/test/unit/ # -------------------- 実行対象のテストケースを指定 ここから -------------------- Exec_test account/normal_test.rb Exec_test account/empty_test.rb Exec_test bookmark/normal_test.rb Exec_test bookmark/empty_test.rb # -------------------- 実行対象のテストケースを指定 ここまで -------------------- ## テスト終了したので,テスト前のマイグレーションを復元 rm -rf $migrate_exec_dir/*.rb cp -p $migrate_taihi_dir/*.rb $migrate_exec_dir/ rm -rf $migrate_taihi_dir/*.rb # exitより下の行は実行されません。 exit # -------------------- 実行したくないテストケース ここから -------------------- Exec_test hoge/normal_test.rb Exec_test hoge/empty_test.rb
テストケース自体の書き方:
test/unit/account/normal_test.rb
この場合
- テスト種別: unit
- テスト対象機能名: account
- テスト対象DB状態名: normal
require File.dirname(__FILE__) + '../../test_helper' # アカウント機能を通常運用状態のDBでテスト class NormalTest < Test::UNit::TestCase # fixturesのパス self.fixture_path = RAILS_ROOT + "/test/fixtures/unit/account/normal" fixtures :users fixtures :groups def test_fuga # 全てのテストメソッド内で、unit/account/normal という分類のテストデータだけを使ってテストが書けます。 end end
コード
' ' ExcelからRailsのテストデータ(fixture)を生成するためのマクロ 2009.12.15. ' ' ・再帰的にデータのincludeが可能 ' ・unit/functionalやテスト対象のDB状態などに応じ,複数のデータを生成可能 ' ' フォルダ構成 ' ドキュメントフォルダ\unit\account\01_init.xls ' ドキュメントフォルダ\path.ini (RailsルートパスをSJISで書いておく) ' Railsルート\test\unit\account\01_init\各yaml ' Excelの記述方法 ' ・シート名はテーブル名とする ' ・1行目に,左から fixture名,コメント,各カラム名を記述。 ' ・2行目以降にfixtureの内容を記述。 ' ・「設定」というシート名のシートは読み取られない。 ' includeの方法 ' fixture名のところに下記のように記述(末尾はExcel上での行番号) ' #include /unit/account/00_base.xls/groups/4 ' #include /unit/account/00_base.xls/users/2-6 ' ボタン押下時 Sub fixture生成() ' 生成先パスを認識・準備 dest_dir_path = get_dest_dir_path() setup_dest_path_for_output dest_dir_path ' 全シートに対して For Each s In Worksheets If s.Name <> "設定" Then ' 1シート分のfixtureを生成 table_name = s.Name 'MsgBox table_name make_one_fixture table_name, dest_dir_path End If Next s ans = MsgBox("終了しました。出力先フォルダを開きますか?", vbYesNo) If ans = vbYes Then cmd_str = "explorer " & dest_dir_path & "" 'MsgBox cmd_str Shell cmd_str End If End Sub ' -------------------- fixture設置先パスに関する関数 -------------------- ' 個々のfixture設置先のフォルダパスを取得します Function get_dest_dir_path() rails_root_path = get_rails_root_path dest_dir_path = rails_root_path _ & "\" _ & get_doc_test_type _ & "\" _ & get_doc_test_function_name _ & "\" _ & get_doc_test_db_cond _ & "\" 'MsgBox dest_dir_path get_dest_dir_path = dest_dir_path End Function ' fixture設置対象のRailsルートパスを返します Function get_rails_root_path() ' iniファイルの存在判定 ini_path = get_ini_path If Dir(ini_path) = "" Then MsgBox "設定ファイル" & ini_path & "は存在しません。" Exit Function End If ' ファイルを開く fp = FreeFile Open ini_path For Input As #fp ' 読み取り(行末の改行はトリムされる) Line Input #fp, rails_root_path Close #fp ' Railsのルートパスの存在判定 If Dir(rails_root_path, vbDirectory) = "" Then MsgBox "設定ファイルに記載されたパス" & rails_root_path & "は存在しません。" Exit Function End If 'MsgBox rails_root_path get_rails_root_path = rails_root_path End Function ' 指定されたフォルダのパスを利用可能にします。(引数のフォルダパスは末尾に\をつけること) ' フォルダを再帰的に作成しています。 Sub prepare_dir_path(dir_path) ' 最深層のディレクトリが存在するか If Dir(dir_path, vbDirectory) <> "" Then ' もうこれでOK 'MsgBox dir_path & "は存在" Else ' 存在しないので・・・ 'MsgBox dir_path & "は存在せず" ' (1)一段浅い階層を利用可能にする Dim arr As Variant arr = Split(dir_path, "\") upper_dir_path = "" For i = 0 To UBound(arr) - 2 upper_dir_path = upper_dir_path & arr(i) & "\" Next i 'MsgBox dir_path & "を作る前に" & upper_dir_path & "を検査" prepare_dir_path upper_dir_path ' (2)この階層にそのディレクトリを実際に作る MkDir dir_path 'MsgBox dir_path & "を作成" End If End Sub ' あるフォルダをfixture書き出し用に整えます。 Sub setup_dest_path_for_output(dest_dir_path) ' フォルダがなければ作成 prepare_dir_path dest_dir_path ' 中身のymlファイルを全削除 continue_flag = True Do While continue_flag f = Dir(dest_dir_path & "*.yml") If f <> "" Then Kill dest_dir_path & f Else continue_flag = False End If Loop End Sub ' -------------------- このブックの設置パスに関する関数 -------------------- ' fixture設置対象を指定している設定ファイルのパスを返します。 Function get_ini_path() doc_root_path = get_doc_root_path ini_path = doc_root_path & "path.ini" 'MsgBox ini_path get_ini_path = ini_path End Function ' fixture関係のドキュメントのルートパスを返します。 Function get_doc_root_path() Dim arr As Variant arr = Split(ThisWorkbook.Path, "\") doc_root_path = "" For i = 0 To UBound(arr) - 2 doc_root_path = doc_root_path & arr(i) & "\" Next i 'MsgBox doc_root_path get_doc_root_path = doc_root_path End Function ' このブックがunit用かfunctional用かを返します。 Function get_doc_test_type() Dim arr As Variant arr = Split(ThisWorkbook.Path, "\") doc_test_type = arr(UBound(arr) - 1) 'MsgBox doc_test_type get_doc_test_type = doc_test_type End Function ' このブックのテスト対象機能を返します。 Function get_doc_test_function_name() Dim arr As Variant arr = Split(ThisWorkbook.Path, "\") doc_test_function_name = arr(UBound(arr)) 'MsgBox doc_test_function_name get_doc_test_function_name = doc_test_function_name End Function ' このブックのテスト対象DB状態を返します。 Function get_doc_test_db_cond() Dim arr As Variant arr = Split(ThisWorkbook.Name, ".") doc_test_db_cond = arr(0) 'MsgBox doc_test_function_name get_doc_test_db_cond = doc_test_db_cond End Function ' -------------------- fixture書き込みに関する関数 -------------------- ' 1シート分のフィクスチャを作成します。 Sub make_one_fixture(table_name, dest_dir_path) ' 出力先ファイル名 output_path = dest_dir_path & table_name & ".yml" 'MsgBox output_path ' 出力内容 content_str = get_fixture_content(table_name) ' 書き込み実行 overwrite_utf8n output_path, content_str End Sub ' UTF8Nの上書きモードでファイル書き込みします。 Sub overwrite_utf8n(output_path, temp_str) ' 定数 Const adTypeText = 2 Const adTypeBinary = 1 ' UTF-8で書き出し(あとでBOMを除去する) Dim ados As Object Set ados = CreateObject("ADODB.Stream") ados.Open ados.Type = adTypeText ados.Charset = "UTF-8" ados.WriteText temp_str ' 先頭のBOM取り ados.Position = 0 ados.Type = adTypeBinary ados.Position = 3 byte_data = ados.Read ados.Close ' UTF-8Nコードのデータを保存 ados.Open ados.Type = adTypeBinary ados.Write byte_data ados.SaveToFile output_path, 2 ados.Close End Sub ' 1シート分のfixtureのデータを文字列として作成します。 Function get_fixture_content(table_name) br = vbNewLine ' 改行 s = "# このテストデータの情報" & br & _ "# ・テスト種別 : " & get_doc_test_type & br & _ "# ・テスト対象機能 : " & get_doc_test_function_name & br & _ "# ・テストDB状態 : " & get_doc_test_db_cond & br & _ "# ・投入テーブル : " & table_name & br & _ "# ・生成元 : " & ThisWorkbook.FullName & br & _ "# " & br & _ "# ※ 手動で編集しないでください。" & br & br & br ' 全行に対して offset_y = 2 ' この行から下にデータ行がある continue_flag = True y = offset_y current_wb_name = ThisWorkbook.Name Do While continue_flag 'MsgBox "y = " & y ' この行にはデータがあるか If is_valid_fixture_line(current_wb_name, table_name, y) Then s = s & get_fixture_content_line(current_wb_name, table_name, y) Else 'MsgBox "シート内で終了" continue_flag = False End If ' 次の行へ y = y + 1 Loop get_fixture_content = s End Function ' ある行をfixtureとして解析すべきかどうか判定します。 Function is_valid_fixture_line(wb_name, table_name, y) offset_x = 3 ' この列から右にカラムがある include_x = 1 ' この列にinclude命令が書いてある ret = False text_value = Workbooks(wb_name).Sheets(table_name).Cells(y, offset_x).Value include_info = Workbooks(wb_name).Sheets(table_name).Cells(y, include_x).Value ' この行には直接書かれたデータがあるか If Len(Replace(text_value, " ", "")) > 0 Then ret = True ElseIf InStr(include_info, "#include ") = 1 Then ret = True End If is_valid_fixture_line = ret End Function ' 解析すべきと判断された1行分のfixtureのデータを解析し,文字列として返します。 Function get_fixture_content_line(wb_name, table_name, y) br = vbNewLine ' 改行 offset_x = 3 ' この列から右にカラムがある include_x = 1 ' この列にinclude命令が書いてある text_value = Workbooks(wb_name).Sheets(table_name).Cells(y, offset_x).Value include_info = Workbooks(wb_name).Sheets(table_name).Cells(y, include_x).Value s = "" If Len(Replace(text_value, " ", "")) > 0 Then ' この行に直接記述されたデータを取得 s = s & get_fixture_content_line_directly(wb_name, table_name, y) s = s & br & br ElseIf InStr(include_info, "#include ") = 1 Then ' この行から参照されたデータを取得 s = s & get_fixture_content_included(include_info) End If get_fixture_content_line = s End Function ' 1行分のfixtureのデータをシートから直接読み取り,文字列として作成します。(includeではない) Function get_fixture_content_line_directly(wb_name, table_name, y) br = vbNewLine ' 改行 fixture_name_x = 1 comment_x = 2 offset_x = 3 ' この列から右にカラムがある s = "" ' コメントを追加 comment_str = Workbooks(wb_name).Sheets(table_name).Cells(y, comment_x).Value comment_str = Replace(comment_str, vbLf, br & "# ") ' セル内の改行はvbLf If Len(comment_str) > 0 Then s = s & "# " & comment_str & br End If ' フィクスチャ名を追加 fixture_name = Workbooks(wb_name).Sheets(table_name).Cells(y, fixture_name_x).Value s = s & fixture_name & ": " & br ' 全列に対して x = offset_x continue_flag = True Do While continue_flag column_name = Workbooks(wb_name).Sheets(table_name).Cells(1, x).Value ' カラムのある列ならば If Len(column_name) > 0 Then column_value = Workbooks(wb_name).Sheets(table_name).Cells(y, x).Value column_value = Replace(column_value, vbLf, "\r\n") ' この列のデータを追加 s = s & " " & column_name & ": " & column_value & br Else continue_flag = False End If x = x + 1 Loop get_fixture_content_line_directly = s End Function ' -------------------- 他ブックの参照に関する関数 -------------------- ' 読み込み記法を解析して,読込先の情報を返します。 Function get_fixture_content_included(include_info) ' 解析 include_path = guess_include_path(include_info) include_sheet_name = guess_include_sheet_name(include_info) include_lines = guess_include_lines(include_info) line_start = include_lines(0) line_end = include_lines(1) 'MsgBox include_path & "というブックの" & include_sheet_name & "というシートの" _ ' & line_start & "行目から" & line_end & "行目を読み込み" ' 開く Set ReturnBook = ActiveWorkbook Set TargetBook = Workbooks.Open(include_path) target_wb_name = TargetBook.Name ReturnBook.Activate Application.ScreenUpdating = True ' 情報抽出 s = "" br = vbNewLine ' 改行 For i = line_start To line_end s = s & get_fixture_content_line(target_wb_name, include_sheet_name, i) Next i ' 閉じる(?) 編集中に勝手に閉じると困るかも 'TargetBook.Saved = True 'TargetBook.Close get_fixture_content_included = s End Function ' 読込先のブックのフルパスを返します。 Function guess_include_path(include_info) t = Replace(include_info, "#include ", "") Dim arr As Variant arr = Split(t, "/") ret = get_doc_root_path() _ & arr(1) _ & "\" _ & arr(2) _ & "\" _ & arr(3) guess_include_path = ret End Function ' 読込先のブックの読むべきシート名を返します。 Function guess_include_sheet_name(include_info) t = Replace(include_info, "#include ", "") Dim arr As Variant arr = Split(t, "/") ret = arr(4) guess_include_sheet_name = ret End Function ' 読込先のブックの読み込むべき行情報(開始行,終了行)を返します。 Function guess_include_lines(include_info) t = Replace(include_info, "#include ", "") Dim arr As Variant arr = Split(t, "/") num_str = arr(5) ' 開始行と終了行を解析 If InStr(num_str, "-") > 0 Then ' 複数行の場合("1-2"など) Dim arr2 As Variant arr2 = Split(num_str, "-") line_start = Val(arr2(0)) line_end = Val(arr2(1)) Else ' 単一行の場合("3"など) line_start = Val(num_str) line_end = line_start End If 'MsgBox line_start 'MsgBox line_end ret = Array(line_start, line_end) guess_include_lines = ret End Function