読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

「実行可能ドキュメント」が満たすべき性質 − テスト自動化ツール「Excelenium」で使われている技術や手法

テスト excel ドキュメント Selenium 開発パラダイム 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列目の値に置換している。

「コマンド一覧」シートが,論理名と物理名をつなぐための「辞書」の役割を果たしている。



記述した操作の「日本語の説明文章」が,リアルタイムで表示される。

手段・該当箇所

テストケースのシートのI列。

効用

可読性がさらに上がる。

単語の羅列ではなく,まともな文章になるので,「テストスクリプト」ではなく「ドキュメント」であると確言できるようになる。

また,自分が打ち込んだコマンドにタイプミスがあったかどうかがすぐにわかる。

タイピングに対するフィードバックが得られる。

実現するための技法

これが実現できるのは,Seleniumのコマンド記述スタイルのすばらしさによる。

動詞,目的語1,目的語2

の3ワードで全てを済ませる。

この3要素の役割さえわかれば,文章を構築することができる。


VLOOKUPとSUBSTITUTEで,「コマンド一覧」シート内の #1 という引数を,目的語1に置換している。
#2 が目的語2になる。


これらのEXCEL関数は,簡易的な「翻訳」を行なっている。

「VLOOKUPとSUBSTITUTEを使うと翻訳ができる」というのは,ちょっとした驚きではないか。


コマンドの自動分類,「確認」項目数の可視化

手段・該当箇所

テストケースのH列。

効用

テストの作業内容は,入力操作と出力検証の2パターンの行為に分かれる。

後者が少ないと,単なるオペレーションになってしまい,テストとしては成り立たなくなってくる。

検証項目の多さを可視化すれば,検証項目数を確保するような圧力が働き,基本的にテストの質は向上する。


実現するための技法

VLOOKUP + 条件付書式。

特定の場合だけセルを強調表示するためには,Excelの条件付き書式が役立つ。

ブック情報を各シートで自動的に共有。手書き不要

手段・該当箇所

テストケースのC3, C4セル。

実現するための技法

「初期設定」シートを参照している。


細かい点だが,こういうところを手動でいちいち打ち込んでいる人は,作業の質が原始的で,状況の変化についてこれない場合が多い。


シート情報をシート内で自動的に共有。手書き不要

手段・該当箇所

テストケースのC5セル。

実現するための技法

セル中でシート名を参照:

=MID(CELL("filename",$A$1),FIND("]",CELL("filename",$A$1))+1,31)


シート上の記述に余裕を持たせる

手段・該当箇所

各行は任意でスキップ可能になっている。

また,空白行を何行続けてよいか,という点もカスタマイズ可能。

効用

記述に幅をもたせることができる。

スクリプトではなく,あくまでも書いているのはドキュメント,という位置づけ。

自動採番

手段・該当箇所

スクリプト生成時に,シート内のテストケースに対して,番号が自動的に振られる。

効用

手動で採番するという方式の場合,「それしか仕事してない」,という人もいる。

1個ずれたら全部,手動で番号を振りなおす必要がある。

そういった無駄な作業をさせない。

開発に関わる全ての成果物は,「変更に強い」ことが大事。



「生成」と「実行」を1セットで考える

手段・該当箇所

「実行」ボタンを押下した時に発生する処理。

効用

中間生成物を意識させないで済む。

最終成果物が得られれば,中間成果物はどうでもよいはず。


生成の前提条件を自動的に満たしてくれる

手段・該当箇所

スクリプトの生成の前準備として,必要なフォルダを自動的に生成する。ユーザはフォルダを意識しない。

生成に先立つものを隠ぺい。

Seleniumのフォルダを一切見なくて済むようにしてある。

1アプリに複数のテスト仕様書が機能単位で存在すると考え,
1アプリごとに1つと,その中に1機能ごとのフォルダを自動作成する。

効用

導入が容易になるし,手作業が減る。

自動生成なので,フォルダ構成も統一される。

' 全シートのテストスクリプトを生成
Sub makeAllScripts()

    ' フォルダ準備
    prepareSystemNameDir
    prepareLargeCateNameDir

    ' スクリプト作成処理

実行画面にアクセスする手間を省く

手段・該当箇所

実行のために必要なURLを意識させない。

実行に先立つものを隠ぺい。

「実行」ボタンを押せば実行画面を開いてくれる。

状況によってURLをいろいろ打ち込む必要が無い。


補足:心理的な面

さらに工夫として,これらのメリットを,実際にメリットと「感じさせる」ように意識している。

例えば

  • 全シートに,(わざと)でかでかと「この仕様書の全テストを実行」というボタンを配置してある。
    • →「文面に残るだけじゃなくて,実行もしてくれるんだ。」というメッセージが伝わる。
      • →書きたくなる。
  • 表紙にも,(わざと)「実行可能テスト仕様書」というタイトルを掲げてある。
    • →「この文書のために割く労力は,机上のものにならず,実行可能であり,現実の価値を生むのだ。」と確信させる。
      • →やる気が出る。このテスト仕様書をメンテしたくなる。
        • →テスト工程の品質が上がる。アプリ本体の品質も上がる。
  • セルのコメントに「入力不要。」というのを散りばめる。
    • →「自分が今まで手動でやってきたことは,本当はやる必要の無いことだったんだ」と開眼させる。
      • →自動化への認識が高まる。より価値の高い作業を人手で行なうようになる。

つまり,実際に楽であるだけでなく,楽であることを心理的に強調し,良い開発サイクルを生ませようとしている。

そういう意図のあるドキュメントなのである。



補足: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