Codeer.FriendlyでWindowsFormsアプリケーションのテストを自動化する

# 概要

[Codeer.Friendly](https://github.com/Codeer-Software/Friendly)というライブラリを使ってWindows用アプリケーションのテストを自動化したので、メモ。

# 動機

WindowsFormsで作ったアプリケーションのリグレッションテストをしたい
→ 頻繁にバージョンアップするので手動テストはダルい
→ テストコード書いて自動テストしたい
→ でもレガシーコードなのでUIに実装がたっぷり入っててテストがし辛い
→ そこでこのCodeer.Friendly！

# サンプルコード

今回解説するサンプルコードは以下に配置しました。
https://github.com/nendo-code/FriendlyTestSample

# テスト対象

単純なアプリケーションです。
■UI
<img width="247" alt="テスト対象.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211743/d96cd6dd-775c-5dd1-3894-152a8a62aabd.png">


■状態遷移
![ステートマシン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211743/ce980825-4631-d458-8b74-d5e3363514aa.png)

ソースコードは以下です(本筋にはあまり関係ありません。)
https://github.com/nendo-code/FriendlyTestSample/blob/master/TestTarget/TestTarget.cs

# テスト実施

さて、早速UIを参照したりボタンを押したりしてテストをしたいわけですが、
各コントロールやメソッドはprivateなのでテストコードからはアクセスできません。

それを回避するために今回は[Codeer.Friendly](https://github.com/Codeer-Software/Friendly)を使います。
FriendlyはNuGetからインストールできます。

## テストヘルパー
まず下記のようにテストヘルパークラスを用意します。
直接テスト仕様に関係ない、テスト対象へのアタッチや、コントロールの操作のラップ等はテストヘルパーを経由してテストすることにします。

```c#:TestHelper.cs
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using System.Diagnostics;
using System.Windows.Forms;

namespace TestProject
{
    class TestHelper
    {
        // Formへの参照。
        // このクラス内でのFormへはdynamic型経由でアクセスしているため、
        // コントロール名やメソッド名が変わったらここでアクセスする名前も変更しなければいけないことに注意
        private dynamic targetForm;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public TestHelper()
        {
            // テスト対象のパスを取得
            var path = System.IO.Path.GetFullPath("TestTarget.exe");
            // テスト対象のプロセスを開始し、WindowsAppFriendでプロセスへアタッチ
            var app = new WindowsAppFriend(Process.Start(path));
            // メインフォームへの参照を取得
            targetForm = app.Type(typeof(Application)).OpenForms[0];
        }

        // Executeボタンの有効/無効状態の取得するヘルパー
        public bool IsExecute_button_Enable()
        {
            return (bool)targetForm.execute_button.Enabled; 
        }

        // Statusテキストボックスのテキストの取得するヘルパー
        public string StatusText()
        {
            return (string)targetForm.result_textBox.Text;
        }

        // Executeボタンの押下を実行するヘルパー
        public void PushExecute()
        {
            targetForm.Execute_button_Click(null, null);
        }

        // フォームを閉じるヘルパー
        public void CloseForm()
        {
            targetForm.Close();
        }
    }
}
```

## テストコード
テストヘルパーを使う形で、テストコードを書きます。
テスト対象の状態遷移が狙い通り行われていることを確認します。

■テスト対象の状態遷移(再掲)

![ステートマシン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211743/ce980825-4631-d458-8b74-d5e3363514aa.png)

■コード

```c#:Test.cs
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Threading;

namespace TestProject
{
    [TestClass]
    public class Test
    {
        private TestHelper helper;
        [TestInitialize]
        public void TestInitialize()
        {
            helper = new TestHelper();
        }
        [TestCleanup]
        public void TestCleanup()
        {
            helper.CloseForm();
        }

        [TestMethod]
        public void Test1()
        {
            // 起動直後は起動中状態であることを確認
            Assert.AreEqual(false, helper.IsExecute_button_Enable());
            Assert.AreEqual(true, helper.StatusText().Contains("起動中"));

            // 起動完了待ち
            Thread.Sleep(1500);

            // 起動完了後のステータス確認
            Assert.AreEqual(true, helper.IsExecute_button_Enable());
            Assert.AreEqual(true, helper.StatusText().Contains("実行可能"));

            // 実行ボタン押下
            helper.PushExecute();

            // 実行直後のステータス確認
            Assert.AreEqual(false, helper.IsExecute_button_Enable());
            Assert.AreEqual(true, helper.StatusText().Contains("実行中"));

            // 実行完了待ち
            Thread.Sleep(1500);

            // 実行完了後のステータス確認
            Assert.AreEqual(true, helper.IsExecute_button_Enable());
            Assert.AreEqual(true, helper.StatusText().Contains("実行可能"));
        }
    }
}
```

これでUIを操作/参照するテストコードが書けます。

## その他

- テストプロジェクトとテスト対象のフレームワークバージョンは合わせた方がよい（指定可能なようだが面倒）
- テストの実行環境とテスト対象のアーキテクチャ(x86/x64)が合っていないとプロセスのアタッチ時に例外が出るので注意。VisualStudioの[テスト - テスト設定 - AnyCPUプロジェクトのプロセッサアーキテクチャ]でテスト実行環境のアーキテクチャが選択できる。
- テストコードとテスト対象の変数/メソッドの紐づけはdynamic変数経由で行うので、変数名/メソッド名の変更時にIDEの支援が受けられない。テストヘルパーを用意するなどして、なるべく名前を使う箇所を減らし、影響範囲を減らした方が良さそう。