ささいなことですが。

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

Friendly ハンズオン 14 -準備- テストシナリオ共通部分抽出

お久しぶりです・・・
だいぶ空いてしまいました。
ブログって一回書かなくなるとダメですね。
気を取り直してテストシナリオの共通部分の抽出行きましょう。

あれ?シナリオ書くって言ってなかった?

はい。
Friendly ハンズオン 13 アプリケーションドライバ -その5- - ささいなことですが。に最後に書いた時はそのつもりだったんですけど、久しぶりに見るとやっぱり共通化しといた方がいいかなーと。あと、見直すとサンプルコードにバグがあったんで修正しときました。(すみません。不具合は見つかったら直しますので、見るたびちょっと変わってるかもです・・・)
Ishikawa-Tatsuya/HandsOn14 · GitHubからダウンロードしてください。このコードを変更していきます。

アプリのライフサイクル管理コード

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

そもそも何をやっているか?

ポイントは_app.Release(bool isContinue)呼び出しの引数です。テストが終了したときに対象アプリを生き残すか否かです。以下の条件で終了させます。

  1. テスト失敗
  2. クラスに定義したテストメソッドを全て実行した

2.の方を実現するためにClassInitializeに面倒なコード入れてます。(実はこれでも完全ではなく、違うクラスの複数メソッドを選択されて実行された場合は、アプリ終了のタイミングが全てのテストが終わった後になってしまします。でも、今回はそのケースはよしとします。)

TestBase作成

共通部分は基本クラスに押し込むことにします。TestBaseクラスを作成します。テストごとにstaticなインスタンスを持ちたいので、ジェネリックパラメータでテストクラスを指定させるようにします。(タイプが異なるとstaticインスタンスは別に確保される)
AppDriverは継承先でも使うのでprotectedなプロパティーにしておきます。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using EmployeeManagementDriver;
using System.Collections.Generic;
using EmployeeManagement;
using System.Linq;

namespace TestScenario
{
    public class TestBase<T>
    {
        static protected AppDriver App { get; set; }
        static Dictionary<string, bool> _tests;
        public TestContext TestContext { get; set; }

        public static void NotifyClassInitialize()
        {
            App = new AppDriver();
            _tests = typeof(T).GetMethods().Where(e => 0 < e.GetCustomAttributes(typeof(TestMethodAttribute), true).Length).ToDictionary(e => e.Name, e => true);
        }

        public static void NotifyClassCleanup()
        {
            App.EndProcess();
        }

        public void NotifyTestInitialize()
        {
            App.Attach();
        }

        public void NotifyTestCleanup()
        {
            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);
        }
    }
}

これをAdjustDriverに継承させます。ライフ管理の部分は随分スッキリしました。

[TestClass]
public class AdjustDriver : TestBase<AdjustDriver>
{
    [ClassInitialize]
    public static void ClassInitialize(TestContext c)
    {
        NotifyClassInitialize();
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        NotifyClassCleanup();
    }

    [TestInitialize]
    public void TestInitialize()
    {
        NotifyTestInitialize();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        NotifyTestCleanup();
    }

パラメタライズドテスト考慮

そして、もう一つこのTestBaseに機能を入れます。それはパラメタライズドテスト用です。VSTestのパラメタライズは癖があって、DBかExcelからパラメータを読み込ませることになっています。TestContext.DataRowに現在の行が入ります。このままだと、使いづらいので便利機能を付けておきます。指定の型に変換するメソッドです。

public Data GetParam<Data>() where Data : new()
{
    Data data = new Data();
    foreach(var e in typeof(Data).GetProperties())
    {
        e.GetSetMethod().Invoke(data, new object[] { Convert(e.PropertyType, TestContext.DataRow[e.Name]) });
    }
    return data;
}

static object Convert(Type type, object obj)
{
    //一旦int,bool,stringで
    //必要に応じて変換方法を追加
    string value = obj == null ? string.Empty : obj.ToString();
    if (type == typeof(int))
    {
        return int.Parse(value);
    }
    else if (type == typeof(bool))
    {
        return string.Compare(value, true.ToString(), true) == 0;
    }
    else if (type == typeof(string))
    {
        return value;
    }
    throw new NotSupportedException();
}

使う側ではこんな感じで使うことを想定しています。
こんなExcelがあって、(セルは全て文字列型にしています。)
f:id:ishikawa-tatsuya:20150426100803p:plain

読み込みコードです。

//Excelのカラム名称と合わせる
class Data
{
    public string Input { get; set; }
    public int Expectation { get; set; }
}
//ファイル名とシート名を合わせる
[DataSource("System.Data.OleDB",
    @"Provider=Microsoft.ACE.OLEDB.12.0; Data Source=TestParams.xlsx; Extended Properties='Excel 12.0;HDR=yes';",
    "DataSheet$",
    DataAccessMethod.Sequential
)]     
[TestMethod]
public void ParameterizedTest()
{
    var data = GetParam<Data>();
}

システムテストだと多くの人と内容を共有する必要もあって、Excelでパラメータ管理する方が受けが良いですね。ソースにパラメータ埋め込んでいいとかだと、@neueccさんのChainingAssertionを使えばVSTestでも気軽にパラメタライズできます。好みに合わせて使ってみてください。
Chaining Assertion - Home

共通化まで終えたコードはこちらに置きました。次回はこれを使ってシナリオを実装します。
HandsOn14-2/Project/TestScenario at master · Ishikawa-Tatsuya/HandsOn14-2 · GitHub

サンプルコード修正

4/29 TestCleanup、NotifyTestCleanupのコードを修正しました。