ちょっと前から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が崩れるとか、なんか変、っていうバグの方が多いんだけど、画面遷移とかはこれで自動化して、人が見ないと分からないようなテストにリソースを集中できれば成功かな。
そして、まだ試してないけど問題文章も自動で打てるのだろうか。そうすると不正ツールを作れてしまうかも。とりあえず打ち込み速度が遅いからよさそうだけど、設定ができたりすると厄介。