Advanced Reuse Tactics for Grid Controls Automated Tests

WebDriver
33 Shares
Advanced Reuse Tactics

In my previous articles Design Grid Control Automated Tests Part 1Design Grid Control Automated Tests Part 2Design Grid Control Automated Tests Part 3 I started a mini-series about writing proper grid control's automated tests. This will be the fourth final part. Here I am going to talk about how we can reuse to the maximum extent the logic that we  already have created for asserting the controls.

Test Cases Reuse Problem

Here is how one existing grid control's automated test looks like.

[TestMethod]
public void OrderIdEqualToFilter()
{
this.driver.Navigate().GoToUrl(@"http://demos.telerik.com/kendo-ui/grid/frozen-columns");
var kendoGrid = new KendoGrid(this.driver, this.driver.FindElement(By.Id("grid")));
var newItem = this.CreateNewItemInDb();
kendoGrid.Filter(
OrderIdColumnName,
Enums.FilterOperator.EqualTo,
newItem.OrderId.ToString());
this.WaitForGridToLoad(1, kendoGrid);
var items = kendoGrid.GetItems<GridItem>();
Assert.AreEqual(1, items.Count);
}

The test case tests that the order id's column filter is working as expected. In real-world applications many times we have grids that display almost the same data in a little different manner. For example, you can have one grid that displays the expired orders and one for the successfully completed. Both grids have almost identical columns. However, you need to automate both grids because usually a custom code is added to enable the desired behaviour such as filtering by status, etc.

Expired Orders Grid

First Orders Grid Control

Successfully Completed Orders Grid 

Second Orders Grid Control

Advanced Reuse Tactics

We will have almost identical tests but sometimes some of the columns might be missing or new columns might be displayed so we need a solution where we can configure which column to be verified or not. So my idea is to create different assert classes for each column. This way through a composition in your tests you can design-time choose which column to be asserted or not.

IGridPage Interface

Until now some of the tests worked with a single page or with the kendo grid controls directly. Nonetheless, in order to make the column's asserter more generic, it should work with an interface. Because of that, I have created the new IGridPage interface.

public interface IGridPage
{
KendoGrid Grid { get; }
IWebElement PagerInfoLabel { get; set; }
IWebElement GoToNextPage { get; set; }
IWebElement GoToFirstPageButton { get; set; }
IWebElement GoToLastPage { get; set; }
IWebElement GoToPreviousPage { get; set; }
IWebElement NextMorePages { get; set; }
IWebElement PreviousMorePages { get; set; }
IWebElement PageOnFirstPositionButton { get; set; }
IWebElement PageOnSecondPositionButton { get; set; }
IWebElement PageOnTenthPositionButton { get; set; }
void NavigateTo();
}

All pages that contain grid control should be implemented it. This way your columns asserters can be used against the different pages and their grids.

This is how our first grid's page looks like.

public class GridFilterPage : IGridPage
{
public readonly string Url = @"http://demos.telerik.com/kendo-ui/grid/filter-row";
private readonly IWebDriver driver;
public GridFilterPage(IWebDriver driver)
{
this.driver = driver;
PageFactory.InitElements(driver, this);
}
public KendoGrid Grid
{
get
{
return new KendoGrid(this.driver, this.driver.FindElement(By.Id("grid")));
}
}
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/span")]
public IWebElement PagerInfoLabel { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/a[3]")]
public IWebElement GoToNextPage { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/a[1]")]
public IWebElement GoToFirstPageButton { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/a[4]/span")]
public IWebElement GoToLastPage { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/a[2]/span")]
public IWebElement GoToPreviousPage { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/ul/li[12]/a")]
public IWebElement NextMorePages { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/ul/li[2]/a")]
public IWebElement PreviousMorePages { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/ul/li[2]/a")]
public IWebElement PageOnFirstPositionButton { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/ul/li[3]/a")]
public IWebElement PageOnSecondPositionButton { get; set; }
[FindsBy(How = How.XPath, Using = "//*[@id='grid']/div[3]/ul/li[11]/a")]
public IWebElement PageOnTenthPositionButton { get; set; }
public void NavigateTo()
{
this.driver.Navigate().GoToUrl(this.Url);
}
}

Reuse Grid Control's Helper Methods

As you can recall from the previous articles from the series, we needed different helper methods like WaitForPageToLoadUntilGetAllItemsFromDb, etc. They were placed inside the test class as private methods. However, if we have different asserters and move the whole logic in them, we will need these methods for the different test cases. By cause of that, we can move them to a new base asserter class that all other columns' asserters are going to derive from.

public class GridColumnAsserter
{
public GridColumnAsserter(IGridPage gridPage)
{
this.GridPage = gridPage;
}
protected IGridPage GridPage { get; set; }
protected void WaitForPageToLoad(int expectedPage, KendoGrid grid)
{
this.Until(() =>
{
int currentPage = grid.GetCurrentPageNumber();
return currentPage == expectedPage;
});
}
protected void WaitForGridToLoad(int expectedCount, KendoGrid grid)
{
this.Until(
() =>
{
var items = grid.GetItems<GridItem>();
return expectedCount == items.Count;
});
}
protected void WaitForGridToLoadAtLeast(int expectedCount, KendoGrid grid)
{
this.Until(
() =>
{
var items = grid.GetItems<GridItem>();
return items.Count >= expectedCount;
});
}
protected void Until(
Func<bool> condition,
int timeout = 10,
string exceptionMessage = "Timeout exceeded.",
int retryRateDelay = 50)
{
DateTime start = DateTime.Now;
while (!condition())
{
DateTime now = DateTime.Now;
double totalSeconds = (now - start).TotalSeconds;
if (totalSeconds >= timeout)
{
throw new TimeoutException(exceptionMessage);
}
Thread.Sleep(retryRateDelay);
}
}
protected List<Order> GetAllItemsFromDb()
{
// Create dummy orders. This logic should be replaced with service oriented call
// to your DB and get all items that are populated in the grid.
List<Order> orders = new List<Order>();
for (int i = 0; i < 10; i++)
{
orders.Add(new Order());
}
return orders;
}
protected Order CreateNewItemInDb(string shipName = null)
{
// Replace it with service oriented call to your DB. Create real enity in DB.
return new Order(shipName);
}
protected void UpdateItemInDb(Order order)
{
// Replace it with service oriented call to your DB. Update the enity in the DB.
}
protected int GetUniqueNumberValue()
{
var currentTime = DateTime.Now;
int result = currentTime.Year +
currentTime.Month +
currentTime.Hour +
currentTime.Minute +
currentTime.Second +
currentTime.Millisecond;
return result;
}
}

Concrete Grid Column Asserter's Implementation

The next part of the refactoring is to create the different column asserters. Below you can find the concrete asserter for the OrderId column. It derives from the GridColumnAsserter class.

public class OrderIdColumnAsserter : GridColumnAsserter
{
public OrderIdColumnAsserter(IGridPage gridPage) : base(gridPage)
{
}
public void OrderIdEqualToFilter()
{
this.GridPage.NavigateTo();
var newItem = this.CreateNewItemInDb();
this.GridPage.Grid.Filter(
GridColumns.OrderID,
Enums.FilterOperator.EqualTo,
newItem.OrderId.ToString());
this.WaitForGridToLoad(1, this.GridPage.Grid);
var items = this.GridPage.Grid.GetItems<GridItem>();
Assert.AreEqual(1, items.Count);
}
public void OrderIdGreaterThanOrEqualToFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Create second new item with the same unique shipping name
var secondNewItem = this.CreateNewItemInDb(newItem.ShipName);
// When we filter by the second unique column ShippingName,
// only one item will be displayed. Once we apply the second
// not equal to filter the grid should be empty.
this.GridPage.Grid.Filter(
new GridFilter(
GridColumns.OrderID,
Enums.FilterOperator.GreaterThanOrEqualTo,
newItem.OrderId.ToString()),
new GridFilter(
GridColumns.ShipName,
Enums.FilterOperator.EqualTo,
newItem.ShipName));
this.WaitForGridToLoadAtLeast(2, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.AreEqual(
secondNewItem.OrderId,
results.FirstOrDefault(x => x.ShipName == secondNewItem.ShipName).OrderId);
Assert.AreEqual(
newItem.OrderId,
results.FirstOrDefault(x => x.ShipName == newItem.ShipName).OrderId);
Assert.IsTrue(results.Count() == 2);
}
public void OrderIdGreaterThanFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Create second new item with the same unique shipping name
var secondNewItem = this.CreateNewItemInDb(newItem.ShipName);
// Filter by the smaller orderId but also by the second unique
// column in this case shipping name
this.GridPage.Grid.Filter(
new GridFilter(
GridColumns.OrderID,
Enums.FilterOperator.GreaterThan,
newItem.OrderId.ToString()),
new GridFilter(
GridColumns.ShipName,
Enums.FilterOperator.EqualTo,
newItem.ShipName));
this.WaitForGridToLoadAtLeast(1, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.AreEqual(
secondNewItem.OrderId,
results.FirstOrDefault(x => x.ShipName == secondNewItem.ShipName).OrderId);
Assert.IsTrue(results.Count() == 1);
}
public void OrderIdLessThanOrEqualToFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Create second new item with the same unique shipping name
var secondNewItem = this.CreateNewItemInDb(newItem.ShipName);
// Filter by the larger orderId but also by the second unique
// column in this case shipping name
this.GridPage.Grid.Filter(
new GridFilter(
GridColumns.OrderID,
Enums.FilterOperator.LessThanOrEqualTo,
secondNewItem.OrderId.ToString()),
new GridFilter(
GridColumns.ShipName,
Enums.FilterOperator.EqualTo,
newItem.ShipName));
this.WaitForGridToLoadAtLeast(2, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.AreEqual(
newItem.OrderId,
results.FirstOrDefault(x => x.ShipName == newItem.ShipName).OrderId);
Assert.AreEqual(
secondNewItem.OrderId,
results.Last(x => x.ShipName == secondNewItem.ShipName).OrderId);
Assert.IsTrue(results.Count() == 2);
}
public void OrderIdLessThanFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Create second new item with the same unique shipping name
var secondNewItem = this.CreateNewItemInDb(newItem.ShipName);
// Filter by the larger orderId but also by the second unique
// column in this case shipping name
this.GridPage.Grid.Filter(
new GridFilter(
GridColumns.OrderID,
Enums.FilterOperator.LessThan,
secondNewItem.OrderId.ToString()),
new GridFilter(
GridColumns.ShipName,
Enums.FilterOperator.EqualTo,
secondNewItem.ShipName));
this.WaitForGridToLoadAtLeast(1, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.AreEqual(
newItem.OrderId,
results.FirstOrDefault(x => x.ShipName == newItem.ShipName).OrderId);
Assert.IsTrue(results.Count() == 1);
}
public void OrderIdNotEqualToFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Create second new item with the same unique shipping name
var secondNewItem = this.CreateNewItemInDb(newItem.ShipName);
// Filter by the larger orderId but also by the second unique
// column in this case shipping name
this.GridPage.Grid.Filter(
new GridFilter(
GridColumns.OrderID,
Enums.FilterOperator.NotEqualTo,
secondNewItem.OrderId.ToString()),
new GridFilter(
GridColumns.ShipName,
Enums.FilterOperator.EqualTo,
secondNewItem.ShipName));
this.WaitForGridToLoadAtLeast(1, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.AreEqual(
newItem.OrderId,
results.FirstOrDefault(x => x.ShipName == newItem.ShipName).OrderId);
Assert.IsTrue(results.Count() == 1);
}
public void OrderIdClearFilter()
{
this.GridPage.NavigateTo();
// Create new item with unique ship name;
var newItem = this.CreateNewItemInDb();
// Make sure that we have at least 2 items if the grid is empty.
// The tests are designed to run against empty DB.
this.CreateNewItemInDb(newItem.ShipName);
this.GridPage.Grid.Filter(
GridColumns.OrderID,
Enums.FilterOperator.EqualTo,
newItem.OrderId.ToString());
this.WaitForGridToLoad(1, this.GridPage.Grid);
this.GridPage.Grid.RemoveFilters();
this.WaitForGridToLoadAtLeast(1, this.GridPage.Grid);
var results = this.GridPage.Grid.GetItems<Order>();
Assert.IsTrue(results.Count() > 1);
}
}

If you recall the code of the examples from the previous articles, you will notice that the above code is almost identical . The main differences are that the helper methods are placed inside the base class and that the different methods are not marked as MSTest test methods. Also, the grid control is accessed through the IGridPage interface so that we can reuse the test cases for different grids. The asserters for the rest of the columns are similar to this example so I am not going to publish their code. If you are interested you can download the fully working examples at the end of the article.

Grid Column Asserters in Tests

Grid Control's Tests Setup

This is how looks the setup of the refactored version of the grid control's tests.

[TestClass]
public class KendoGridAdvanceReuseTacticsAutomationTests
{
private IWebDriver driver;
private IGridPage gridPage;
private FreightColumnAsserter freightColumnAsserter;
private OrderDateColumnAsserter orderDateColumnAsserter;
private OrderIdColumnAsserter orderIdColumnAsserter;
private ShipNameColumnAsserter shipNameColumnAsserter;
private GridPagerAsserter gridPagerAsserter;
[TestInitialize]
public void SetupTest()
{
this.driver = new FirefoxDriver();
this.driver.Manage().Timeouts().SetPageLoadTimeout(TimeSpan.FromSeconds(5));
this.gridPage = new GridFilterPage(this.driver);
this.freightColumnAsserter = new FreightColumnAsserter(this.gridPage);
this.orderDateColumnAsserter = new OrderDateColumnAsserter(this.gridPage);
this.orderIdColumnAsserter = new OrderIdColumnAsserter(this.gridPage);
this.shipNameColumnAsserter = new ShipNameColumnAsserter(this.gridPage);
this.gridPagerAsserter = new GridPagerAsserter(this.gridPage);
}
[TestCleanup]
public void TeardownTest()
{
this.driver.Quit();
}

You assign the concrete implementation of the concrete grid's page to the IGridPage interface variable. Then you initialize only the required grid columns' asserters. This means that if for example the ship name column is not present on this grid, you won't include its asserter. You can have different combinations of column asserters depending on the columns' combination in the currently tested grid.

Grid Control's Tests

#region OrderID Test Cases
[TestMethod]
public void OrderIdEqualToFilter()
{
this.orderIdColumnAsserter.OrderIdEqualToFilter();
}
[TestMethod]
public void OrderIdGreaterThanOrEqualToFilter()
{
this.orderIdColumnAsserter.OrderIdGreaterThanOrEqualToFilter();
}
[TestMethod]
public void OrderIdGreaterThanFilter()
{
this.orderIdColumnAsserter.OrderIdGreaterThanFilter();
}
[TestMethod]
public void OrderIdLessThanOrEqualToFilter()
{
this.orderIdColumnAsserter.OrderIdLessThanOrEqualToFilter();
}
[TestMethod]
public void OrderIdLessThanFilter()
{
this.orderIdColumnAsserter.OrderIdLessThanFilter();
}
[TestMethod]
public void OrderIdNotEqualToFilter()
{
this.orderIdColumnAsserter.OrderIdNotEqualToFilter();
}
[TestMethod]
public void OrderIdClearFilter()
{
this.orderIdColumnAsserter.OrderIdClearFilter();
}
#endregion

As you can see the usage of the column asserter is pretty simple. An additional benefit is the readability of the tests. Moreover, if you need to fix some of the test cases or refactor them, you can do it only in a single place, directly in the column asserter. This makes your code even more maintainable. You can find the rest of the examples in the full source code.