• Background Image

    Creating A Selenium Testing Framework In C#

    April 25, 2018

April 25, 2018

Creating A Selenium Testing Framework In C#

In this post, we will take a look at how you can create a Selenium testing framework to simplify your tests using C#. Of course, the principles apply to other programming languages as well.

Why A Testing Framework?

Why should you even go through the hassle of creating your own testing framework? For 3 very good reasons:

  • It makes it easier for you to create and maintain tests
  • It makes your tests more readable and maintainable
  • It establishes an organisation wide guidelinde for creating Selenium tests

The Basic Component

The basic component of your testing framework should always be a Unit testing framework geared towards your programming language of choice. This gives us the ability to create, execute and report on all of our test cases out of the box. No need to reinvent the wheel here.

In this example, we will use Microsoft Test Framework for .Net but this is just an implementation detail.

A Basic Test

Let us take a look at a basic test example. In this example, we will open Chrome, navigate to a page, click a button and close the browser again.

namespace Selenium.Academy.Tests
{
    [TestClass]
    public class BasicExampleTest
    {
        [TestMethod]
        public void ClickTestExample()
        {
            IWebDriver driver = new ChromeDriver();

            driver.Navigate().GoToUrl("http://www.selenium.academy/Examples/Interaction.html");

            IWebElement button = driver.FindElement(By.Id("button"));

            button.Click();

            driver.Quit();
        }
    }
}

As you can see here we have a lot of boilerplate code that is repeated for every new test, like creating an instance of the browser, navigating and closing the browser again.

The Test Base Class

Wouldn’t it be nice if we could write all this boilerplate code only once? To do this let us create a base class that will open and close the browser for us after each test run automatically.

namespace Selenium.Academy.Framework
{
    [TestClass]
    public class SeleniumTest
    {
        protected IWebDriver driver;

        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void CreateDriver()
        {
            driver = new ChromeDriver();
        }

        [TestCleanup]
        public void QuitDriver()
        {
            if (driver != null)
                driver.Quit();
        }

    }
}

And of course we need to modify our test code as well:

namespace Selenium.Academy.Tests
{
    [TestClass]
    public class BasicExampleTest : SeleniumTest
    {
        [TestMethod]
        public void ClickTestExample()
        {
            driver.Navigate().GoToUrl("http://www.selenium.academy/Examples/Interaction.html");

            IWebElement button = driver.FindElement(By.Id("button"));

            button.Click();
        }
    }
}

Already we have less code in our test case which is great if you have hundreds of them later on.

The Page Attribute

We still have the duplicated navigation method in there. And maybe we want a way to quickly change the Url our tests use (to switch between local development and staging environment). For this we can add a custom attribute which we call PageAttribute to our framework.

namespace Selenium.Academy.Framework
{
    [AttributeUsage(validOn: AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class PageAttribute : Attribute
    {
        public string Url { get; set; }

    }
}

And of course, we need to modify our base class to actually use the attribute and navigate the browser as necessary.

namespace Selenium.Academy.Framework
{
    [TestClass]
    public class SeleniumTest
    {
        protected IWebDriver driver;

        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void CreateDriver()
        {
            driver = new ChromeDriver();

            PageAttribute page = (PageAttribute)GetCustomAttribute<PageAttribute>(TestContext.FullyQualifiedTestClassName, TestContext.TestName);

            if (page == null)
            {
                throw new InvalidOperationException("No Page attribute is specified. ");
            }

            driver.Navigate().GoToUrl(page.Url);
        }

        [TestCleanup]
        public void QuitDriver()
        {
            if (driver != null)
                driver.Quit();
        }

        private Attribute GetCustomAttribute<T>(string className, string testName)
        {
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                var currentType = assembly.GetTypes().FirstOrDefault(f => f.FullName == className);

                if (currentType == null)
                    continue;

                Attribute classAttribute = currentType.GetCustomAttribute(typeof(T));
                if (classAttribute != null)
                {
                    return classAttribute;
                }

                var currentMethod = currentType.GetMethod(testName);

                Attribute methodAttribute = currentMethod.GetCustomAttribute(typeof(T));
                if (methodAttribute != null)
                {
                    return methodAttribute;
                }
            }

            return null;
        }
    }
}

Now the driver will automatically navigate to the URL we give the test case as an attribute. You can use the attribute either on the class (to apply for all test cases) or to an individual method. This can look something like this:

namespace Selenium.Academy.Tests
{
    [TestClass]
    public class BasicExampleTest : SeleniumTest
    {
        [TestMethod]
        [Page(Url = "http://www.selenium.academy/Examples/Interaction.html")]
        public void ClickTestExample()
        {
            IWebElement button = driver.FindElement(By.Id("button"));

            button.Click();
        }
    }
}

Less code again, maybe we are onto something here after all.

The Browser Attribute

You might have noticed that our test case is still only executed in Google Chrome. Maybe we should add a second attribute to specify this for every test case.

namespace Selenium.Academy.Framework
{
    [AttributeUsage(validOn: AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false)]
    public class BrowserAttribute : Attribute
    {
        public string Browser { get; set; }
        public string Version { get; set; }
        public bool IsRemote { get; set; }
        public string Url { get; set; }
    }
}

And again we need to change our base class so that it will pick the correct browser based on our attribute. You will notice that the IsRemote property tells our base class to execute the test against a remote Selenium Grid instead of the local browser.

Modified our base class will look like this:

namespace Selenium.Academy.Framework
{
    [TestClass]
    public class SeleniumTest
    {
        protected IWebDriver driver;

        public TestContext TestContext { get; set; }

        [TestInitialize]
        public void CreateDriver()
        {
            BrowserAttribute browser = (BrowserAttribute)GetCustomAttribute<BrowserAttribute>(TestContext.FullyQualifiedTestClassName, TestContext.TestName);

            if (browser == null)
            {
                throw new InvalidOperationException("No Browser attribute is specified. ");
            }

            if (browser.IsRemote)
            {
                ICapabilities capabilities = new DesiredCapabilities(browser.Browser, browser.Version, Platform.CurrentPlatform);

                Uri uri = new Uri(browser.Url);

                driver = new RemoteWebDriver(uri, capabilities);
            }
            else
            {
                switch (browser.Browser)
                {
                    case "Chrome":
                        driver = new ChromeDriver();
                        break;
                    case "Firefox":
                        driver = new FirefoxDriver();
                        break;
                    case "IE":
                        driver = new InternetExplorerDriver();
                        break;
                    case "Edge":
                        driver = new EdgeDriver();
                        break;
                    default:
                        throw new InvalidOperationException("Browser " + browser.Browser + " not found!");
                }
            }

            PageAttribute page = (PageAttribute)GetCustomAttribute<PageAttribute>(TestContext.FullyQualifiedTestClassName, TestContext.TestName);

            if (page == null)
            {
                throw new InvalidOperationException("No Page attribute is specified. ");
            }

            driver.Navigate().GoToUrl(page.Url);
        }

        [TestCleanup]
        public void QuitDriver()
        {
            if (driver != null)
                driver.Quit();
        }

        private Attribute GetCustomAttribute<T>(string className, string testName)
        {
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                var currentType = assembly.GetTypes().FirstOrDefault(f => f.FullName == className);

                if (currentType == null)
                    continue;

                Attribute classAttribute = currentType.GetCustomAttribute(typeof(T));
                if (classAttribute != null)
                {
                    return classAttribute;
                }

                var currentMethod = currentType.GetMethod(testName);

                Attribute methodAttribute = currentMethod.GetCustomAttribute(typeof(T));
                if (methodAttribute != null)
                {
                    return methodAttribute;
                }
            }

            return null;
        }

    }
}

And if we add the attribute (on the class or method level) to our test case we get this:

namespace Selenium.Academy.Tests
{
    [TestClass]
    public class BasicExampleTest : SeleniumTest
    {
        [TestMethod]
        [Browser(Browser = "Firefox")]
        [Page(Url = "http://www.selenium.academy/Examples/Interaction.html")]
        public void ClickTestExample()
        {
            IWebElement button = driver.FindElement(By.Id("button"));

            button.Click();
        }
    }
}

Summary

With just a few easy steps we managed to make our test example from the beginning much more readable and easier to understand. This way you can create tests much faster and understand tests created by your colleges more easily.

Feel free to download the code and try it out yourself.

Want to know more about Selenium? Check out the full course with 30+ lessons and screencasts over at http://selenium.academy.