Help us understand the problem. What is going on with this article?

Friendlyを使ったWPFのGUIテスト

More than 3 years have passed since last update.

目的

GUIのテスト
自分がよく開発しているWPFかつLivetのMVVM状況下でのテスト

GUIテストを探していてなんかよさげだったのがこれ
他は試していないけど
Friendlyってライブラリ
Codeerって会社がフリー出しているものとのことです。

参考にしたサイト

ありがとうございました。
- http://blog.okazuki.jp/archive/category/Friendly
- http://qiita.com/advent-calendar/2014/friendly

インストール

↑のサイトとかぶっているだろうけど
Nugetでプロジェクト毎に以下のものをインストール
- Codeer.Friendly
- Codeer.Friendly.Windows
- Codeer.Friendly.Windows.Grasp
- RM.Friendly.WPFStandardControls

最後のはWPFなのでこれですがほかの環境ではWinFormやUWPでは必要なのが異なるようです。

テスト元のGUI

基本的には↑で参考にしたかずきさんのサンプルの改造。芸がなくてすいません。
Livetにしてただボタンを増やしただけです。
コマンドのバインドのを複数とLivetCallMethodActionへの対応を考えました

<Window
    x:Class="GUITestFriendly.Views.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    xmlns:v="clr-namespace:GUITestFriendly.Views"
    xmlns:vm="clr-namespace:GUITestFriendly.ViewModels"
    Title="MainWindow" Width="525" Height="350">
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <l:LivetCallMethodAction MethodName="Initialize" MethodTarget="{Binding}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <StackPanel>
        <TextBox Text="{Binding Lhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <TextBox Text="{Binding Rhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Click" ToolTip="ButtonClickCommand">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <l:LivetCallMethodAction MethodName="ButtonClickCommand" MethodTarget="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <TextBlock Text="{Binding Answer}" />
        <Button Content="Click" ToolTip="ButtonClickCommand2">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <l:LivetCallMethodAction MethodName="ButtonClickCommand2" MethodTarget="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <Button Content="Click" ToolTip="ButtonClickCommand3">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <l:LivetCallMethodAction MethodName="ButtonClickCommand3" MethodTarget="{Binding}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <Button Command="{Binding ACommand}" Content="Click" ToolTip="ACommand"/>
        <Button Command="{Binding BCommand}" CommandParameter="1" Content="Click" ToolTip="BCommand 1"/>
        <Button Command="{Binding BCommand}" CommandParameter="Q" Content="Click" ToolTip="BCommand Q"/>
    </StackPanel>
</Window>

テスト用のコード

ドライバー

GUIのアイテムを取得するやつ
これもほぼ一緒ですが複数ボタン用にしてます。

public class MainWindowDriver
{
    public WPFTextBox Lhs { get; }
    public WPFTextBox Rhs { get; }
    public WPFButtonBase Add { get; }
    public WPFTextBlock Answer { get; }
    //ここ追加
    public WPFButtonBase[] Buttons { get; }
    public IWPFDependencyObjectCollection<DependencyObject> LogicalTree { get; }

    public MainWindowDriver(dynamic window)
    {
        var w = new WindowControl(window);
        this.LogicalTree = w.LogicalTree();
        this.Lhs = new WPFTextBox(this.LogicalTree.ByBinding("Lhs").Single());
        this.Rhs = new WPFTextBox(this.LogicalTree.ByBinding("Rhs").Single());
        this.Answer = new WPFTextBlock(this.LogicalTree.ByBinding("Answer").Single());
        var btns = this.LogicalTree.ByType<Button>();
        this.Add = new WPFButtonBase(btns[0]);
        this.Buttons = Enumerable.Range(0, btns.Count).Select(i => new WPFButtonBase(btns[i])).ToArray();
    }
}

LogicalTreeはIWPFDependencyObjectCollection型で
コントロールの元の集合体です。
ここからBy~で必要な奴を絞りこんで一意のものを取得します。
コントロール(TextBoxとかButton)のタイプとバインド名とかが一致すれば一意に定まるようです。
ただ最初LogicalTreeはIWPFDependencyObjectCollectionな型で
コレクションって書いてあるからよし、linqだforeachだ思ってたらできなくて焦った。
インテリセンスにもなんかでないし
デバッグで確認してもよくわからない??
Single以外の取り出し方が???と思いましたが
この型の定義を確認するとthisインデクサがいました。

public interface IWPFDependencyObjectCollection<out T> where T : DependencyObject
{
    AppVar this[int index] { get; }
    int Count { get; }
    AppVar Single();
}

おう。どれがどれかかいまいち不明だけどアクセスは簡単にできるじゃないか。
配列のようにアクセスできました。
全部押してbind忘れているボタンがないかとかのチェックはthisインデクサ使ってforでOKだな。
レイアウトが単純な場合はこれでも問題なさそう。
レイアウトが複雑な場合は色々と工夫をしないとハマりそう。

コマンドをバインドしている場合でコマンドは同じでパラメータ違いのパターンはByBindingの後にByCommandParameterを付ければ一意に定まる。

var button = new WPFButtonBase(this.driver.LogicalTree.ByType<Button>().ByBinding("BCommand").ByCommandParameter("Q").Single());

ただ同じコマンドで同じパラメータの場合はどうしたら。
ある程度Gridとかで分割されない領域にある場合は、
同じボタンが2つあるっていう設計ミス扱いって感じで
設計ミスが拾えるという認識でよいのかな?

不明点

あとボタンを調べるときにCommandにバインドしていると
よくあるサンプルのように簡単だけど、
Livet使ってLivetCallMethodActionでボタンクリックを扱っている場合のバインドの方法が不明。
やり方を知っている人は教えて下さい。
現状ボタン一覧を取得してthisインデクサでアクセスしてボタン押してます。

今回作ったサンプルは

githubにおいてます。
https://github.com/kskhsn/GUITestFriendly

サンプルだと無駄にMsTest版とNunit版の2パターンのテストを置いています。

まとめ

WPFかつLivetのMVVMでもFriendlyのおかげでGUIテストができた。
LivetMethodCallActionの場合はボタンの番号指定で一応なんとかなる。

Friendlyは優秀でGUIテストだけでなく、
GUIの定型操作をプログラミングかマウスのできるマクロより優秀そう。
GUI操作のバッチ処理みたいのもできそう。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした