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 administrator
winAppDriverProcess = 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 session
if
(AppSession !=
null
)
{
AppSession.Close();
AppSession.Quit();
}
// Close the desktopSession
if
(DesktopSession !=
null
)
{
DesktopSession.Close();
DesktopSession.Quit();
}
}
public
static
void
StopWinappDriver()
{
// Stop the WinAppDriverProcess
if
(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.