ささいなことですが。

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

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

前回の続きです。

今回は対象アプリのライフサイクルの管理に関してやります。

あれ?でも今までも起動と終了を管理してましたよね。
それだけではダメなんでしょうか?

もちろん、今までのでもOKです。
一つのテストごとにexeを起動しなおすと、前回のテストに影響されずにテストができます。バッチリです。
でも・・・

大きなアプリだと起動と終了は遅い

だいたい、システムテストまで自動化したいと考えるのは大きなアプリであることが多いですね。
そして、そのようなアプリは起動も終了も大抵遅いのです。
平均で5秒と考えても1000ケースやたら1時間の無駄ですね。
なんとかならないでしょうか?

アプリの終了のタイミング

基本は、一つのアプリでテストを実行させ続けるようにして、以下のタイミングのどちらかで再起動します。
①テストが失敗した場合
②クラスに定義されたテストが完了したとき

①に関しては、テストが失敗した場合は内部状態が不正になっている可能性が高いからです。前回のテストの影響をうけます。これはあまりよろしくないですね。だから一度終了させます。もちろん成功している場合だって前のテストの影響を受けていることもあります。これはトレードオフなのです。

②に関してはまさにそれで、成功しているとは言え、長時間実行させ続けると、何か変な状態になる可能性があるので、クラス単位では終了させると。
「え、それって見つけたいんじゃないの?」って思うかもしれませんね。でも、そういうので不具合を見つけたとしても原因の解明が難しいのです。で、テストが不安定になり、本来そのテストで確認したいこと以外に大量に工数がとられて費用対効果が悪くなってしまうのです。

もちろんそういうのを見つけるために組んだ自動テストなら問題ないし、そのようなランニングテストもやっておくべきですが、それを通常のテストケースで一緒にやるのは避けた方が良いでしょう。テストにはそれぞれ目的があるのです。

これは一つの案です。

必ずこうしてくださいってわけではありません。時間が許すなら、毎回再起動したほうがスッキリしますよね。でも、大量にテストを実施する場合は、こんな方法もあるんだよなーって、候補の一つとして検討してみてください。

実装方法

これを実現するために必要なことは次のものです。

  • テストの成否をコード中で知る
  • クラスの初期化と終了のタイミング

VSTestでは、こんなコードで知ることができます。
まあ、「なんでstaticやねん!」って思うかもしれませんが、こういうものなので・・・。

[ClassInitialize]
public static void ClassInitialize(TestContext c)
{
}

[ClassCleanup]
public static void ClassCleanup()
{
}

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
    {
        Process _process;
        WindowsAppFriend _app;
        public MainFormDriver MainForm { get; private set; }
        public Killer Killer;

        public void Attach()
        {
            if (_process == null)
            {
                _process = Process.Start("EmployeeManagement.exe");
            }
            _app = new WindowsAppFriend(_process);
            MainForm = new MainFormDriver(new WindowControl(_app, _process.MainWindowHandle));

            //アプリを初期状態に戻す
            InitApp();

            Killer = new Killer(1000 * 60 * 5, _process.Id);
        }

        public void Release(bool isContinue)
        {
            if (isContinue)
            {
                Killer.Finish();
                _app.Dispose();
            }
            else
            {
                EndProcess();
            }
        }

        private void InitApp()
        {
            MainForm.ListBoxEmployee.Dynamic().Items.Clear();
        }

        public void EndProcess()
        {
            try
            {
                _killer.Finish();
            }
            catch { }
            try
            {
                _process.Kill();
            }
            catch { }
            _process = null;
        }
    }
}

で、テストシナリオもこれに合わせて書き換えてみます。

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;
using System.Linq;

namespace TestScenario
{
    [TestClass]
    public class AdjustDriver
    {
        static AppDriver _app;
        static Dictionary<string, bool> _tests;
        public TestContext TestContext { get; set; }

        [ClassInitialize]
        public static void ClassInitialize(TestContext c)
        {
            _app = new AppDriver();
            _tests = typeof(AdjustDriver).GetMethods().Where(e => 0 < e.GetCustomAttributes(typeof(TestMethodAttribute), true).Length).ToDictionary(e => e.Name, e => true);
        }

        [ClassCleanup]
        public static void ClassCleanup()
        {
            _app.EndProcess();
        }

        [TestInitialize]
        public void TestInitialize()
        {
            _app.Attach();
        }

        [TestCleanup]
        public void TestCleanup()
        {
            if (TestContext.DataRow == null ||
                ReferenceEquals(TestContext.DataRow, TestContext.DataRow.Table.Rows[TestContext.DataRow.Table.Rows.Count - 1]))
            {
                _tests.Remove(TestContext.TestName);
            }
            _app.Release(TestContext.CurrentTestOutcome == UnitTestOutcome.Passed && 0 < _tests.Count);
        }

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

        [TestMethod]
        public void TestTimeup()
        {
            if (_app.IsDebug)
            {
                return;
            }

            _app.SetTimeup(1000);
            try
            {
                var addForm = _app.MainForm.ButtonAdd_EmulateClick();
                addForm.ButtonEntry_EmulateClickAndClose();
                Assert.Fail();
            }
            catch (FriendlyOperationException)
            {
                _app.EndProcess();
            }
        }
    }
}

プロセス管理に関してもう一つ工夫があります。

それは、対象プロセスのデバッグです。
でも、長くなったんで次回に続く・・・

サンプルコード修正

4/29 TestCleanupの処理を修正しました。