スポンサーリンク

Ruby on Railsのfixturesを,Excelから生成しよう (テストデータを管理しやすくするためのマクロ)

〜書き途中〜



Ruby on Railsのテストの書き方 (モデルの単体テストと,コントローラの機能テスト)
http://language-and-engineering.hatenablog.jp/entry/20091023/p1

テストデータやテストケースを作る際,下記のような要望が生じる。

  • yaml形式のfixturesではなく,もっと管理しやすい+編集しやすいファイル形式でテストデータを作りたい。
    • Excelでテストデータを書きたい。
      • Excelならテーブル内のカラム追加・削除・名称変更が容易になる。yamlだと大変。
      • 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 あたりでは:
特定のテストケースでのみ使いたいようなテストデータはサブディレクトリに置いとけば良いじゃん、というのがトレンドになりそう。


テストデータは,下記のような区分を持つ。

  1. テスト対象アプリケーション名 (=プロジェクト名)
  2. テスト種別 (= "unit" もしくは "functional")
  3. テスト対象機能名 (=コントローラ名とか)
  4. テスト対象DB状態名 (= 初期状態(init) とか 通常運用状態(normal) とか)
  5. テストデータのテーブル
  6. テストデータのレコード
  7. テストデータのカラム


上の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

ダウンロード
http://www.name-of-this-site.org/codi...

利用方法


全テスト実行バッチ

#!/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