ちょっと前からWeather TypingのWindowsデスクトップ版以外の更新をやっているのだが、テストが2300件くらいあって、さすがにテストが大変。てことでUIテストの自動化を本格的に調査。1週間くらいかかってようやく見えてきたのでメモ。ちなみにAndroid/UWP/WPFは実験成功して、Mac/iOSはまだ試せていない。
UIテストはいろいろな方法があると思うけど、マルチプラットフォーム対応だとAppiumがよさそう。Appiumは、WebのUIテスト自動化Seleniumと同様の方法でモバイルアプリのテストをするフレームワーク。サーバクライアントになっていて、クライアントでテストコードを書くと、サーバにJSON形式のコマンドを投げて、サーバからアプリのUIに向かってキーボードやマウスのイベントを投げてくれる。AppiumのサーバだとAndroidとiOSがテストできるが、Windows Application Driverを使えばWindowsのWPFやUWPもテストができる。クライアントについては、Pythonなどでやるのが普通かもしれないけど、Appium.WebDriverを使えばでC#で開発が可能。
まとめると、C#でUIをテストするコードを書いて、Appiumのサーバを起動すればAndroid/iOSのUIテスト、Windows Application Driverを起動すればWPF/UWPのテストが可能ということ。さらに一工夫いるが、Xamarin.Formsのアプリもテスト可能である。
- Appiumのインストール
- ここは普通にAppiumとAppium-doctorをインストール。Pythonとかnodeとかそのあたり。Appium-doctorを通すためにいろいろインストールする必要がある。だいたいはなんとかなるはずだが、Windowsで「bundletool.jar cannot be found」になるので環境変数「PATHEXT」に「.JAR」を追加する。これ1日はまったけどappium-doctorのソースまで見てようやく分かった。
- Windows Application Driverのインストール
- 普通にインストール。特に困ることはない
- Appium.WebDriverのインストール
- とりあえずVisual StudioでUnitTestプロジェクトを作り、NuGetからインストールした。UnitTestなの? って思うけど・・・別にコンソールアプリでもいいのだが、テストケースの管理とかでUnitTestがいいかな、ってくらい
- UI部品にIDを振る
- テストの時にUI部品を特定するため、それぞれIDを振る。Appiumではいくつか特定の仕方があるが、これ実質Accessibility IDしか使えないよね。てことでXamarinのXAMLで、ButtonとかにAutomationId=”hoge”を入れる。だけでよければ話は簡単なんだけど、Android、UWPはうまくいったがWPFはダメだった。Xamarinのソースを見る限りWPFではViewRendererでSetAutomationIdをやっていないみたい。なので、Effectを使って特定の属性を設定すると、各プラットフォーム向けにAccessibility IDに対応する属性を設定するようにしてみた。これもEffectなの? って感じだけど、これ以外にうまい方法は思い浮かばない。
まずはEffectの属性。プラットフォーム共通。
public static class TestEffect { public static readonly BindableProperty NameProperty = BindableProperty.CreateAttached("Name", typeof(string), typeof(TestEffect), "", propertyChanged: OnNameChanged); public static string GetName(BindableObject view) { return (string)view.GetValue(NameProperty); } public static void SetName(BindableObject view, string value) { view.SetValue(NameProperty, value); } private static void OnNameChanged(BindableObject bindable, object oldValue, object newValue) { var view = bindable as View; if (view == null) { return; } var name = (string)newValue; view.Effects.Add(new UITestEffect()); } } class UITestEffect : RoutingEffect { public UITestEffect() : base("com.denasu.UITestEffect") { } }
次に各プラットフォーム用のEffect。
[assembly: ResolutionGroupName("company name")] [assembly: ExportEffect(typeof(App1.WPF.UITestEffect), "UITestEffect")] namespace App1.WPF { public class UITestEffect : PlatformEffect { protected override void OnAttached() { var name = TestEffect.GetName(Element); Control.SetValue(System.Windows.FrameworkElement.NameProperty, name); } protected override void OnDetached() { Control.SetValue(System.Windows.FrameworkElement.NameProperty, ""); } } }
[assembly: ResolutionGroupName("company name")] [assembly: ExportEffect(typeof(App1.UWP.UITestEffect), "UITestEffect")] namespace App1.UWP { public class UITestEffect : PlatformEffect { protected override void OnAttached() { var name = TestEffect.GetName(Element); Control.SetValue(Windows.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty, name); } protected override void OnDetached() { Control.SetValue(Windows.UI.Xaml.Automation.AutomationProperties.AutomationIdProperty, ""); } } }
[assembly: ResolutionGroupName("company name")] [assembly: ExportEffect(typeof(App1.Droid.UITestEffect), "UITestEffect")] namespace App1.Droid { public class UITestEffect : PlatformEffect { protected override void OnAttached() { var name = TestEffect.GetName(Element); Control.ContentDescription = name; } protected override void OnDetached() { Control.ContentDescription = ""; } } }
XAMLではEffectを付ける。
<Entry local:TestEffect.Name="Text1" Text="{Binding TextA}" WidthRequest="100" /> <Entry local:TestEffect.Name="Text2" Text="{Binding TextB}" WidthRequest="100"/> <Label local:TestEffect.Name="Result" Text="{Binding TextC}" WidthRequest="100"/> <Button local:TestEffect.Name="Execute" Text="Button" Clicked="Button_Click" WidthRequest="100"/>
C#ではこんな感じ。各プラットフォームのセッションを作って、共通のテストを実行するようにしてみた。
[TestClass] public class UnitTest { [TestMethod] public void TestMethod1() { var session = CreateSessionUWP(); session.FindElementByAccessibilityId("Text1").Click(); session.Keyboard.PressKey("111"); session.FindElementByAccessibilityId("Text2").Click(); session.Keyboard.PressKey("222"); session.FindElementByAccessibilityId("Execute").Click(); Assert.AreEqual(session.FindElementByAccessibilityId("Result").Text , "111222"); } private WindowsDriver<WindowsElement> CreateSessionUWP() { DesiredCapabilities appCapabilities = new DesiredCapabilities(); appCapabilities.SetCapability("app", "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXXXXXXXXXXXX!App"); return new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities); } private WindowsDriver<WindowsElement> CreateSessionWPF() { DesiredCapabilities appCapabilities = new DesiredCapabilities(); appCapabilities.SetCapability("app", @"c:\xxx\App1.WPF.exe"); return new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723"), appCapabilities); } private AndroidDriver<AndroidElement> CreateSessionAndroid() { DesiredCapabilities appCapabilities = new DesiredCapabilities(); appCapabilities.SetCapability(MobileCapabilityType.DeviceName, "emulator-5554"); appCapabilities.SetCapability(MobileCapabilityType.PlatformVersion, "8.1"); appCapabilities.SetCapability(AndroidMobileCapabilityType.AppPackage, "com.companyname.App1"); appCapabilities.SetCapability(MobileCapabilityType.PlatformName, "Android"); appCapabilities.SetCapability(AndroidMobileCapabilityType.AppActivity, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.MainActivity"); return new AndroidDriver<AndroidElement>(new Uri("http://127.0.0.1:4723/wd/hub"), appCapabilities); }
実行はこんな感じ。静止画じゃ分からないけど、勝手に文字が入力されてボタンが押される。
てことで、単純なUIのテストはこれで自動化できるかな。まあ、UIが崩れるとか、なんか変、っていうバグの方が多いんだけど、画面遷移とかはこれで自動化して、人が見ないと分からないようなテストにリソースを集中できれば成功かな。
そして、まだ試してないけど問題文章も自動で打てるのだろうか。そうすると不正ツールを作れてしまうかも。とりあえず打ち込み速度が遅いからよさそうだけど、設定ができたりすると厄介。