ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Friendly ハンズオン 13 アプリケーションドライバ -その1-

今回はアプリケーションドライバの話です。
Frienldyは内部APIを使って効率的に自動化を実現するためのライブラリです。でも、内部APIは文字通り内部仕様にあたるので、テストシナリオにそのまま出てきては、テストのメンテ、作成効率が落ちます。外部仕様が変わらなくても内部仕様が変わっただけでテストが壊れてしまうのです。

また、実際これをテストチームで運用するのは難しいと感じた人もいると思います。それはその通りで、内部APIや内部仕様を有効利用する以上は開発チームのサポートが必須となります。でも、テストシナリオはテストチームで書けた方が効率いいですよね。
アプリケーションドライバは、そのような問題を解決する設計パターンなのです。

アプリケーションドライバとは

対象アプリを操作するためのレイヤです。
これ自体はテストを記述するものではなく、テストをする前段階の「操作」というところに特化した実装を記述するものです。
f:id:ishikawa-tatsuya:20150117171026p:plain

ハンズオン

こちらからダウンロードしてビルドしてください。
Ishikawa-Tatsuya/HandsOn13 · GitHub

対象アプリ

今回の対象は、社員を登録して、住所検索するというアプリです。
EmployeeManagementがそれに当たります。

起動画面です。
f:id:ishikawa-tatsuya:20150117171909p:plain

追加画面です。
f:id:ishikawa-tatsuya:20150117171942p:plain

入力に不備があるとエラーメッセージが表示されます。
f:id:ishikawa-tatsuya:20150117172100p:plain

登録が成功すると、追加ダイアログが消えてメインの画面に戻ります。
メインの画面では、「名前(性別) 住所」でリストに表示されます。
f:id:ishikawa-tatsuya:20150117173604p:plain

検索画面です。
テキストボックスに文字列を入力して実行を押すと、部分一致で検索にヒットしたものがリスト表示されます。
f:id:ishikawa-tatsuya:20150117173906p:plain

検索ヒット
f:id:ishikawa-tatsuya:20150117174120p:plain

該当なし
f:id:ishikawa-tatsuya:20150117174136p:plain

アプリケーションドライバとテストシナリオ

これも、プロジェクトの追加と参照設定までやっています。
EmployeeManagementDriverがアプリケーションドライバで、TestScenarioがテストシナリオです。
では、やってみましょう。

ハンズオン開始

まず、アプリケーションドライバを実装します。
このレイヤの目的はテストシナリオが優雅に記述できるようにするということですね。
内部仕様とか技術的に高度な部分はここで隠蔽します。書き方は、ケースによって異なります。
まずは、GUIマップから隠蔽してみましょう。
このアプリは3つ画面があるので3つクラスを実装します。
EmployeeManagementDriverプロジェクトにそれぞれ追加してください。

メイン画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    public class MainFormDriver
    {
        public WindowControl Window { get; private set; }
        public FormsListBox ListBoxEmployee { get; private set; }
        public FormsButton ButtonAdd { get; private set; }
        public FormsButton ButtonSearch { get; private set; }

        public MainFormDriver(WindowControl window)
        {
            Window = window;
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
            ButtonAdd = new FormsButton(Window.Dynamic()._buttonAdd);
            ButtonSearch = new FormsButton(Window.Dynamic()._buttonSearch);
        }
    }
}

追加画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    public class AddFormDriver
    {
        public WindowControl Window { get; set; }
        public FormsButton ButtonEntry { get; private set; }
        public FormsTextBox TextBoxName { get; private set; }
        public FormsTextBox TextBoxAddress { get; private set; }
        public FormsRadioButton RadioButtonWoman { get; private set; }
        public FormsRadioButton RadioButtonMan { get; private set; }

        public AddFormDriver(WindowControl window)
        {
            Window = window;
            ButtonEntry = new FormsButton(Window.Dynamic()._buttonEntry);
            TextBoxName = new FormsTextBox(Window.Dynamic()._textBoxName);
            TextBoxAddress = new FormsTextBox(Window.Dynamic()._textBoxAddress);
            RadioButtonWoman = new FormsRadioButton(Window.Dynamic()._radioButtonWoman);
            RadioButtonMan = new FormsRadioButton(window.Dynamic()._radioButtonMan);
        }
    }
}

検索画面

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;

namespace EmployeeManagementDriver
{
    public class SearchFormDriver
    {
        public WindowControl Window { get; set; }
        public FormsButton ButtonExecute { get; private set; }
        public FormsTextBox TextBoxSearch { get; private set; }
        public FormsListBox ListBoxEmployee { get; private set; }

        public SearchFormDriver(WindowControl window)
        {
            Window = window;
            ButtonExecute = new FormsButton(Window.Dynamic()._buttonExecute);
            TextBoxSearch = new FormsTextBox(Window.Dynamic()._textBoxSearch);
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
        }
    }
}

何も難しことはしていませんね。単に画面要素の取得をラップしているだけです。
あと一つ追加します。アプリケーション自体の操作のクラスです。AppDriverという名前にしますか。

アプリケーション自体の操作

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System.Diagnostics;

namespace EmployeeManagementDriver
{
    public class AppDriver
    {
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }

        public AppDriver()
        {
            var process = Process.Start("EmployeeManagement.exe");
            _app = new WindowsAppFriend(process);
            MainForm = new MainFormDriver(new WindowControl(_app, process.MainWindowHandle));
        }

        public void Release()
        {
            Process.GetProcessById(_app.ProcessId).CloseMainWindow();
        }
    }
}

このままで良いでしょうか?ちょっと使ってみましょう。上記の仕様の操作がテストシナリオから簡単にできればOKです。
調整用のテストコードをTestScenarioにAdjustDriverと言う名前で追加します。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.NativeStandardControls;

namespace TestScenario
{
    [TestClass]
    public class AdjustDriver
    {
        AppDriver _app;

        [TestInitialize]
        public void TestInitialize()
        {
            _app = new AppDriver();
        }

        [TestCleanup]
        public void TestCleanup()
        {
            _app.Release();
        }
    }
}

では、追加コードを検討してみましょう。
まずは、成功するケース。

[TestMethod]
public void TestAddSuccess()
{
    var async = new Async();
    _app.MainForm.ButtonAdd.EmulateClick(async);

    var addForm = new AddFormDriver(_app.MainForm.Window.WaitForNextModal());

    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    addForm.RadioButtonMan.EmulateCheck();
    addForm.ButtonEntry.EmulateClick();

    async.WaitForCompletion();

    Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee.Dynamic().Items[0].ToString());
}

で、失敗するケース。

[TestMethod]
public void TestAddError()
{
    var asyncAddForm = new Async();
    _app.MainForm.ButtonAdd.EmulateClick(asyncAddForm);

    var addForm = new AddFormDriver(_app.MainForm.Window.WaitForNextModal());

    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");

    var asyncMsg = new Async();
    addForm.ButtonEntry.EmulateClick(asyncMsg);
    var msgBox = new NativeMessageBox(addForm.Window.WaitForNextModal());

    Assert.AreEqual("性別を入力してください。", msgBox.Message);
    msgBox.EmulateButtonClick("OK");
    asyncMsg.WaitForCompletion();

    addForm.Window.Dynamic().Close();

    asyncAddForm.WaitForCompletion();
}

どうでしょう?
一見スッキリしているような気がしますが、問題があります。
①内部API(内部仕様)を使っている
②若干難易度が高い

①に関してはルール違反ですね。テストシナリオではこれは使ってはいけません。

_app.MainForm.ListBoxEmployee.Dynamic().Items[0].ToString()
addForm.Window.Dynamic().Close();

内部APIを使うこと自体はOKです。上位ライブラリでラップ提供されていないものは多く存在するので、その場合は内部APIを使えば良いのです。そしてFriendlyはそれが簡単にできます。
ただ、それはアプリケーションドライバ層でラップしてテストシナリオに提供してください。

②に関しては、微妙なとこです。テストシナリオを書く人のスキルによります。
テスト自動化は最終的にはプログラムになるので、技術的な要素は必ず入ります。でも、テストシナリオではもう少し隠れていた方がいいですよね。と言うのはできれば、テストチームの人でも書けた方が良いのです。テストチームの人はテストのスペシャリストであってプログラムは専門と言うわけではありません。(時々、プログラムスキルも高い人もいますが)その人たちの協力も得れるようにしておきたいのです。

先ほどのテストシナリオのTestAddで、難しいかなーというポイントを挙げてみます。

  • Async
  • new AddFormDriver

Asyncというかモーダルダイアログのコントロールですね。そこを実装したプログラマーからすると当たり前なのですが、ボタンを押した処理が固まっているというのは理解しづらいもののようです。(WaitForCompletionでの待ち合わせも)
後は、new AddFormDriverですね。これは何かというとAddFormDriverと言う名前を見つけるのが面倒なのです。他の処理を見てみると全てインテリセンスで解決できています。まあ、全てのnewをなくすことは実際難しいかもしれませんが、インテリセンスで解決できると難易度は格段に下がります。これはポイントですね。

今回はここまで

長くなりましたね。今回はここまでにします。
次回はこれを調整していきます。