In this lab we will rewrite the previous tests so the code becomes more maintaintable.
We will apply the following principles:
- Single Responsibility (S in SOLID)
- Dont Repeat Yourself (DRY)
- Descriptive and Meaningfull Phrases (DAMP)
If you look at the tests we wrote, then you can see the test class takes a lot of responsibilities. It interacts with Appium, it starts the driver and cleans it up, in the tests we find element and we itneract with elements. How can we clean this up?
First start with moving the start and stop of the application to a class that will take care of this.
Create a new folder in the project called PageObjects
and in this folder you create a new static class called CarvedRockApplication
. This class implements two methods:
StartApplication()
CloseApplication()
Implement the StartApplication
method as follows:
public WindowsDriver<WindowsElement> StartApplication()
{
var capabilities = new AppiumOptions();
capabilities.AddAdditionalCapability(MobileCapabilityType.App, @"C:\temp\appium-hol\AppsToTest\WinForms\CarvedRock.exe");
capabilities.AddAdditionalCapability(MobileCapabilityType.PlatformName, "Windows");
capabilities.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC");
//start the application
_driver = new WindowsDriver<WindowsElement>(new Uri("http://127.0.0.1:4723/wd/hub"), capabilities);
return _driver;
}
And implement CloseApplication
as follows:
public static void CloseApplication()
{
if (_driver != null)
{
try
{
_driver.CloseApp();
}
finally
{
_driver.Dispose();
}
}
_driver = null;
}
Next, you replace all the code that starts the application in the previous created test methods with a call to CarvedRockApplication.StartApplication()
.
In the test code there is some duplicate code for finding things in the user interface. Apply the refactoring tools to extract these repetitions and place them in functions with a meaningful name.
Some examples:
Before:
// click add
var addButton = driver.FindElementByName("AddNewItem");
addButton.Click();
Refactor this into a function ClickAddOnMainForm()
and replace the repetitions in the other test functions
Before:
//close the dialog
var closeButton = AddDialogWindow.FindElementByName("Close");
closeButton.Click();
Refactor this into a method named CloseAddDialog()
and replace the repetitions in the other tests
Do this for most of the repetitions you find, and see how the code in the test becomes more readable and expressive of the intents you have instead of technical steps to get it done.
This is a bit more involved type of refactoring, where we are going to apply a well known pattern in the test industry called the PageObject Pattern.
We create a class, a so called page object, for every window we have in our application. So for our CarvedRock application this means we write the following classes that relfect all windows in the application:
- A class
MainForm
, representing the Main Form of the application - A class
AddNewItemDialog
, representing the Add New Item Dialog - A class
ItemDetail
, representing the Details dialog shown after clicking one row in the listview.
The MainForm
will abstract away all interactions with the Main Form. This means it needs to get public methods that abstract things we can do on the form. In this application this is limited to the following functions:
AddNewItem
- This triggers the Add New Item dialog and returns an
AddNewItemDialog
object
- This triggers the Add New Item dialog and returns an
ShowDetailOfItem
- This triggers the selection of an item in the listview and returns an instance of the
ItemDetail
class
- This triggers the selection of an item in the listview and returns an instance of the
Create the MainForm
class first like this:
public class MainForm
{
public AddNewItemDialog OpenNewItemDialog()
{
// click add button and return new dialog object
return new AddNewItemDialog();
}
public DetailsDialog SelectItemInList(string itemText)
{
// find item in the listview and return yourself
return this;
}
public bool IsItemInListview(string itemText)
{
// verify we can find the item in the list
return (true);
}
}
Next, we create the class that abstracts the AddNewItemDialog
. Here is the skeleton code for that class to get started:
public class AddNewItemDialog
{
public MainForm AddNewItem(string newItemText, string newItemDetailText)
{
// verify we are showing the dialog
// find the input field for ItemText
// clear the field
// send the keys
// find the input field for ItemDetail
// clear the field
// send the keys
// close the dialog
// return MainForm object
return new MainForm();
}
}
And create the class that abstracts the Details Dialog
public class DetailsDialog : BaseWinFormPageObject
{
public MainForm CloseDetailDialog()
{
// close dialog and return MainForm
return new MainForm();
}
}
We now have the main structure to write a test that becomes a meaningful phrase.
For example, we can now construct the following steps we want to test:
//arrange
var mainForm = CarvedRockApplication.StartApplication();
//act
var modifiedForm = mainForm.OpenNewItemDialog()
.AddNewItem("NewItem", "NewItemDetail")
.SelectItemInList("NewItem")
.CloseDetailDialog();
// assert
// close app
CarvedRockApplication.CloseApplication();
The only thing we need to do is to fix the fact that we need to pass the driver along in these classes, because all the methods that interact with the forms, need to use the driver to call one of the FindBy*
methods.
So we change the CarvedRockApplication.StartApplication();
function to return a new instance of the MainForm
class and when we create this instance we pass the created driver to the constructor. Se we create a new constructor in the MainForm
class.
Before we make this change, we want all page objects to have the same base behavior which is that it passes along the driver when it returns a new page object. This way the test code itself does not have to deal with this anymore, making the test clean.
Create a base class that all PageObjects will inherit from. This class looks as follows:
public class BaseWinFormPageObject
{
protected WindowsDriver<WindowsElement> _driver;
public BaseWinFormPageObject(WindowsDriver<WindowsElement> driver)
{
_driver = driver;
}
}
Next, make all PageObject classes inherit from this base class and give them all a constructor that accepts the driver and passes it along to the base class. Here is the example for the MainForm
class:
public class MainForm : BaseWinFormPageObject
{
public MainForm(WindowsDriver<WindowsElement> driver) : base(driver) { }
}
Do this for all page objects you created and make changes to the action methods that return new instances of a page object, and pass the _driver
to the constructor.
Finally, we need to implement the actual methods to interact with the windows. For this, you can borrow all the code you already wrote in the intial tests.
For example, to click on the Add button on the main form you can use the following code and add it to the MainForm
method OpenNewItemDialog()
:
public AddNewItemDialog OpenNewItemDialog()
{
// click add
var addButton = _driver.FindElementByName("AddNewItem");
addButton.Click();
return new AddNewItemDialog(_driver);
}
And for the method to select an item and show the details dialog you can do the following. I added some extra functionality to scroll to an item that we want to select:
public DetailsDialog SelectItemInList(string itemText)
{
while(!IsItemInListview(itemText))
{
ScrollDown();
}
FindElementByTextInList(itemText).Click();
return new DetailsDialog(_driver);
}
public bool IsItemInListview(string itemText)
{
// verify we can find the item in the list
AppiumWebElement newElement = FindElementByTextInList(itemText);
return (newElement != null);
}
private AppiumWebElement FindElementByTextInList(string itemText)
{
var listview = _driver.FindElementByAccessibilityId("listView1");
var newElement = listview.FindElementByName(itemText);
return newElement;
}
private void ScrollDown()
{
var scrollDownButton = _driver.FindElementByAccessibilityId("DownButton");
scrollDownButton.Click();
scrollDownButton.Click();
}
Continue implementing all methods until we can really test the application.
You have completed the lab if you can run the following test with success:
[TestMethod]
public void More_Maintainable_Version_Multiple_Add_Item_Dialog_AddsNewItem()
{
// arrange
var mainForm = CarvedRockApplication.StartApplication();
var numIterations = 5;
MainForm modifiedForm = null;
// act
while (numIterations-- > 0)
{
modifiedForm = mainForm
.OpenNewItemDialog()
.AddNewItem("NewItem" + numIterations, "NewItemDetail" + numIterations);
}
//assert
Assert.IsTrue(modifiedForm.IsItemInListview("NewItem4"));
CarvedRockApplication.CloseApplication();
}
It is easy to read that:
- we start the application
- open the New Item dialog multiple times
- validate we can find the 4th item we added to the list
This test is maintainable, because when a change is made to e.g. the Add New Item dialog, you only need to make a change in the way you interact with the dialog in that specific class. All tests always keep compiling and -when implemented correctly in the PageObject representing the dialog-, the test will also keep running.
If you want to see the results of this refactoring you can also switch to the MaintainableCode
branch in this repo. You will find the full implementation.