Wednesday, December 28, 2011

31 Days of Testing—Day 21: Data Driving Your Functional Tests

Index to all posts in this series is here!

This post shouldn’t be confused with Seth’s awesome post he lent me on Rules for Effective Data-Driven Tests. He was talking about testing interactions with the database. This post will show you how to push sets of data through a functional test.

This post’s examples are in C# with Selenium. If you’d like to see how the same test rolls in Test Studio, please go check out the short video I recorded on Data Driving Dynamically Loaded Elements for Telerik TV.

I’m going to refer back to the post I wrote on day 13: Functional Test 201 (Common Problems), specifically, case 3 where the elements you are working with are loaded up in the DOM, but have no content. That example used the ASP.NET AJAX Cascading Drop Down site. Feel free to go explore the original post and AJAX site if you need to refresh yourselves. I’ll wait.

The example site I used offers up three option lists (make, model, color) and gives you a matrix of combinations to deal with. Writing up separate test scripts for each combination would be insanity, so let’s not. Instead, we’ll create a list of items to pass through one test, and iterate that test repeatedly through the list.

First off, I’ve refactored the original example from day 13 to make it more modular and readable. Day 13 worked for an elementary example, but let’s move on to something more real-world-ish. Here’s the crux of the new test:

 Test: Working_with_no_content_data_driven
   1: [Test]
   2: public void Working_with_no_content_data_driven()
   3: {
   4:     IList<Car> cars = CarFactory.Return_three_valid_cars();
   5:  
   6:     browser.Navigate().GoToUrl(
   7:         "http://www.asp.net/ajaxLibrary/AjaxControlToolkitSampleSite/CascadingDropDown/CascadingDropDown.aspx");
   8:     WebDriverWait wait = new WebDriverWait(browser, TimeSpan.FromSeconds(10));
   9:  
  10:     foreach (Car car in cars)
  11:     {
  12:         browser.Navigate().Refresh();
  13:     
  14:         Select_make(car, wait);
  15:         Select_model(car, wait);
  16:         Select_color(car, wait);
  17:         Validate_message(car, wait);
  18:     }
  19: }

First I’m using a factory to build a list of three cars. Here’s what that looks like:

 Class: CarFactory
   1: public static class CarFactory
   2: {
   3:     public static IList<Car> Return_three_valid_cars()
   4:     {
   5:         return new List<Car>
   6:         {
   7:             new Car { Make = "Acura", Model = "Integra", Color = "Sea Green",
   8:                 Message = "Sea Green Acura Integra" },
   9:             new Car { Make = "Audi", Model = "S4", Color = "Metallic", 
  10:                 Message = "Metallic Audi S4" },
  11:             new Car { Make = "BMW", Model = "7 series", Color = "Brown", 
  12:                 Message = "Brown BMW 7 series" }
  13:         };
  14:     }
  15: }

Here I’m simply creating a list of Cars right in the method. This factory method could just as easily reach out to a database, read from an Excel file, etc., etc. The point being, the test itself has no idea what the data source is—and that’s exactly how it should be! Hiding the data source in the Factory lets me change the source as needed without impacting any of the tests which rely on that Factory.

Here’s the inspiring Car class:

 Class: Car
   1: public class Car
   2: {
   3:     public string Make { get; set; }
   4:     public string Model { get; set; }
   5:     public string Color { get; set; }
   6:     public string Message { get; set; }
   7: }

Back to the actual test now!

Lines 6-8 navigate to the site and set up our wait, quite similarly to day 13’s example.

Lines 10-18 let us iterate through our list of Cars. If you’re working in Python, Java, Ruby, or some other platform then obviously things will look different. The idea is we loop through our cars and run the same test each time.

Note that line 12 explicitly refreshes the browser each time through. Because we’re pulling data back from service calls, I’ve found the page DOM can hold old contents around. Refreshing each iteration ensures I have exactly the DOM I expect to work with.

Line 14, Select_make() calls a newly extracted method to interact with the page’s drop down for Make.

   1: private void Select_make(Car car, WebDriverWait wait)
   2: {
   3:     var listOfMakes = browser.FindElement(By.Id("ctl00_SampleContent_DropDownList1"));
   4:     wait.Until<IWebElement>((d) =>
   5:     {
   6:         return d.FindElement(By.XPath(
   7:                                       "id('ctl00_SampleContent_DropDownList1')/option[text()='" +
   8:                                       car.Make + "']"));
   9:     });
  10:     var makeOptions = new SelectElement(listOfMakes);
  11:     makeOptions.SelectByText(car.Make);
  12: }

This works exactly the same as described in the previous post: get the drop down list, wait until its contents populate with the desired make for this iteration. I’m passing in the Car class here, which as I’m writing this makes me realize I should instead be passing in only the Make property from the Car, not the entire Car. Select_make shouldn’t have to know how to deal with a Car, only with what it expects to.

Enough ponderings on software design for now. I’ll refactor later.

The calls to Select_model() and Select_color() work in exactly the same fashion:

   1: private void Select_model(Car car, WebDriverWait wait)
   2: {
   3:     var listOfModels = browser.FindElement(By.Id("ctl00_SampleContent_DropDownList2"));
   4:     wait.Until<IWebElement>((d) =>
   5:     {
   6:         return d.FindElement(By.XPath(
   7:                                       "id('ctl00_SampleContent_DropDownList2')/option[text()='" +
   8:                                       car.Model + "']"));
   9:     });
  10:     var modelOptions = new SelectElement(listOfModels);
  11:     modelOptions.SelectByText(car.Model);
  12: }
  13:  
  14: private void Select_color(Car car, WebDriverWait wait)
  15: {
  16:     var listOfColors = browser.FindElement(By.Id("ctl00_SampleContent_DropDownList3"));
  17:     wait.Until<IWebElement>((d) =>
  18:     {
  19:         return d.FindElement(By.XPath(
  20:                                       "id('ctl00_SampleContent_DropDownList3')/option[text()='" +
  21:                                       car.Color + "']"));
  22:     });
  23:     var colorOptions = new SelectElement(listOfColors);
  24:     colorOptions.SelectByText(car.Color);
  25: }

Now for the validation which checks that the expected message is correctly displayed:

   1: private void Validate_message(Car car, WebDriverWait wait)
   2: {
   3:     var messageActual = wait.Until<IWebElement>((d) =>
   4:     {
   5:         return d.FindElement(By.XPath(
   6:                                       "id('ctl00_SampleContent_Label1')[contains(.,'" +
   7:                                       car.Message + "')]"));
   8:     });
   9:     Assert.IsTrue(messageActual.Text.Contains(car.Message), "Message: " +
  10:                                                             messageActual.Text);
  11: }

The XPath in this method uses the “contains” function to check contents under the element pointed to by id ct100_SampleContent_Label1. I don’t check for an exact match—I only want to check that the message property’s contents for the current car are somewhere in that element.

There you have it: a simple data driven example for your functional test. Key takeaway: Construct your actual data list behind some sort of façade, be it a Factory or some other equivalent. Never let your tests themselves be responsible for constructing the data list. This ensures you’ll always have the easy flexibility to change how the data is built, where it’s built from, what it looks like, etc.

I should also point out that, as with all my examples, I’m not making use of the Page Object pattern. My examples here are all very linear with locators defined right in the tests. Why am I not using Page Objects for you to read? Two reasons. First, the examples are long enough and I’m trying to keep things fairly simple. Secondly, quite frankly I’ve not worked with it enough to be confident in showing you proper examples. Go do your own research on it, get to know it, and decide if it’s a sensible path for you to follow.

If you’re interested, you can find the complete source for this example (and the ones from Day 13) in my GitHub repository.

1 comment:

Adam Goucher said...

And from there you should get the data from outside the code; such as reaching into the database just a raw text box. This is super useful for users and authentication (if you have a way of resetting user passwords in your automation environment to a known values. In this example, that uses selects it seems, I would just data drive it from the values in the browser themselves.

Some pseudo-ish code
how_many_options = driver.find_elements(By.id, "my locator)
which_option = random.randint(0, how_many_options)
how_many_options[which_option].click()

Subscribe (RSS)

The Leadership Journey