Test automation plays an important part in application development. Let me share a walkthrough on how you can automate your WPF application using Appium along with Windows Application Driver and UI Recorder tool.
Every test automation needs a framework that it can rely on. One such framework is Appium—used for native, hybrid and mobile web apps. It covers iOS, Android and Windows platforms. Today I will focus on how your WPF app can benefit from it.
I will skip the setup of the WPF app, as this is not our goal here. It is a simple mail app demo with limited functionality for the purpose of this article. What I am going to do is to walk you through the process of creating your test project, setting it up and then writing several test scenarios. A full working demo is available in the following repo: Get-your-WPF-apps-automated-with-Appium.
private static void StartWinAppDriver(){ProcessStartInfo psi = new ProcessStartInfo(WinAppDriverPath);psi.UseShellExecute = true;psi.Verb = "runas"; // run as administratorwinAppDriverProcess = Process.Start(psi);}private const string WindowsApplicationDriverUrl = "http://127.0.0.1:4723";private const string ApplicationPath = @"..\..\..\..\MailApp\bin\Release\net5.0-windows\MailApp.exe";private const string DeviceName = "WindowsPC";private const int WaitForAppLaunch = 5;private const string WinAppDriverPath = @"C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe";private static Process winAppDriverProcess;public WindowsDriver<WindowsElement> AppSession { get; private set; }public WindowsDriver<WindowsElement> DesktopSession { get; private set; }public void Initialize(){StartWinAppDriver();var appiumOptions = new AppiumOptions();appiumOptions.AddAdditionalCapability("app", ApplicationPath);appiumOptions.AddAdditionalCapability("deviceName", DeviceName);appiumOptions.AddAdditionalCapability("ms:waitForAppLaunch", WaitForAppLaunch);this.AppSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), appiumOptions);Assert.IsNotNull(AppSession);Assert.IsNotNull(AppSession.SessionId);AppSession.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(1.5);AppiumOptions optionsDesktop = new AppiumOptions();optionsDesktop.AddAdditionalCapability("app", "Root");optionsDesktop.AddAdditionalCapability("deviceName", DeviceName);DesktopSession = new WindowsDriver<WindowsElement>(new Uri(WindowsApplicationDriverUrl), optionsDesktop);CloseTrialDialog();}protected void CloseTrialDialog(){this.GetElementByName("Telerik UI for WPF Trial").FindElementByName("Cancel").Click();}public void Cleanup(){// Close the sessionif (AppSession != null){AppSession.Close();AppSession.Quit();}// Close the desktopSessionif (DesktopSession != null){DesktopSession.Close();DesktopSession.Quit();}}public static void StopWinappDriver(){// Stop the WinAppDriverProcessif (winAppDriverProcess != null){foreach (var process in Process.GetProcessesByName("WinAppDriver")){process.Kill();}}}protected void SelectAllText(){Actions action = new Actions(AppSession);action.KeyDown(Keys.Control).SendKeys("a");action.KeyUp(Keys.Control);action.Perform();}protected void PerformDelete(){Actions action = new Actions(AppSession);action.SendKeys(Keys.Delete);action.Perform();}protected void PerformEnter(){Actions action = new Actions(AppSession);action.SendKeys(Keys.Enter);action.Perform();}protected void WriteText(string text){Actions action = new Actions(AppSession);action.SendKeys(text);action.Perform();}Now that we have our TestsBase ready, we can move into writing some tests.
[TestClass]public class MailAppTests : TestsBase{[TestInitialize]public void TestInitialize(){this.Initialize();}. . .}[TestClass]public class MailAppTests : TestsBase{[TestInitialize]public void TestInitialize(){this.Initialize();}[TestCleanup]public void TestCleanup(){this.Cleanup();}[ClassCleanup]public static void ClassCleanusp(){StopWinappDriver();}Our tests should be able to locate elements so we can react with them. UI Recorder is an open-source tool for selecting UI elements and viewing their attributes’ data. You can find the tool in the WinAppDriver source code downloaded earlier at the beginning of this article. It is located in the ...\WinappDriver\Tools\UIRecorder folder. Simply open and run the solution. You can now select the elements you need and use their attributes by clicking the “Record” button and hovering on the target element.
One way to find elements is to use session’s FindElementsByXPath(string XPath) method, which takes the XPath string as a parameter. The XPath can be taken using the UI Recorder tool.
Another faster and more convenient approach if possible is to use FindElementByAccessibilityId(string automationID) method, also exposed by the session. The AccessibilityID is the identifier assigned to the elements in our test app. It can be applied using the attached property AutomationProperties.AutomationId. For example:
<telerik:RadRibbonButton Text="Reply"LargeImage="..\Images\Reply.png"Command="{Binding ReplyCommand}"Size="Large"CollapseToSmall="WhenGroupIsMedium"telerik:ScreenTip.Title="Reply"telerik:ScreenTip.Description="Reply to the sender of this message."telerik:KeyTipService.AccessText="R"AutomationProperties.AutomationId="buttonReply"/>For our convenience and for code readability, we create another class named TestsBaseExtensions. The purpose of this class is to hold in one place all logic we need for locating our elements.
Yet another way to find elements is by their names—FindElementByName(string elementName) where you pass the name of the element as a string.
Our first test will verify that the email fields of a selected email are properly filled in.
[TestMethod]public void AssureEmailFieldsAreFilledInTest(){string replyTo = "SethBarley@telerikdomain.es";string replyCC = "SethCavins@telerikdomain.uk";string subject = "RE: Let's have a party for new years eve";this.GetButtonReply().Click();Thread.Sleep(1000);Assert.AreEqual(replyTo, this.GetFieldReplyTo().Text);Assert.AreEqual(replyCC, this.GetFieldReplyCc().Text);Assert.AreEqual(subject, this.GetFieldSubject().Text);}public static class TestsBaseExtensions{private const string ButtonReplydId = "buttonReply";. . . . .public static WindowsElement GetButtonReply(this TestsBase testsBase){return testsBase.AppSession.FindElementByAccessibilityId(ButtonReplydId);}Using XPath can be trickier when you have elements that correspond to the same path. In such cases using FindElementsByXPath is useful as in the code above. This way you can further specify which element exactly you are trying to locate.
private const string fieldsReplyToCcSubjectXPath = "/Window[@Name=\"Inbox - Mark@telerikdomain.com - My Application\"][@AutomationId=\"MyApplication\"]/Custom[@ClassName=\"MailView\"]/Custom[@ClassName=\"RadDocking\"][@Name=\"Rad Docking\"]/Custom[@ClassName=\"RadSplitContainer\"][@Name=\"Rad Split Container\"]/Tab[@ClassName=\"RadPaneGroup\"][@Name=\"Rad Pane Group\"]/Edit[@ClassName=\"TextBox\"]";public static WindowsElement GetFieldReplyTo(this TestsBase testsBase){return testsBase.AppSession.FindElementsByXPath(fieldsReplyToCcSubjectXPath)[0];}Our second test is about replying to an email. Finding and locating elements is done the same way as in the previous example. See how the SelectAllText(), PerformDelete(), WriteText(string text) from our parent class TestsBase are used here. They use the Actions we mentioned earlier.
[TestMethod]public void ReplyToAnEmailTest(){string textToWrite = "Writing some text here...";this.GetButtonReply().Click();this.GetRichTextBoxReply().Click();this.SelectAllText();this.PerformDelete();this.WriteText(textToWrite);Assert.AreEqual(textToWrite, this.GetRichTextBoxReply().Text);this.GetButtonSendEmail().Click();Assert.AreEqual(@"Send's command executed.", this.GetElementByName(@"Send's command executed.").Text);this.GetElementByName("OK").Click();}I would like to draw your attention to the GetElementByName(string name) method. After the SendEmail button is clicked, a dialog that is not part of the same visual tree appears on the screen. To get that dialog and find the element, we first try to find the element using the AppSession , but, if this fails, then we use the DesktopSession from the base class. As you probably remember, the DesktopSession uses the whole desktop visual tree to find the element.
public static WindowsElement GetElementByName(this TestsBase testsBase, string elementName){try{return testsBase.AppSession.FindElementByName(elementName);}catch{Logger.LogMessage("Element was not found using the AppSession. Trying to locate the element using the DesktopSession.");}return testsBase.DesktopSession.FindElementByName(elementName);}In the third and final test in our test project, we verify that emails are properly marked as read.
[TestMethod]public void MarkMessageAsReadTest(){this.GetTabUnreadEmail().Click();this.GetUnreadEmailCellByFromAddress("SethCavins@telerikdomain.uk").Click();this.GetUnreadEmailCellByFromAddress("JimmieFields@telerikdomain.eu").Click();Assert.AreEqual("[31]", this.GetUInboxUnreadMessagesCount().Text);}In this walk-through article we were able to cover:
Feel free to download and explore the full demo app and the test project we worked on. I hope this walkthrough will help you get started with automating your WPF applications using Appium and WinAppDriver. Any feedback or comments on the topic are more than welcome.
Petar Horozov is a Principal QA Engineer at Progress with more than 12 years of experience. He holds a master degree in Software Technologies. He is keen on cooking and loves sports, especially skiing and CrossFit.