いきなりですが
最近Windows GUIアプリケーションの開発に関わるようになりました。自動テストに関して、一応ないわけではないですが、メンテ追い付かず動いたり、動かなかったりする、まぁよくある話ですね。私自身もそこまでテスト熱心なわけではないですが、何せデグレに恐れて夜眠れなくなる弱い人間なわけで、そんな心の声に従い長く付き合える自動テストを求める旅にでました。
Friendlyとの出会い
UIの自動テスト、マウス操作をレコードし、繰り返す実行するいわゆるキャプチャリプレイーはまず思いつくでしょう。RPAという呼び方も最近のはやりらしいが、これはダメですね(RPAがだめなわけではない)、断言しましょう。アプリの反応が早かったり、遅かったりするので、確実に捕まえる保証はないからです。まぁ、実際一回見ればすぐわかる話です。というわけで最初段階で候補から外しました。
ご存じのとおり、Windows GUIアプリ(Win32,WinForm,WPF)にはUI AutomationというUIの各種操作をシミュレートする機能を持つFWがあり、UIツリー構造を表示するVSの機能にも使用されているもので、一応自動テストに応用した記事をご紹介しますが、正直使いこなせる自信がないですね。
https://www.atmarkit.co.jp/fdotnet/special/uiautomation/uiautomation_01.html
そんなわけでいろいろ調べて、これ以上手がかりなければもうそろそろ諦めるという自分の勘所に来てこころが曇ってきたある日、Friendlyというものが目に入りました、最初に飛び込んだのはかずきさんの記事でした、まだFriendlyそのものの存在はしならい。
https://blog.okazuki.jp/archive/category/Friendly
初見の感想
かずきさんは足し算アプリの自動テストを紹介しました。UIを捕まえるにはByBindingという見慣れないもので、全部のUIが都合よくBinding使っているわけではないし、文字列なのでインテリセンス効かないし、変わってもビルド時気づかないし、どういうものかいまいちよさ理解できない自分がいました(ByTypeがあるのは知らないだけでした)。とはいえいままでにない期待感がわいてきましたので、早速入れてみることに
実際使ってみる
同時にFriendlyというキーワードを意識し、ネット記事の探しまくりです。そこでQiitaの2014年のAdvent Canlenderにたどり着き、作者は日本の方で、制作するまでのいきさつも語られてて、Frienldyを詳しくしりたいならこの方ブログがおすすめです。
https://qiita.com/advent-calendar/2014/friendly
http://ishikawa-tatsuya.hatenablog.com/
前置き長くなりました、本題に入ります。
まずここのソースを実際動かして感覚つかめることにしました。主要API使い方のを中心に、かなり洗練されている感じです、最初の人はまず見ておいたほうがいいと思います。
https://github.com/Ishikawa-Tatsuya/WPFFriendlySampleDotNetConf2016
いくつかポイントをご紹介します。詳しい説明ではなく、メモ程度のものなので、ご容赦ください。
//このXamlを前提に話します()
<Window x:Class="WpfApp1.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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TextBox x:Name="_text1" Text="{Binding Text1}"/>
<TextBox x:Name="_text2" Text="{Binding Text2}"/>
<Button Content="Command1" Command="{Binding Command1}"/>
<Button Content="Command2" Command="{Binding Command2}"/>
</Grid>
</Window>
//Friendlyの足場を作る作業
var dir = Path.GetFullPath("../../../WpfApp1/bin/Release");
var pathExe = dir + "/WpfApp1.exe";
var info = new ProcessStartInfo(pathExe) { WorkingDirectory = dir };
Process = Process.Start(pathExe); //対象アプリケーションの起動
app = new WindowsAppFriend(Process); //魔法の時間、これで相手のプロセスに潜り込んでやりたい放題
//UIインスタンスをつかんでみる
//おなじみのApplication.Current.MainWindowですね、
//MainWindowここにきてるように見えますが、実はきてません(相手プロセスにいます)
AppVar mainWindow = app.Type<Application>().Current.MainWindow;
// 明らかに一個しかない場合はこれで特定できるが、上のXamlではエラーになります。
var onlyOne = mainWindow.LogicalTree().ByType<TextBox>().Single();
// 普通はもう一段ByBinding書いて対象を絞る。
// ByType,ByBidingはDependencyObjectのコレクションを返すのでメソッドチェンできる
var textbox1 = mainWindow.LogicalTree().ByType<TextBox>().ByBinding("Text1").Single();
// 型参照できない場合は文字列のインターフェスを使う
var unkwownType = mainWindow.LogicalTree().ByType("ThirdPartyTextBox").Single();
// ちなみにあんまりおすすめできないがインデックスアクセスできます
var command1Button = mainWindow.LogicalTree().ByType<Button>()[0];
//x:Nameでの捕まえ方、実はリフレクションを使ったフィールドアクセス(と思います)
var textbox2 = mainWindow.Dynamic()._text2;
// リフレクションなのでVisablity関係ないですから、DataContextもとれる
// MVVMを採用しれいれば、通常DataContextが内部API詰まってるので、ユニットテストに活用する手もありです
// ちなみに、これは結合した状態の生きたインスタンスなので、普段ユニットテストで足場を作る作業は不要ですよ。
var dataContext = mainWindow.Dynamic().DataContext;
// staticメンバーへのアクセス
//インスタンス前提で話してきたが、staticメンバーの場合はこれでアクセスできます。(結構はまりました)
var staticMember = app.Type<MainWindowVM>().StaticMember
//UIのふるまいをシミュレートする
WPFTextBox wpfTextBox = new WPFTextBox(textbox1);
wpfTextBox.EmulateChangeText("NewValue");
WPFButtonBase wpfButtonBase = new WPFButtonBase(command1Button);
wpfButtonBase.EmulateClick();
UIテストの難所
いかがでしょうか、初歩的なこと一通り書きましたが、まぁ、実際のUIの操作は複雑でこんな一筋にはいきません。たとえばモーダルダイアログはどうでしょう。スレッドが止まるから相手プロセスに行ったきりですよね。そこでWindowControlとAsyncという仕組みが用意されていました。
http://ishikawa-tatsuya.hatenablog.com/entry/2015/01/05/230614
詳しくはここに書かれていますので、要点だけ
var mainWindow = new WindowControl(app);
var modalDialogButton = app.LogicalTree().ByType<ModalDialogButton>().Single;
//Asyncでクリックする、スレッドは止まらない
var async = new Async();
buttonModal.EmulateClick(async);
//モーダルダイアログが表示されるのを確実に待ち合わせる
var dlg = mainWindow.WaitForNextModal();
//ダイアログ上のボタンを押す、dlgからしか取れません。
var buttonOK = new WPFButtonBase(dlg.Dynamic()._buttonOK);
buttonOK .EmulateClick();
//非同期で実行したモーダルボタン押下の処理が完全に終了するのを待つ
async.WaitForCompletion();
これで一般的なモーダルダイアログパターンを突破できたわけですが、実はダイアログが表示されるか不確定な場合は結構厄介で悩みところです。表示されない場合のWaitForNextModal()は現在のTopのダイアログが返されて。dynamic型なので確実に型を判定できないのでそのあとの処理できず結構焦りました。
最後
そんなところですが、UIテスト自動化の戦いがまだまだ続きそうです。これからもFriendlyと末永く付き合いたいので、また新たなネタがありましたら書いていきたいと思います。