読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

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

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

前回の続きです。読んでない方は、そちらを先に読んでください。
前回作ったアプリケーションドライバでは「内部仕様」と「操作技術」の隠蔽度が不十分ってことでしたね。
では改善してみます。

内部仕様の隠蔽

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

Dynamic()は隠蔽しましょう。
単にラッパー関数を作ればよいだけです。関数名は外部仕様からわかる名前が良いですね。僕はやったことありませんが、日本語の関数名でもいいかもしれません。目的は開発チーム以外でも理解できるインターフェイスを提供することなのです。
経験的にコントロールの型から始めると、使う人は探しやすいようです。(少人数からのアンケート結果です)命名規則はプロジェクトごとに最適なものにしていただければ。

[MainFormDriver.cs]

public string ListBoxEmployee_GetItemText(int index)
{
    return ListBoxEmployee.Dynamic().Items[index].ToString();
}

こちらはAddFormを閉じるということなのでプレフィックス的なのはいらないですね。

[AddFormDriver.cs]

public void Close()
{
    Window.Dynamic().Close();
}

技術的難易度の高い部分の隠蔽

テストシナリオは簡単に書きたいのです。難易度の高い文法も隠蔽しましょう。
ここでは、モーダルダイアログ対応ですね。
AddFormはモーダルダイアログとして表示されると仕様で決まっているので、ここに表示されるトリガとなった処理のAsyncを持たせます。
まあ、コードを見ると早いですね。

//関連するとこだけ、書いています。
public class AddFormDriver
{
    Async Async { get; set; }

    //...その他の定義

    public AddFormDriver(WindowControl window, Async async)
    {
        Async = async;
        //...その他の定義
   }

    //...その他の定義

    public void ButtonEntry_EmulateClickAndClose()
    {
        ButtonEntry.EmulateClick();
        Async.WaitForCompletion();
    }

    public void Close()
    {
        Window.Dynamic().Close();
        Async.WaitForCompletion();
    }
}

[MainFormDriver]

public AddFormDriver ButtonAdd_EmulateClick()
{
    var async = new Async();
    ButtonAdd.EmulateClick(async);
    return new AddFormDriver(Window.WaitForNextModal(), async);
}

これによって、追加のテストシナリオはこんな感じで書けるようになりました!

[TestMethod]
public void TestAdd()
{
    var addForm = _app.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    addForm.RadioButtonMan.EmulateCheck();
    addForm.ButtonEntry_EmulateClickAndClose();
    Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee_GetItemText(0));
}

これなら、全てインテリセンスが効くので専門職のプログラマでなくても書けるのではないでしょうか?

もう一つ、追加でエラーメッセージが表示されるパスがありますね。そちらもメッセージボックスがモーダルで出る仕様なので、隠蔽します。メッセージボックス自体を隠蔽しても良いと思います。と言うのは今回の仕様だとメッセージボックスの操作はOKボタンだけなので、シナリオで操作する必要はないのです。

[AddFormDriver.cs]

public string ButtonEntry_EmulateClickAndGetMessage()
{
    Async async = new Async();
    ButtonEntry.EmulateClick(async);
    var msgBox = new NativeMessageBox(Window.WaitForNextModal());
    var msg = msgBox.Message;
    msgBox.EmulateButtonClick("OK");
    async.WaitForCompletion();
    return msg;
}

テストシナリオはこう書けます。

[TestMethod]
public void TestError()
{
    var addForm = _app.MainForm.ButtonAdd_EmulateClick();
    addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
    addForm.TextBoxAddress.EmulateChangeText("Japan");
    Assert.AreEqual("性別を入力してください。", addForm.ButtonEntry_EmulateClickAndGetMessage());
    addForm.Close();
}

これも、随分と簡単になりましたね。

ショートカット

次は、検索のテストのためのインターフェイスも考えてみましょう。追加画面とほぼ同じやり方で作れます。
でも、検索にはセットアップとしてデータ追加が必要になってきますよね。TestAddで使ったインターフェイスを利用して追加してもいいのですが、ちょっと書き方が煩雑です。この場合、僕ならMainFormDriverに、こんなインターフェイスを提供します。

public void AddEmployeeData(params EmployeeData[] datas)
{
    ListBoxEmployee.Dynamic().Items.AddRange(datas);
}

検索のテストの場合、テストしたいのは検索画面からです。テストデータのセットアップはテスト対象外なのです。画面を操作して、100件追加するだけで200秒程度はかかると思います。1万件とかならもう非現実的な時間になります。でも、内部仕様を知っていて、データの型と格納するバッファがわかっていれば、そこにデータを詰めれば良いのです。これなら一瞬ですね。
テスト対象外の部分ならこんな手法でOKです。
ちょっと、実験してみましょう。PCスペックにもよりますが、1万件でも1秒くらいですね。

[TestMethod]
public void TestAddShortcut()
{
    List<EmployeeData> data = new List<EmployeeData>();
    for (int i = 0; i < 10000; i++)
    {
        data.Add(new EmployeeData()
            {
                Name = "Name" + i.ToString(),
                Address = "Osaka-" + i.ToString(),
                IsMan = i % 2 == 0
            });
    }
    _app.MainForm.AddEmployeeData(data.ToArray());
}

今回は書きませんが、テスト対象の場合でも、時間的な都合や設計、本当にテストしたい部分を考慮すると、GUI層のを省いてもいい時ってあります。こちらで発表しましたのでよろしければ見てください。

次回はエラー時の対応です。

ここまででアプリケーションドライバの画面への対応部分は実装できました。
これで正常系はテストできます。
でも、実は問題があるのです。実際にデグレが発生すると、予想外のところでモーダルダイアログが表示されて、テストが固まるのです。次の朝来たら、「一つ目のテストで固まってて状況がわからん。」なんてことがあります。次回はその対応をやります。

ここまでの全文を貼っておきます。

参考までに、ここまでの実装を貼っておきます。
慣れると、これくらいは簡単に書けるようになります。何か画面を実装したときには一緒にアプリケーションドライバにも対応する操作クラスを増やすようにすると良いと思います。

アプリケーション操作クラス

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();
        }
    }
}

メイン画面操作クラス

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

namespace EmployeeManagementDriver
{
    public class MainFormDriver
    {
        WindowControl Window { get; set; }
        public FormsListBox ListBoxEmployee { get; set; }
        public FormsButton ButtonAdd { get; set; }
        public FormsButton ButtonSearch { get; 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);
        }

        public AddFormDriver ButtonAdd_EmulateClick()
        {
            var async = new Async();
            ButtonAdd.EmulateClick(async);
            return new AddFormDriver(Window.WaitForNextModal(), async);
        }

        public SearchFormDriver ButtonSearch_EmulateClick()
        {
            var async = new Async();
            ButtonSearch.EmulateClick(async);
            return new SearchFormDriver(Window.WaitForNextModal(), async);
        }

        public string ListBoxEmployee_GetItemText(int index)
        {
            return ListBoxEmployee.Dynamic().Items[index].ToString();
        }

        public void AddEmployeeData(params EmployeeData[] datas)
        {
            ListBoxEmployee.Dynamic().Items.AddRange(datas);
        }
    }
}

追加画面操作クラス

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

namespace EmployeeManagementDriver
{
    public class AddFormDriver
    {
        Async Async { get; set; }
        public WindowControl Window { get; set; }
        public FormsButton ButtonEntry { get; 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, Async async)
        {
            Window = window;
            Async = async;
            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);
        }

        public string ButtonEntry_EmulateClickAndGetMessage()
        {
            Async async = new Async();
            ButtonEntry.EmulateClick(async);
            var msgBox = new NativeMessageBox(Window.WaitForNextModal());
            var msg = msgBox.Message;
            msgBox.EmulateButtonClick("OK");
            async.WaitForCompletion();
            return msg;
        }

        public void ButtonEntry_EmulateClickAndClose()
        {
            ButtonEntry.EmulateClick();
            Async.WaitForCompletion();
        }

        public void Close()
        {
            Window.Dynamic().Close();
            Async.WaitForCompletion();
        }
    }
}

検索画面操作クラス

using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Ong.Friendly.FormsStandardControls;
using System;
using System.Linq;
using System.Windows.Forms;

namespace EmployeeManagementDriver
{
    public class SearchFormDriver
    {
        Async Async { get; set; }
        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, Async async)
        {
            Async = async;
            Window = window;
            ButtonExecute = new FormsButton(Window.Dynamic()._buttonExecute);
            TextBoxSearch = new FormsTextBox(Window.Dynamic()._textBoxSearch);
            ListBoxEmployee = new FormsListBox(Window.Dynamic()._listBoxEmployee);
        }

        public void Close()
        {
            Window.Dynamic().Close();
            Async.WaitForCompletion();
        }

        public string[] ListBoxEmployee_GetSearchResult()
        {
            WindowsAppExpander.LoadAssembly(Window.App, GetType().Assembly);
            return Window.App.Type(GetType()).GetListItems(ListBoxEmployee);
        }

        static string[] GetListItems(ListBox listBox)
        {
            return listBox.Items.Cast<object>().Select(e => e.ToString()).ToArray();
        }
    }
}

アプリケーションドライバ調整用テストコード

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows.NativeStandardControls;
using System.Diagnostics;
using System.Collections.Generic;
using EmployeeManagement;

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

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

        }

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

        [TestMethod]
        public void TestAdd()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            addForm.RadioButtonMan.EmulateCheck();
            addForm.ButtonEntry.EmulateClick();
            Assert.AreEqual("ishikawa-tatsuya(男) Japan", _app.MainForm.ListBoxEmployee_GetItemText(0));
        }

        [TestMethod]
        public void TestError()
        {
            var addForm = _app.MainForm.ButtonAdd_EmulateClick();
            addForm.TextBoxName.EmulateChangeText("ishikawa-tatsuya");
            addForm.TextBoxAddress.EmulateChangeText("Japan");
            Assert.AreEqual("性別を入力してください。", addForm.ButtonEntry_EmulateClickAndGetMessage());
            addForm.Close();
        }

        [TestMethod]
        public void TestAddShortcut()
        {
            List<EmployeeData> data = new List<EmployeeData>();
            for (int i = 0; i < 10000; i++)
            {
                data.Add(new EmployeeData()
                    {
                        Name = "Name" + i.ToString(),
                        Address = "Osaka-" + i.ToString(),
                        IsMan = i % 2 == 0
                    });
            }
            _app.MainForm.AddEmployeeData(data.ToArray());
        }
    }
}

サンプルコード修正

2015/1/30
モーダルダイアログの書き方が適切でなかったので修正しました。
詳細は後日にダイアログ表示のパターン編とかやって解説します。