GUIアプリの自動テストが可能なライブラリ「Friendly」についてまとめてみた

# 概要
GUIの試験を自動化できるFriendlyというライブラリについて、使用する際に知っておくと役に立ちそうな知識やサイト、そのほか簡単なサンプルコードを自身の勉強も兼ねてまとめてみました。

:::note
- 個人的にわかる範囲で記載しています。
- 自分はWPFアプリケーションの自動テストで使用していましたので、それをベースに記載しています。
:::

# 目次
- [Friendlyとは](#friendlyとは)
- [導入方法](#導入方法)
- [Friendlyの設計方針](#friendlyの設計方針)
- [テスト対象の画面](#テスト対象の画面)
- [サンプルコード](#サンプルコード)
  - [Sampleプロジェクト](#sampleプロジェクト)
  - [Driverプロジェクト](#driverプロジェクト)
  - [Scenarioプロジェクト](#scenarioプロジェクト) 
- [知識面で役に立つサイト](#知識面で役に立つサイト)
  - [ささいなことですが](#ささいなことですが)
  - [かずきのBlog](#かずきのblog) 
- [Test Assistant Pro](#test-assistant-pro)

# Friendlyとは
[株式会社Codeer](https://www.codeer.co.jp/)様が作成、公開している、GUIのアプリを自動で操作できるライブラリです。GUIテストを自動化したいときなどに便利です。
GUIを自動操作するアプリケーションをFriendlyを使用して作成し、そのアプリから対象のアプリを操作するイメージです。

# 導入方法
Visual Studioのnugetで「Codeer.Friendly」と検索すると関連するdllが表示されるので、そこからインストールできます。
以下辺りを導入すると良さそうです。
- Codeer.Friendly
※Windowsアプリを操作するときに必ず使うライブラリ
- Codeer.Friendly.Windows
※Windowsアプリを操作するときに必ず使うライブラリ
- Codeer.Friendly.Grasp
※このライブラリについては開発者のブログの[こちら](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/04/133345)を参照
- Codeer.Friendly.KeyMouse
※このライブラリについては開発者のブログの[こちら](https://ishikawa-tatsuya.hatenablog.com/entry/2017/11/29/070239)を参照
- Codeer.Friendly.NativeStandardControls
このライブラリについては開発者のブログの[こちら](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/08/000942)を参照

自分はWPFアプリの自動テストを行いたかったので、上記に加えて
- RM.Friendly.WPFStandardControls

というライブラリも導入しました。

# Friendlyの設計方針
上記にも記載しましたが、自動テストはGUIを自動操作するアプリケーションを別に作成し、そのアプリから対象のアプリを自動操作する形になります。そのGUIを自動操作するアプリの設計方針は、Driver/Scenarioパターンという考え方で実装する方針が基本になります。Driver/Scenarioパターンについては以下に解説があります。
- [Driver/Scenarioパターン](https://github.com/Codeer-Software/Friendly/blob/master/TestAutomationDesign.jp.md)

# サンプルコード
めちゃくちゃ簡単ですが、サンプルを作成してみました。

## テスト対象の画面
以下の画面をテスト対象とします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/3c49a4d0-5266-a0d2-7b0b-758cc983a122.png)

一番上のサンプルボタンを押すと、ボタンすぐ下のテキストボックスに文字が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/e6ac1649-31c5-e9f7-6023-34268e1f4336.png)

サンプルトグルボタンを選択状態にすると、ボタンすぐ下のテキストボックスに文字が表示されます。
未選択状態にすると、テキストボックスの文字が消えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/9edc4cff-62f4-257a-a803-c98c6024ac18.png)

コンボボックスは下記のように選択肢が格納されています。
※一番上の選択肢は空文字にしています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/a6a948f8-9289-91b7-def2-b641eb6be4be.png)

## コード
以下のように3つのプロジェクトを作成してテストをしてみました。
Sampleプロジェクトはテスト対象のプロジェクト、DriverとScenarioプロジェクトはテスト用のプロジェクトです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3533049/1c172541-f7bf-908f-c512-e32348543240.png)

### Sampleプロジェクト
テスト対象のプロジェクトです。ビルドしてできたexeファイルを実行すると[テスト対象の画面](#テスト対象の画面)が起動します。
WPFで記載しています。

#### View

```xml:SampleView.xaml
<Window x:Class="Sample.SampleView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:vm="clr-namespace:Sample"
        mc:Ignorable="d"
        Height="240" Width="200">
    <Window.DataContext>
        <vm:SampleViewModel />
    </Window.DataContext>
    <StackPanel>
        <Button Margin="10"
                Content="サンプルボタン"
                Width="150"
                Command="{Binding SampleButtonCommand}"/>
        <TextBox Margin="10"
                 Text="{Binding SampleButtonText}"
                 Width="150"/>
        <ToggleButton x:Name="SampleToggle"
                      Margin="10"
                      Content="サンプルトグルボタン"
                      Width="150"
                      Command="{Binding SampleToggleButtonCommand}"
                      IsChecked="{Binding IsCheckedToggleButton}"/>
        <TextBox Margin="10"
                 Text="{Binding SampleToggleButtonText}"
                 Width="150"/>
        <ComboBox Width="150">
            <ComboBoxItem Content=""/>
            <ComboBoxItem Content="選択肢1"/>
            <ComboBoxItem Content="選択肢2"/>
            <ComboBoxItem Content="選択肢3"/>
            <ComboBoxItem Content="選択肢4"/>
        </ComboBox>
    </StackPanel>
</Window>
```

```C#:SampleView.xaml.cs
using System.Windows;

namespace Sample
{
    public partial class SampleView : Window
    {
        public SampleView()
        {
            InitializeComponent();
        }
    }
}
```

#### ViewModel

```C#
using Prism.Commands;
using Prism.Mvvm;
using System.Windows.Input;

namespace Sample
{
    public class SampleViewModel : BindableBase
    {
        /// <summary>
        /// サンプルボタン用のテキストボックスのテキスト
        /// </summary>
        public string SampleButtonText { get => _sampleButtonText; set => SetProperty(ref _sampleButtonText, value); }
        private string _sampleButtonText;

        /// <summary>
        /// サンプルボタン押下時のコマンド
        /// </summary>
        public ICommand SampleButtonCommand => _sampleButtonCommand ?? (_sampleButtonCommand = new DelegateCommand(() =>
        {
            SampleButtonText = "ボタンが押されました";
        }));
        private ICommand _sampleButtonCommand;

        /// <summary>
        /// サンプルトグルボタン用のテキストボックスのテキスト
        /// </summary>
        public string SampleToggleButtonText { get => _sampleToggleButtonText; set => SetProperty(ref _sampleToggleButtonText, value); }
        private string _sampleToggleButtonText;

        /// <summary>
        /// サンプルトグルボタン押下時のコマンド
        /// </summary>
        public ICommand SampleToggleButtonCommand => _sampleToggleButtonCommand ?? (_sampleToggleButtonCommand = new DelegateCommand(() =>
        {
            SampleToggleButtonText = IsCheckedToggleButton ? "トグルボタンが押されました" : string.Empty;
        }));
        private ICommand _sampleToggleButtonCommand;

        /// <summary>
        /// サンプルトグルボタンの選択状態
        /// </summary>
        public bool IsCheckedToggleButton { get; set; }
    }
}
```

### Driverプロジェクト
`SampleView`にアタッチして操作するためのクラスです。
Driverは後述する[Test Assistant Pro](#test-assistant-pro)を使用すると作成しやすさが格段にアップします。
`SampleView.xaml`や`SampleViewModel.cs`に定義していたバインド対象のプロパティ名やコマンド名を使用して、画面にあるそれぞれのコントロールを辿っています。
`SampleToggleButton`は`SampleView.xaml`に`x:Name`で名前が定義されているので、`Core.Dynamic().SampleToggle;`みたいな形で名前で辿れています。

```C#:SampleViewDriver.cs
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Codeer.TestAssistant.GeneratorToolKit;
using RM.Friendly.WPFStandardControls;

namespace Driver
{
    [WindowDriver(TypeFullName = "Sample.SampleView")]
    public class SampleViewDriver
    {
        public WindowControl Core { get; }

        /// <summary>
        /// サンプルボタン
        /// </summary>
        public WPFButtonBase SampleButton => Core.LogicalTree().ByBinding("SampleButtonCommand").FirstOrDefault()?.Dynamic();
        
        /// <summary>
        /// サンプルボタン用のテキストボックス
        /// </summary>
        public WPFTextBox SampleText => Core.LogicalTree().ByBinding("SampleButtonText").FirstOrDefault()?.Dynamic();
        
        /// <summary>
        /// サンプルトグルボタン
        /// </summary>
        public WPFToggleButton SampleToggleButton => Core.Dynamic().SampleToggle;
        
        /// <summary>
        /// サンプルトグルボタン用のテキストボックス
        /// </summary>
        public WPFTextBox SampleToggleButtonText => Core.LogicalTree().ByBinding("SampleToggleButtonText").FirstOrDefault()?.Dynamic();
        
        /// <summary>
        /// コンボボックス
        /// </summary>
        public WPFComboBox ComboBox => Core.LogicalTree().ByType("System.Windows.Controls.ComboBox").FirstOrDefault()?.Dynamic();

        public SampleViewDriver(WindowControl core)
        {
            Core = core;
        }

        public SampleViewDriver(AppVar core)
        {
            Core = new WindowControl(core);
        }
    }

    public static class SampleViewDriverExtensions
    {
        [WindowDriverIdentify(TypeFullName = "Sample.SampleView")]
        public static SampleViewDriver AttachSampleView(this WindowsAppFriend app)
            => app.WaitForIdentifyFromTypeFullName("Sample.SampleView").Dynamic();
    }
}
```
コントロールの拡張メソッド用のクラスです。
今回はコンボボックスの表示値を取得するための拡張メソッドのみを定義しています。

```C#:ControlsExtensions.cs
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;

namespace Driver
{
    public static class ControlsExtensions
    {
        public static string GetText(this WPFComboBox comboBox) => comboBox.Dynamic().Text;
    }
}
```

### Scenarioプロジェクト
実際にテストシナリオを記載するプロジェクトです。
今回はテストエクスプローラからテストを実行したいので最初からあるMSTestを使用していますが、他にもNunit等、Visual Studioで使用できるテストフレームワークなら好きなものを使用できると思います。
また、トグルボタンを`Click()`するために`Codeer.Friendly.KeyMouse`を使用しています。

```C#:Scenario.cs
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.KeyMouse;
using Driver;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Diagnostics;

namespace SampleTest
{
    [TestClass]
    public class Scenario
    {
        [TestMethod]
        public void TestMethod1()
        {
            // 操作対象プロセスの起動
            var process = Process.Start("Sample.exeまでのパス");

            // WindowsAppFriendでラップ
            var app = new WindowsAppFriend(process);

            // サンプル画面にアタッチ
            var driver = app.AttachSampleView();

            // 以下、操作

            // サンプルボタン用のテキストの値が空か確認
            string expected = driver.SampleText.Text;
            Assert.AreEqual(expected, string.Empty);

            // サンプルボタンを押下
            driver.SampleButton.EmulateClick();

            // サンプルボタン用のテキストに「ボタンが押されました」と表示されているか確認
            expected = driver.SampleText.Text;
            Assert.AreEqual(expected, "ボタンが押されました");

            // サンプルボタン用のテキストの値が空か確認
            expected = driver.SampleToggleButtonText.Text;
            Assert.AreEqual(expected, string.Empty);

            // サンプルボタン用のテキストに直接入力する
            driver.SampleText.EmulateChangeText("直接入力しました");

            // サンプルボタン用のテキストの値が変わっているか確認
            expected = driver.SampleText.Text;
            Assert.AreEqual(expected, "直接入力しました");

            // サンプルトグルボタンを押下 選択状態にする

            // EmulateCheckだと、トグルボタンが押下状態になるが、
            // 紐づいたViewModelのCommandは走らなかった
            //driver.SampleToggleButton.EmulateCheck(true);

            // NugetからCodeer.Friendly.KeyMouseをインストールするとトグルボタンもクリック可能
            // Click()だとViewModelのCommandも走った
            driver.SampleToggleButton.Click();

            // サンプルトグルボタン用のテキストに「トグルボタンが押されました」と表示されているか確認
            expected = driver.SampleToggleButtonText.Text;
            Assert.AreEqual(expected, "トグルボタンが押されました");

            // サンプルトグルボタンを押下 未選択状態にする
            driver.SampleToggleButton.Click();

            // サンプルトグルボタン用のテキストの値が空か確認
            expected = driver.SampleToggleButtonText.Text;
            Assert.AreEqual(expected, string.Empty);

            // コンボボックスのテキストの値が空か確認
            expected = driver.ComboBox.GetText();
            Assert.AreEqual(expected, string.Empty);

            // コンボボックスの値を変更
            driver.ComboBox.EmulateChangeSelectedIndex(2);

            // コンボボックスの選択肢が「選択肢2」となっているか確認
            expected = driver.ComboBox.GetText();
            Assert.AreEqual(expected, "選択肢2");

            // 後処理
            // これをしないとテスト終了後もアプリが起動状態のままになってしまう
            app.Dispose();
            process.Kill();
        }
    }
}
```

基本的には上記のように、GUI自動操作アプリの処理で`Process`クラスの`Start()`メソッドを使用して操作対象のアプリを起動し、そのprocessを`Friendly`の`WindowsAppFriend`クラスを使用してラップしてから、自動操作を行っていく形になるのかなと思います。

:::note
テストを実行した際、同じテストケースでも`対象アプリケーションが通信不能な状態になったか、 シリアライズ不可能な型のデータを転送しようとした可能性があります。`というエラーが出て時々失敗となったり、その中でも再度実行すると時々成功したりする場合がありました。
その場合は、Visual Studioのテストエクスプローラからデバッグで実行すると、必ず成功するようになりました。
私の場合は`Codeer.Friendly.Windows.KeyMouse.dll`の`SendKeys()`を使用したり、`RM.Friendly.WPFStandardControls.dll`の`EmulateChangeText()`を使用した直後にエラーが発生する時がありました。
PCのスペックがよいとこの現象は発生せず、PCのスペックがあまりよくない場合だと発生することがあったため、スペックも関係していたかもしれません。
もしかしたら現象が発生した低スペックPCでも現象が発生しないよう根本的に解決する方法があったのかもしれませんが、一旦参考まで。
:::

# 知識面で役に立つサイト

### ささいなことですが
Friendlyの開発者の日記です。

https://ishikawa-tatsuya.hatenablog.com/

下記に、覚えたてのころに私がよく確認していたページをまとめてみました。

### Friendly ハンズオン
- [Frienldy いまさら概要と学習計画](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/19/000216)
- [Friendlyハンズオン 1.用意](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/08/005850)
- [Friendlyハンズオン 2.アタッチ](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/09/071702)
- [Friendlyハンズオン 3.操作対象プロセスに内部APIを実行させる](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/10/000326)
- [Friendlyハンズオン 4.操作対象プロセスに内部APIを実行させる - 続き](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/11/001046)
- [Friendlyハンズオン 5.DLLインジェクション](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/12/004205)
- [Friendly ハンズオン6 GUI操作・・・あれ？](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/17/000527)
- [Friendly ハンズオン7 WinForms用上位ライブラリ](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/29/162557)
- [Friendly ハンズオン8 Friendly.Windows.Grasp - .Netアプリ操作でよく使う機能 -](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/05/230614)
- [Friendlyハンズオン9 Win32用上位ライブラリ](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/08/000942)
- [Friendly ハンズオン 10 .Netアプリで出てくるネイティブウィンドウ対応](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/09/081848)
- [Friendly ハンズオン 11 WPF用上位ライブラリ](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/10/114835)
- [Friendly ハンズオン 12 TestAssistant(キャプチャリプレイもできますw)](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/11/123709)
- [Friendly ハンズオン 13 アプリケーションドライバ -その1-](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/17/193925)
- [Friendly ハンズオン 13 アプリケーションドライバ -その2-](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/22/194554)
- [Friendly ハンズオン 13 アプリケーションドライバ -その3-](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/28/191958)
- [Friendly ハンズオン 13 アプリケーションドライバ -その4-](https://ishikawa-tatsuya.hatenablog.com/entry/2015/02/04/075335)
- [Friendly ハンズオン 13 アプリケーションドライバ -その5-](https://ishikawa-tatsuya.hatenablog.com/entry/2015/02/07/012701)
- [Friendly ハンズオン 14 -準備- テストシナリオ共通部分抽出](https://ishikawa-tatsuya.hatenablog.com/entry/2015/04/26/102356)
- [Friendly ハンズオン 14-2 テストシナリオ実装](https://ishikawa-tatsuya.hatenablog.com/entry/2015/04/26/171032)

### 内部的な知識
- [Friendlyの処理実行～プロセス、スレッド、同期、非同期～　その1](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/04/015358)
- [Friendlyの処理実行～プロセス、スレッド、同期、非同期～　その2](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/05/002429)
- [Friendly なんで別プロセス操作できるの？](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/06/000720)
- [Friendly 操作対象プロセス内のインスタンス](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/07/004041)
- [世界が驚いた！Friendlyマジック - 種明かし編](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/25/233836)

### 予備知識
- [Friendlyを作った理由　その1](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/01/011121)
- [Friendlyを作った理由　その2](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/02/012908)
- [Friendly GUI操作用上位ライブラリの必要性と思想](https://ishikawa-tatsuya.hatenablog.com/entry/2014/12/18/000601)
- [Friendly、ラムダ送れるってよ](https://ishikawa-tatsuya.hatenablog.com/entry/2015/09/03/123346)
- [Windowsアプリテスト自動化でのキーエミュレートはありなのか？](https://ishikawa-tatsuya.hatenablog.com/entry/2017/03/10/065545)
- [Friendlyでx64のプロセスからx86のプロセスを操作する](https://ishikawa-tatsuya.hatenablog.com/entry/2019/07/27/123301)
- [FriendlyでWPFアプリをテストするときのコツ](https://ishikawa-tatsuya.hatenablog.com/entry/2020/05/17/142034)

### Friendlyのdllについて
- [Friendly.Windows.Graspとは](https://ishikawa-tatsuya.hatenablog.com/entry/2015/01/04/133345)
- [Friendly.Windows.KeyMouseを公開しました](https://ishikawa-tatsuya.hatenablog.com/entry/2017/11/29/070239)

### かずきのBlog
様々なことが記載されていますが、その中でFriendlyのことも記載されています。

https://blog.okazuki.jp/archive/category/Friendly

# Test Assistant Pro

## Test Assistant Proとは

Friendlyを使用した自動テストの作成の、作業効率をぐんと上げてくれるツールです。
[こちら](https://www.codeer.co.jp/TestAssistantPro)がCodeer様のTest Assistant Proのページです。
また、Test Assistant Proは有料になります。
下記にインストール方法を記載していますが、まずはライセンス契約が必要です。

### インストール方法
1. Visual Studioの拡張機能から「Test Assistant Pro」と選択してダウンロードできます。ダウンロードした後はVisual Studioを一度閉じてください。
1. VisiualStudioを閉じるとインストールが始まり、modifyかcancelか選択を聞かれるのでmodifyを選択します。

### ライセンスのアクティブ方法
[インストール方法](#インストール方法)の続きです。
1. Visual Studioが起動したら上部のメニューバーから 「ツール」 → 「TestAssistantPro」 → 「Licence Information」 を選択します。
1. 表示されたダイアログで「Licence Key」と「Developer Name」を入力後、「Activate」を選択します。
※「Developer Name」はライセンスサーバーで識別するために付けるもののため、任意の値で大丈夫です。　
1. 成功すると、「Licence activation scuceeded」というダイアログが表示されます。

:::note
もし「The Maximum number of licensed users has been reached.」というメッセージが表示された場合は、ライセンスサーバーからアカウントを削除してもう一度試すと成功すると思います。
:::

### 使用方法
個人的には画面のDriverを作成する際によく使用していました。
Driverの作成の他にも、画面の操作を元にシナリオのコードを自動で生成してくれる機能等、色々な機能があります。

下記にマニュアルがあります。
- [Test Assistant Pro マニュアル](https://github.com/Codeer-Software/TestAssistantPro.Manual)

具体的な使用方法については下記にまとめられています。
- [WinForms](https://github.com/Codeer-Software/TestAssistantPro.Manual/blob/master/WinForms/README.md)
- [WPF](https://github.com/Codeer-Software/TestAssistantPro.Manual/blob/master/WPF/README.md)
- [Web](https://github.com/Codeer-Software/TestAssistantPro.Manual/blob/master/Web/README.md)

以下もご参考に。
- [Test Assistant Pro リリース！](https://ishikawa-tatsuya.hatenablog.com/entry/2019/02/19/123109)

:::note
WPFの自動テストを行っていた私の経験からだと、Test Assistant Proの操作方法については
- 「AnalzeWindow」の表示方法
- Driverの作成方法
- Scenario作成の補助機能である「Capture」機能の使い方

辺りを覚えておけばまずは大丈夫かと思いました。
:::

# 終わりに
別プロセスから別プロセスを操作するというFrienldyの仕組みが面白いと思っています。
簡単な自動テストなら少し慣れれば記載できると思いますが、スピード感をもってがっつりプロジェクトに自動テストを組み込むなら、Test Assistant Proがあった方がかなり効率があがると思いました。