かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Friendlyを入門してみた

デスクトップアプリのテストを行うためのFriendlyというライブラリのハンズオンの補助講師してきた(受講者に非常に近い立ち位置で)のでちょっと試してみました。WPFの足し算アプリを用意してみた。XAMLがわかればテストできる感じなので、XAMLだけさくっとさらしておきます。

<Window x:Class="WpfApplication4.MainWindow"
        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:local="clr-namespace:WpfApplication4"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Lhs.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Text="{Binding Rhs.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <Button Content="Click" Command="{Binding AddCommand}"/>
        <TextBlock Text="{Binding Answer.Value}" />
    </StackPanel>
</Window>

WPFのテストをするならRM.Friendly.WPFStandardControlsをNuGetから参照追加するといい感じです。

左辺値右辺値を入れてボタンを押すと答えが出る感じです。一応入力値チェックしてて、正しい入力をしないとボタンが押せなくしてます。Friendlyでテストするときは、Processをスタートしてアタッチするという感じのコードになります。プロセスの起動はProcess.Startでさくっとやります。

そして、WindowsAppFriendというクラスにProcessを渡すことで内部でいじくれるようにしてくれてます。

this.process = Process.Start(@"WpfApplication4.exe");
this.app = new WindowsAppFriend(this.process);

このWindowsAppFriendのTypeメソッドで相手プロセス内にある型をひっこぬいてくることができます。文字列指定や型引数指定などの方法がありますが、ここでは文字列指定でWPFのApplication型をひっこぬいてきました。ApplicationがとれたらCurrentのMainWindowでテスト対象アプリのWindowにアクセスできます。

app.Type("System.Windows.Application").Current.MainWindow

Typeメソッドの戻り値はdynamic型なのでなんでも呼び出せる!というのもいいんですがタイプセーフな感じのものでラップしてやるとテストが捗ります。先ほどのMainWindowをラップする感じでこんなクラスを定義します。WindowControlのLogicalTree拡張メソッドで論理ツリーがとれて、それに対してBindingのパスでコントロールを探せます。

public class MainWindowDriver
{
    public WPFTextBox Lhs { get; }
    public WPFTextBox Rhs { get; }
    public WPFButtonBase Add { get; }
    public WPFTextBlock Answer { get; }

    public MainWindowDriver(dynamic window)
    {
        var w = new WindowControl(window);
        this.Lhs = new WPFTextBox(w.LogicalTree().ByBinding("Lhs.Value").Single());
        this.Rhs = new WPFTextBox(w.LogicalTree().ByBinding("Rhs.Value").Single());
        this.Add = new WPFButtonBase(w.LogicalTree().ByBinding("AddCommand").Single());
        this.Answer = new WPFTextBlock(w.LogicalTree().ByBinding("Answer.Value").Single());
    }
}

このクラスを使ったテストの全体はこんな感じ。割とさくっと書けました。

using Codeer.Friendly.Dynamic;
using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RM.Friendly.WPFStandardControls;
using System.Diagnostics;

namespace UnitTestProject1
{
    [TestClass]
    public class UnitTest1
    {
        private Process process;
        private WindowsAppFriend app;
        private MainWindowDriver driver;

        [TestInitialize]
        public void Initialize()
        {
            this.process = Process.Start(@"WpfApplication4.exe");
            this.app = new WindowsAppFriend(this.process);
            this.driver = new MainWindowDriver(app.Type("System.Windows.Application").Current.MainWindow);
        }

        [TestCleanup]
        public void Cleanup()
        {
            this.app.Dispose();
            this.process.Kill();
        }

        [TestMethod]
        public void TestMethod1()
        {
            this.driver.Lhs.EmulateChangeText("10");
            this.driver.Rhs.EmulateChangeText("3");
            this.driver.Add.EmulateClick();
            Assert.AreEqual("13", this.driver.Answer.Text);
        }

        [TestMethod]
        public void TestMethod2()
        {
            this.driver.Lhs.EmulateChangeText("aaa");
            this.driver.Rhs.EmulateChangeText("3");
            this.driver.Add.EmulateClick();
            Assert.AreEqual("0", this.driver.Answer.Text);
        }
    }

    public class MainWindowDriver
    {
        public WPFTextBox Lhs { get; }
        public WPFTextBox Rhs { get; }
        public WPFButtonBase Add { get; }
        public WPFTextBlock Answer { get; }

        public MainWindowDriver(dynamic window)
        {
            var w = new WindowControl(window);
            this.Lhs = new WPFTextBox(w.LogicalTree().ByBinding("Lhs.Value").Single());
            this.Rhs = new WPFTextBox(w.LogicalTree().ByBinding("Rhs.Value").Single());
            this.Add = new WPFButtonBase(w.LogicalTree().ByBinding("AddCommand").Single());
            this.Answer = new WPFTextBlock(w.LogicalTree().ByBinding("Answer.Value").Single());
        }
    }
}