ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

FriendlyでWPFアプリをテストするときのコツ

WPFのアプリをテストするときは躓きやすいのでちょっとコツを書きます。

こんな感じの構成のときどうやって操作していいのか最初はちょっと難しいですよね。
f:id:ishikawa-tatsuya:20200517124431p:plain

<NavigationWindow x:Class="WpfAppSample.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:WpfAppSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
</NavigationWindow>
<Page x:Class="WpfAppSample.SamplePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      xmlns:local="clr-namespace:WpfAppSample"
      mc:Ignorable="d" 
      d:DesignHeight="450" d:DesignWidth="800"
      Title="SamplePage">

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <local:SampleUserControl Grid.Column="0">
        </local:SampleUserControl>
        <Canvas Grid.Column="1">
            <TextBox Height="119" Canvas.Left="82" TextWrapping="Wrap" Text="{Binding Data}" Canvas.Top="182" Width="224"/>
        </Canvas>
    </Grid>
</Page>
<UserControl x:Class="WpfAppSample.SampleUserControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfAppSample"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Canvas Background="#FFC9D6F6">
        <ListView Height="368" Width="340" Canvas.Left="25" Canvas.Top="56" ItemsSource="{Binding Users}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding Name}"/>
                        <CheckBox Content="Engineer" IsChecked="{Binding IsEngineer}"/>
                    </StackPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Button Content="Add" Canvas.Left="227" Canvas.Top="28" Width="75" Click="Button_Click"/>
        <TextBox Height="23" Canvas.Left="25" TextWrapping="Wrap" Text="{Binding Name}" Canvas.Top="28" Width="120"/>
        <CheckBox Content="Engineer" Canvas.Left="150" Canvas.Top="30" Width="77" IsChecked="{Binding IsEngineer}"/>
    </Canvas>
</UserControl>

ドライバを作る

まずはドライバを作ります。このサンプルだと以下に作ります。一つ一つ作っていくところがポイントですね。

  • Window
  • Page
  • UserControl
  • ItemsControlのItem

要素の特定

特定方法ですが、Windowを捕まえるのはWindowControlのメソッドを使います。これはWinFormsと同じですね。以下WindowsAppFriendに対する拡張メソッドにしていますが中身はWindowControlのstaticメソッドです。

var win = app.WaitForIdentifyFromTypeFullName("WpfAppSample.MainWindow");

それで、Window以下に関しては、x:nameでアクセスできるならWinFormsの時と同じくフィールドを使ってもらえばいいのですが、ついていない場合はVisualTreeをだどって見つけます。前に書いてたのでそのリンクを貼っておきます。
ishikawa-tatsuya.hatenablog.com
ishikawa-tatsuya.hatenablog.com

ドライバのサンプル

using Codeer.Friendly.Windows;
using Codeer.Friendly.Windows.Grasp;

namespace Driver.Windows
{
    //ドライバ
    public class MainWindowDriver
    {
        public WindowControl Core { get; }

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

    //捕まえるための拡張
    public static class MainWindowDriverExtensions
    {
        public static MainWindowDriver AttachMainWindow(this WindowsAppFriend app)
            => new MainWindowDriver(app.WaitForIdentifyFromTypeFullName("WpfAppSample.MainWindow"));
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;

namespace Driver.Windows
{
    public class SamplePageDriver
    {
        public WPFUserControl Core { get; }
        public WPFTextBox TextBox => Core.LogicalTree().ByBinding("Data").Single().Dynamic();

        public SamplePageDriver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //MainWindowからSamplePageを取得するための拡張
    public static class SamplePageDriverExtensions
    {
        public static SamplePageDriver AttachSamplePage(this MainWindowDriver window)
            => window.Core.VisualTree().ByType("WpfAppSample.SamplePage").Single().Dynamic();
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;
using System.Windows.Controls;

namespace Driver.Windows
{
    public class SampleUserControl_Driver
    {
        public WPFUserControl Core { get; }
        public WPFListView ListView => Core.LogicalTree().ByBinding("Users").Single().Dynamic();
        public WPFButtonBase Add => Core.LogicalTree().ByType<Button>().ByContentText("Add").Single().Dynamic();
        public WPFTextBox TextBox => Core.LogicalTree().ByBinding("Name").Single().Dynamic();
        public WPFToggleButton Engineer => Core.LogicalTree().ByBinding("IsEngineer").Single().Dynamic();

        public SampleUserControl_Driver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //SamplePageからSampleUserControlを取得するための拡張
    public static class SampleUserControlDriverExtensions
    {
        public static SampleUserControl_Driver Attach_SampleUserControl(this SamplePageDriver page)
            => page.Core.VisualTree().ByType("WpfAppSample.SampleUserControl").Single().Dynamic();
    }
}
using Codeer.Friendly;
using Codeer.Friendly.Dynamic;
using RM.Friendly.WPFStandardControls;

namespace Driver.Windows
{
    public class UserListViewItemDriver
    {
        public WPFUserControl Core { get; }
        public WPFTextBlock TextBlock => Core.VisualTree().ByBinding("Name").Single().Dynamic();
        public WPFToggleButton Engineer => Core.VisualTree().ByBinding("IsEngineer").Single().Dynamic();

        public UserListViewItemDriver(AppVar core)
        {
            Core = new WPFUserControl(core);
        }
    }

    //ListViewItemにUserListViewItemDriverを適用するための拡張メソッド
    public static class UserListViewItemDriverExtensions
    {
        public static UserListViewItemDriver AsUser(this WPFListViewItem item)
            => new UserListViewItemDriver(item.AppVar);
    }
}

シナリオのサンプル

using System.Diagnostics;
using Codeer.Friendly.Windows;
using Driver;
using NUnit.Framework;
using Driver.Windows;

namespace Scenario
{
    [TestFixture]
    public class Test
    {
        WindowsAppFriend _app;

        [SetUp]
        public void TestInitialize() => _app = ProcessController.Start();

        [TearDown]
        public void TestCleanup() => Process.GetProcessById(_app.ProcessId).Kill();

        [Test]
        public void TestMethod()
        {
            //Windowを見つける
            var mainWindow = _app.AttachMainWindow();

            //ページを見つける
            var samplePage = mainWindow.AttachSamplePage();

            //UserControlを見つける
            var sampleUserControl = samplePage.AttachSampleUserControl();

            //アイテムを取得
            var item = sampleUserControl.ListView.GetItem(1);
            
            //アイテム一つに対するドライバにする
            var userItem = item.AsUser();

            //操作
            userItem.Engineer.EmulateCheck(true);
        }
    }
}

もっと複雑なのは?

おそらく操作する対象を特定するのが難しいのだと思います。これ以上複雑な場合はFriendlyの基本機能、「対象プロセスのAPIをなんでも使える」というのを使って最適な特定方法を作ってみてください。その場合、必要なのはFriendlyの知識ではなくWPFの知識だったりします。頑張ってください!