23
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Friendlyを使ったWPFのGUIテスト

Last updated at Posted at 2017-02-18

目的

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

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

参考にしたサイト

ありがとうございました。

インストール

↑のサイトとかぶっているだろうけど
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操作のバッチ処理みたいのもできそう。

23
27
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?