SpecDriver: A simple, open-source, page object model framework for C# automated web testing

There doesn’t seem to be a lot of material available in the C# .NET automated testing space, so I thought I would create and share my own page object model centered framework: SpecDriver.

It uses SpecFlow to define features, scenarios and steps, and then WebDriver to actually drive the browser to automate these steps, with a page object model in between to ensure maximum maintainability of the solution.

I have previously documented the steps to getting SpecFlow up and running with Visual Studio C# Express which is free to use for both commercial and non-commercial reasons. You should follow these if you would like to play around with SpecDriver.

You can access all the source code on the github repository, and please feel free to fork/improve it as you see fit.

I will explain the various elements here and how they fit together.

Feature Files for Test Specifications

SpecFlow uses .feature files exactly the same as Cucumber, so it’s pretty easy to create these. I actually used my feature files from my Cucumber framework for this example; the benefits of writing these in a non-technical style!

Feature: Google Search
  As a casual internet user
  I want to find some information about watir, and do a quick conversion
  So that I can be knowledgeable being

Scenario: Search for Watir
  Given I am on the Google Home Page
  When I search for "Watir"
  Then I should see at least 100,000 results

Scenario: Do a unit conversion
  Given I am on the Google Home Page
  When I convert 10 cm to inches
  Then I should see the conversion result "10 centimetres = 3.93700787 inches"

Scenario: Do a search using data specified externally
  Given I am on the Google Home Page
  When I search for a ridiculously small number of results
  Then I should see at most 100 results

Step Definitions that call page objects

The step definitions are small, granular methods that call methods on page objects and do assertions against expected results.

namespace Project1.StepDefinitions
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using TechTalk.SpecFlow;
    using NUnit.Framework;
    using Project1.Pages;

    [Binding]
    public class GoogleSearchStepDefinitions : BaseStepDefinitions
    {

        [Given(@"I am on the Google Home Page")]
        public void GivenIAmOnTheGoogleHomePage()
        {
            GoogleHomePage = new GoogleHomePageModel(Driver);
            Assert.AreEqual("Google", GoogleHomePage.Title);
        }

        [When(@"I search for ""(.+)""")]
        public void WhenISearchForSomething(string searchTerm)
        {
            GoogleSearchResultsPage = GoogleHomePage.Search(searchTerm);
        }

        [When(@"I search for a ridiculously small number of results")]
        public void WhenISearchForARidiculouslySmallNumberOfResults()
        {
            GoogleSearchResultsPage = GoogleHomePage.Search("macrocryoglobulinemia marvel");
        }

        [When(@"I convert (.+)")]
        public void WhenIConvertSomething(string ConversionString)
        {
            GoogleSearchResultsPage = GoogleHomePage.Search("convert " + ConversionString);
        }

        [Then(@"I should see at most ([\d,]+) results")]
        public void ThenIShouldSeeAtMostNumberOfResults(string expMaxNumberResults)
        {
            expMaxNumberResults = expMaxNumberResults.Replace(",", "");
            Assert.LessOrEqual(Convert.ToInt32(GoogleSearchResultsPage.NumberOfResults), Convert.ToInt32(expMaxNumberResults));
        }

        [Then(@"I should see at least ([\d,]+) results")]
        public void ThenIShouldSeeAtLeastNumberOfResults(string expMinNumberResults)
        {
            expMinNumberResults = expMinNumberResults.Replace(",", "");
            Assert.GreaterOrEqual(Convert.ToInt32(GoogleSearchResultsPage.NumberOfResults), Convert.ToInt32(expMinNumberResults));
        }

        [Then(@"I should see the conversion result ""(.+)""")]
        public void ThenIShouldSeeTheConversionResult(string expectedConversionResult)
        {
            Assert.AreEqual(expectedConversionResult, GoogleSearchResultsPage.ConversionResult);
        }
    }
}

Page Object Model

Each page in the application under test is represented by a page class (that inherits from a base page class), and this page class has elements and methods associated with it. The pages are the things that actually use WebDriver to interact with browsers. You can see, like my ruby page object pattern, methods that change pages return an instance of that new page. The other thing to note is that there is a base page class that has a constructor, that requires an known element to instantiate the page. This is a way of knowing where you are in your application and constantly checking it is in the right place. This also ensures consistent syncronization, especially when pages contain dynamic content such as AJAX calls.

namespace Project1.Pages
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using OpenQA.Selenium;

    public class GoogleHomePageModel : BasePageModel
    {
        private static readonly By SearchEditSelector = By.Name("q");
        private static readonly By SearchButtonSelector = By.Name("btnG");

        public GoogleHomePageModel(IWebDriver driver)
            : base(driver, SearchEditSelector)
        {
        }

        private IWebElement SearchEditElement
        {
             get { return Driver.FindElement(SearchEditSelector); }
        }

        private IWebElement SearchButtonElement
        {
            get { return Driver.FindElement(SearchButtonSelector); }
        }

        public GoogleSearchResultsPageModel Search(string term)
        {
            this.SearchEditElement.Set(term);
            this.SearchEditElement.SendKeys(Keys.Escape);
            this.SearchButtonElement.Click();
            return new GoogleSearchResultsPageModel(Driver);
        }
    }
}

Putting it all together

I have two batch files that I use in Visual Studio 2010 C# Express, one to generate the SpecFlow NUnit tests, the other to execute them and provide a visual report. I add these as external tools in VS2010 and run them from the menu. Neat.

17 thoughts on “SpecDriver: A simple, open-source, page object model framework for C# automated web testing

  1. Since you based SpecDriver on an inheritance model, I was just wondering if you’d run into any Liskov Substitution violation weirdness.

    Thanks for the link in the comment above. Guess it is time to RT(SpecFlow)M.

  2. Out of curiosity, why, if you know AfterTestRun isn’t being executed, didn’t you decorate StartWebDriver with BeforeScenario and put the call to IWebDriver.Close in the method decorated with AfterScenario?

    • I like opening and closing the browser as least as possible.
      Therefore, my preference
      1) before and after test run -> doesn’t work
      2) before and after each feature file -> currently using
      3) before and after each scenario -> too often for my liking

  3. I know you’re using NUnit, we’re using MSTest…. but doesn’t NUnit have an attribute similar to [AssemblyInitialize]? We have a special method here that fires up all browsers and makes them available via a static context object. A similar method using [AssemblyCleanup] attribute goes through and quits them all at the end of the test run.

    • Thanks, it probably does.
      I just prefer to use the SpecFlow events are they are tool agnostic.
      I believe they’ve all been fixed in SpecFlow 1.8 anyway.

  4. I don’t know if you are still monitoring this. If so, I have 2 questions. I am new to Visual Studio , Specflow and the rest so I apologize in advance if I should know these. What is the GoogleSearch.feature that has the “balloon” icon? When I try to build the project after gathering everything else together from GitHub, I get an error that highlights the word Feature: in the GoogleSearch.feature.cs. The error is “A namespace cannot directly contan members such as fields or methods” Any suggestions?

  5. Alister, great write up! First time setting up selenium test framework, this source has been a great start point. Thanks.

  6. Hi Alister,

    Many thank for this post. I am planning to use this model in my current project. I have a question. I am planning to use mbUnit with Selenium Gird as nUnit doesn’t support parallel execution of tests. I need to execute tests in parallel for all supported browsers (ie, ff and chrome) in scope. I have been combing the web to find a solution. The only working solution I found was to compile dll per browser. Is there a way for me to achieve this by using only one dll ? Can you pls share your thoughts ? Can we add some code at assembly or feature level so that my tests will send requests to hub in parallel for each browser (provided I have nodes with different browsers i scope )

  7. Thanks @Alister. This blog has good information, I did used it in my project & successfully able to create Page object model framework in Speflow using Nunit.

Comments are closed.