「実行可能ドキュメント」が満たすべき性質 − テスト自動化ツール「Excelenium」で使われている技術や手法
Exceleniumとは,Webアプリのテスト自動化ツール。
"Excelenium"(エクセレニウム)で,快適な自動回帰テストを (Seleniumのテストスクリプトとテスト仕様書を自動生成)
http://language-and-engineering.hatenablog.jp/entry/20090524/p1
Excelenium (テスト対象として,Ruby on Rails のサンプルアプリつき)
http://www.name-of-this-site.org/codi...
このエントリでは,
- Exceleniumのコンセプトである「実行可能ドキュメント」という概念について,少々解説する。
- そのコンセプトを実現するために,Exceleniumにどのような技術や手法が使われているか,を解説する。
「実行可能ドキュメント」というコンセプト
書籍「達人プログラマー」第2章,「二重化の過ち:ドキュメントとコード」より:
「・・・顧客は当然の事ながら,大量のテスト仕様を要求し,
ソフトウェアを納品するたびにすべてのテストを行なうよう要求しました。このため,テスト自体が仕様を正確に反映したものであることを保証できるよう,
ドキュメント自体から直接テストを行なうプログラムを作成して,
テストを自動化したのです。顧客が仕様を変更すると,一連の関係しているテストも自動的に変更されるのです。
こういった手続きがしっかりしたものであることを,
いったん顧客に納得さえしてもらえれば,
受け入れテストはたった数秒しかかからないようになるのです。」
また,同書の第8章,「すべてはドキュメント:実行可能ドキュメント」の項も参照。
上の記述は,私の物の見方(開発観)に大きな影響を与えた。
Exceleniumは,その結果生まれた。
Exceleniumには,以下のようなメリットがある。
- 作成(新規作成と保守)が楽。読み書きが楽。
- 日本語(論理名)で読み書きできる。内部設計を意識しなくて済む
- 操作名に日本語を使える
- 画面項目名に日本語を使える
- 説明文がリアルタイムで表示される
- 手書きするものが少ない
- 自動採番
- アプリ名や機能区分
- 確認項目のハイライト
- Excelの生来の入力しやすさを転用できる(フィルや関数,切り張りしやすさなど)
- 実行が楽
- テストスクリプトの生成が楽
- 生成と実行がワンセットなので,生成を意識しなくて済む。
- 生成に先立つフォルダを自動生成してくれる。
- テスト開始が楽
- 実行用のブラウザを自動で立ち上げてくれる。
- 実行用のURLを自動で開いてくれる。
Exceleniumの持つこれらの特徴は,「『実行可能ドキュメント』が満たすべき性質のサンプル」と捉えることができる。
※「Selenium」自身の持つメリットは,この中に含めていない。
また,Exceleniumの後継である「IE AutoTester」には,上記に加えてさらに以下のようなメリットがある。
- 結果の記録が楽
- 全テスト結果を自動で記録してくれる
下記では,上に挙げたメリット一覧のそれぞれを解説する。
「論理名でテストケースを記述する」というスタイル
「日本語の操作名でテストケースを記述する」というスタイル
実現するための技法
前項も含め,
「テストスクリプト生成のタイミングで,論理名を物理名に置換する。」
という処理を行っているわけだが,その実体は,ExcelのVLOOKUP関数である。
VBAのsousa2command_name関数を参照。
ret = Application.WorksheetFunction.VLookup( _ sousa, _ Sheets("コマンド一覧").Range("C6:H25"), _ 3, False _ )
このコードは,日本語の操作名を,「コマンド一覧」シートの該当Range内の3列目の値に置換している。
「コマンド一覧」シートが,論理名と物理名をつなぐための「辞書」の役割を果たしている。
記述した操作の「日本語の説明文章」が,リアルタイムで表示される。
コマンドの自動分類,「確認」項目数の可視化
ブック情報を各シートで自動的に共有。手書き不要
シート情報をシート内で自動的に共有。手書き不要
シート上の記述に余裕を持たせる
自動採番
生成の前提条件を自動的に満たしてくれる
実行画面にアクセスする手間を省く
補足:心理的な面
さらに工夫として,これらのメリットを,実際にメリットと「感じさせる」ように意識している。
例えば
- 全シートに,(わざと)でかでかと「この仕様書の全テストを実行」というボタンを配置してある。
- →「文面に残るだけじゃなくて,実行もしてくれるんだ。」というメッセージが伝わる。
- →書きたくなる。
- 表紙にも,(わざと)「実行可能テスト仕様書」というタイトルを掲げてある。
- →「この文書のために割く労力は,机上のものにならず,実行可能であり,現実の価値を生むのだ。」と確信させる。
- →やる気が出る。このテスト仕様書をメンテしたくなる。
- →テスト工程の品質が上がる。アプリ本体の品質も上がる。
- セルのコメントに「入力不要。」というのを散りばめる。
- →「自分が今まで手動でやってきたことは,本当はやる必要の無いことだったんだ」と開眼させる。
- →自動化への認識が高まる。より価値の高い作業を人手で行なうようになる。
つまり,実際に楽であるだけでなく,楽であることを心理的に強調し,良い開発サイクルを生ませようとしている。
そういう意図のあるドキュメントなのである。
補足:VBAのコード全文
全文を掲載する。
' 全テストを実行します Sub execAllTests() ' 全シートのテストケースを採番 makeAllNumbers ' 全シートのテストスクリプトを生成 makeAllScripts ' ブラウザを起動して全テストを実行 execTestsBrowser End Sub ' ---------- 環境設定事項を取得する関数 ---------- ' テストケースのシートかどうか判定します Private Function isTestCaseSheet(ws) If ws.Name = "初期設定" _ Or ws.Name = "コマンド一覧" _ Or ws.Name = "項目マスタ" _ Or ws.Name = "テストケース原紙" Then isTestCaseSheet = False Else isTestCaseSheet = True End If End Function ' シート終端とみなす空行数を取得します Private Function getOverLinenum() getOverLinenum = Worksheets("初期設定").Cells(33, 2).Value End Function ' ブックの持つ大区分名を取得します Private Function getLargeCateName() getLargeCateName = Worksheets("初期設定").Cells(17, 6).Value End Function ' ブックの持つ大区分論理名を取得します Private Function getLargeCateRonriName() getLargeCateRonriName = Worksheets("初期設定").Cells(17, 2).Value End Function ' システム名を取得します Private Function getSystemName() getSystemName = Worksheets("初期設定").Cells(11, 6).Value End Function ' SeleniumのルートURLを取得します Private Function getSeleniumURL() getSeleniumURL = Worksheets("初期設定").Cells(29, 2).Value End Function ' Seleniumの設置パスを取得します Private Function getSeleniumPath() getSeleniumPath = Worksheets("初期設定").Cells(23, 2).Value End Function ' ブラウザの呼び出しコマンドを取得します Private Function getBrowserCommand() getBrowserCommand = Worksheets("初期設定").Cells(40, 2).Value End Function ' 操作名称をselenのコマンドに変換 Function sousa2command_name(sousa) ' VLOOKUPの保険 On Error GoTo ErrorHandler ret = Application.WorksheetFunction.VLookup( _ sousa, _ Sheets("コマンド一覧").Range("C6:H25"), _ 3, False _ ) ' http://www.eurus.dti.ne.jp/~yoneyama/Excel/vba/vba_ws_kansu.html#WorksheetFunction sousa2command_name = ret Exit Function ' 参照失敗時 ErrorHandler: sousa2command_name = sousa ' そのまま返す End Function ' 引数を項目マスタで変換 Function arg2element_name(arg) ' VLOOKUPの保険 On Error GoTo ErrorHandler ret = Application.WorksheetFunction.VLookup( _ arg, _ Sheets("項目マスタ").Range("D5:E200"), _ 2, False _ ) ' http://www.eurus.dti.ne.jp/~yoneyama/Excel/vba/vba_ws_kansu.html#WorksheetFunction arg2element_name = ret Exit Function ' 参照失敗時 ErrorHandler: arg2element_name = arg ' そのまま返す End Function ' ---------- 採番に関する関数 ---------- ' 全テストケースを自動採番します Sub makeAllNumbers() ' 全ワークシートに対して For Each ws In Worksheets If isTestCaseSheet(ws) Then ws.Activate ' このシートの採番 makeNumbersOneSheet End If Next ws End Sub ' アクティブなシートのテストケースを自動採番します Sub makeNumbersOneSheet() overLinenum = getOverLinenum ' 操作が書かれている列の開始セル offset_y = 9 isEmpty_x = 5 testcase_name_x = 4 ' 番号を書く列 numbering_x = 2 ' 全行について continue_flag = True continue_counter = 0 output_counter = 1 y = offset_y Do While continue_flag = True ' 操作が書かれているか If Len(Cells(y, isEmpty_x).Value) > 0 Then continue_counter = 0 ' テストケース名が書かれているか If Len(Cells(y, testcase_name_x).Value) > 0 Then ' 番号を記入 Cells(y, numbering_x).Value = output_counter output_counter = output_counter + 1 Else ' 空白を記入 Cells(y, numbering_x).Value = "" End If Else ' 空白を記入 Cells(y, numbering_x).Value = "" ' リミットに一歩近づく continue_counter = continue_counter + 1 ' 一定数以上の空行が続いたか If overLinenum <= continue_counter Then continue_flag = False End If End If ' 次の行へ y = y + 1 Loop End Sub ' ---------- スクリプト生成に関する関数 ---------- ' 作成されるフォルダ構造 ' \selenium\tests\システム名\ブックの機能区分\シート番号ごとにhtml ' 全シートのテストスクリプトを生成 Sub makeAllScripts() ' フォルダ準備 prepareSystemNameDir prepareLargeCateNameDir ' 全ワークシートに対して cnt = 1 For Each ws In Worksheets If isTestCaseSheet(ws) Then ws.Activate ' このシートのスクリプト作成 makeScriptOneSheet (cnt) cnt = cnt + 1 End If Next ws ' 全シートをまとめる一覧HTMLを作成 makeScriptListPage End Sub ' アクティブなシートのスクリプトを生成 Sub makeScriptOneSheet(sheet_index) overLinenum = getOverLinenum br = vbNewLine ' 改行 tb = vbTab ' タブ ' 出力ファイル名 output_name = "Test" & sheet_index & ".html" output_path = getSeleniumPath _ & "\tests\" _ & getSystemName _ & "\" _ & getLargeCateName _ & "\" _ & output_name ' 操作が書かれている列の開始セル offset_y = 9 offset_x = 5 testcase_name_x = 4 skip_command_x = 3 ' ファイルを開く fp = FreeFile Open output_path For Output As #fp Print #fp, "<table border='1'><tbody>" ' 全行について continue_flag = True continue_counter = 0 y = offset_y Do While continue_flag = True ' テストケース名が書かれているか If Len(Cells(y, testcase_name_x).Value) > 0 Then casename = Cells(y, testcase_name_x).Value ' テストケース名の行とする temp_caseline = "<tr>" & br _ & tb _ & "<td rowspan='1' colspan='3' align='center' bgcolor='#ddddff'>" _ & casename _ & "</td>" _ & br _ & "</tr>" Print #fp, temp_caseline End If ' 操作が書かれているか?しかもスキップ対象でないか If (Len(Cells(y, offset_x).Value) > 0) _ And (Len(Cells(y, skip_command_x).Value) = 0) Then continue_counter = 0 ' 操作の3要素を収集 sousa = Cells(y, offset_x).Value arg1 = Cells(y, offset_x + 1).Value arg2 = Cells(y, offset_x + 2).Value ' 変換 command_name = sousa2command_name(sousa) arg1 = arg2element_name(arg1) arg2 = arg2element_name(arg2) ' 書き込み temp_line = "<tr>" & br _ & tb & "<td>" & command_name & "</td>" & br _ & tb & "<td>" & arg1 & "</td>" & br _ & tb & "<td>" & arg2 & "</td>" & br _ & "</tr>" ' MsgBox temp_line Print #fp, temp_line Else ' リミットに一歩近づく continue_counter = continue_counter + 1 ' 一定数以上の空行が続いたか If overLinenum <= continue_counter Then continue_flag = False End If End If ' 次の行へ y = y + 1 Loop ' 終了 Print #fp, "</tbody></table>" Close #fp End Sub ' フォルダを準備します Sub prepareSystemNameDir() target_dir = getSeleniumPath & "\tests\" & getSystemName If Dir(target_dir, vbDirectory) = "" Then ' http://www.k1simplify.com/vba/tipsleaf/leaf243.html MkDir target_dir ' http://www.optimizm.jp/003/vba_file_005.shtml End If End Sub ' フォルダを準備します Sub prepareLargeCateNameDir() target_dir = getSeleniumPath & "\tests\" & getSystemName & "\" & getLargeCateName If Dir(target_dir, vbDirectory) = "" Then MkDir target_dir End If End Sub ' ブック単位での一覧を作成 Sub makeScriptListPage() ' このブックの持つ大区分名を取得 large_cate_name = getLargeCateName large_cate_ronri_name = getLargeCateRonriName system_name = getSystemName ' 出力パス output_name = large_cate_name & "Test.html" output_path = getSeleniumPath & "\tests\" & system_name & "\" & output_name ' 書き込み fp = FreeFile Open output_path For Output As #fp Print #fp, "<table id=suiteTable cellpadding=1 cellspacing=1 border=1 class=selenium>" Print #fp, "<tbody>" Print #fp, "<tr><td><b>" & large_cate_ronri_name & "</b></td></tr>" ' 全ワークシートに対して cnt = 1 For Each ws In Worksheets If isTestCaseSheet(ws) Then ws.Activate ' このシートの小区分名を取得 small_cate_name = Cells(5, 3).Value Print #fp, "<tr><td><a href=""./" _ & large_cate_name _ & "/Test" _ & cnt _ & ".html"">" _ & small_cate_name _ & "</a></td></tr>" cnt = cnt + 1 End If Next ws ' 終了 Print #fp, "</tbody></table>" Close #fp End Sub ' ---------- テスト実行に関する関数 ---------- ' ブラウザを起動してこのブックの全テストを実行 Sub execTestsBrowser() browser_command = getBrowserCommand ' 開きたいURL selen_test = getSeleniumURL _ & "core/TestRunner.html?test=../tests/" _ & getSystemName _ & "/" _ & getLargeCateName _ & "Test.html" _ ' 起動 Shell "cmd.exe /c start " _ & browser_command _ & " """ _ & selen_test _ & """" End Sub
補足
追記:
速報:グーグルが新言語「Noop」を公開。JavaVMで動作
http://www.publickey1.jp/blog/09/noop...
- 名言:「ドキュメントが実行可能ならば、そのドキュメントが古くなることはない」
関連する記事:
ドキュメント作成を楽にするための,Excel VBA 頻出8パターン
http://language-and-engineering.hatenablog.jp/entry/20090401/p1
Selenium 中級者になろう (変数+XPath+JavaScriptを,テストケース中で利用する方法)
http://language-and-engineering.hatenablog.jp/entry/20090818/p1
IE AutoTester で,UIの回帰テストを完全自動化
http://language-and-engineering.hatenablog.jp/entry/20090922/p1