Testability, interfaces and unit tests for Dummies (and junior SDETs)
- Launching Selenium WebDriver in .NET Core the easy way
- Starting to build a (new) .net Core PageFactory
- Launching WebDrivers in .net Core
- Creating an internal WebDriverFactory
- Testability, interfaces and unit tests for Dummies (and junior SDETs)
- Preparing to inject test configuration – Refactoring to use a configuration object.
- Test Configuration with .NET Core and NUnit 3
- Applying a .runsettings file from the command line in .NET Core
- Dependency Injection the .NET Core way
Having got to version 2.0.0 of my WebDriverFactory with working system tests and CI builds it was time to extend things a little. This could be considered YAGNI (You Aren’t Gonna Need It) but I quite like to have a Manager class that deals with getting and closing down WebDriver instances. The idea is that the Manager knows the specifics of the driver that I want to use, and will return a given instance, or a new instance on request.
Testing this functionality could be performed with system tests, but as discussed last time, this is where unit tests can speed things up significantly. In this case I don’t care whether the WebDriver works, just that the Manager gets it when requested. Implementing a non functional FakeWebDriver, and a FakeWebDriverFactory to provide it should allow me to test only the Manager class in isolation
Step One: IWebDriverManager Interface and implementation
Design intention:
- The idea of the manager is that I define the configuration of WebDriver that I want to use in all tests, early in my test code. Typically this will b manually early in the Test class e.g. a [SetUp] method or automated in some kind of configuration module.
- Having done that I have very simple zero argument calls to Get() and Quit() the current WebDriver.
- I may also need an additional WebDriver instance in some tests, so also add a call for that.
IWebDriverManager interface
using OpenQA.Selenium; namespace AlexanderOnTest.NetCoreWebDriverFactory { public interface IWebDriverManager { /// <summary> /// Return a singleton WebDriver instance; /// </summary> /// <returns></returns> IWebDriver Get(); /// <summary> /// Quit and clear the current singleton WebDriver instance; /// </summary> IWebDriver Quit(); /// <summary> /// Return a new WebDriver instance independent of the singleton instance; /// </summary> /// <returns></returns> IWebDriver GetAdditionalWebDriver(); } }
Note that it is not providing a true singleton, but a reusable WebDriver instance that I can retrieve with a call to
myWebDriverManager.Get();
When I am finished I can dispose of it with:
myWebDriverManager.Quit();
and if I need a second WebDriver (e.g. to login to an administrator account):
myWebDriverManager.GetAdditionalWebDriver();
WebDriverManager implementation:
using System; using OpenQA.Selenium; namespace AlexanderOnTest.NetCoreWebDriverFactory { public class WebDriverManager : IWebDriverManager { private IWebDriver driver; private readonly Func<IWebDriver> webDriverConstructor; private WebDriverManager() { } public WebDriverManager( IWebDriverFactory factory, Browser browser, WindowSize windowSize = WindowSize.Hd, bool isLocal = true, PlatformType platformType = PlatformType.Any, bool headless = false) { this.webDriverConstructor = () => factory.GetWebDriver(browser, windowSize, isLocal, platformType, headless); } public IWebDriver Driver { get => driver ?? (driver = webDriverConstructor()); private set => driver = value; } public virtual IWebDriver Get() => Driver; public virtual IWebDriver Quit() { if (driver != null) { driver.Quit(); driver = null; } return driver; } public virtual IWebDriver GetAdditionalWebDriver() { return webDriverConstructor(); } } }
As you can see above, The majority of these classes are comments and documentation (the grey text) there is I think only one bit of code that I’m not happy with; the constructor:
public WebDriverManager( IWebDriverFactory factory, Browser browser, WindowSize windowSize = WindowSize.Hd, bool isLocal = true, PlatformType platformType = PlatformType.Any, bool headless = false) { this.webDriverConstructor = () => factory.GetWebDriver(browser, windowSize, isLocal, platformType, headless); }
I like that it defines and stores the function that creates a new WebDriver, but I dislike the fact that it requires six arguments. As five of them are the configuration of the driver that it will manage, I definitely see the need to create a single IConfiguration object to pass in: Something for next time. But first let’s get some tests running to ensure that it works as intended. This is especially important as I am already planning a refactoring!
Step Two: Adding Unit tests
If, like me, you start your programming career writing system tests, unit tests are probably something other people write. You probably have an idea about the unit tests written by your developers, you may even help review them, but you work solidly on testing completed systems.
If you are writing a framework however, you should be designing it from the ground up with testability in mind. A large part of that will be by using interfaces at any boundary that you may want to test against.
Interface: An interface in software is a contract for the methods that a given type of object MUST support
Of course you will already be familiar with at least one interface the WebDriver (Java) or IWebDriver (C#) that defines the calls that you can make to your controlled web browser instance, whatever browser it is controlling. (I tend to always use WebDriver as I started out seriously in Java!)
Interfaces and testability
Interfaces aid testability for two reasons:
- An interface defines exactly what this class does. It forces thought about what should be done either side of it and so encourages good design and separation of concerns.
- Critically it also means that for testing purposes we can replace our production classes surrounding it with Mock or Fake implementations that do nothing but test that the class under test doing what it should be.
Faking it
There are a number of frameworks to make it easy to generate fake objects for tests. In this case I am not going to be be too ambitious. The manager uses a WebDriverFactory to return a WebDriver instance.
The tests should be able to run with a real WebDriver, but switching to Fakes should allow them to complete far faster without having to wait for a real WebDriver instance to start up each time. Rather than listing the test cases separately, I hope the comments and documentation are enough to work out what’s going on in each test:
The only bit I think will need explaining is:
Action actOne = () => DriverOne.Manage().Window.FullScreen(); actOne.Should().ThrowExactly<WebDriverException>();
This is the simplest means I could think of to determine whether a WebDriver instance has been quit: In a real WebDriver Instance this call returns a WebDriverException for an instance that has been Quit().
FakeWebDriverFactory
all other methods: -> throw new NotImplementedException();
using System; using AlexanderOnTest.NetCoreWebDriverFactory; using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using OpenQA.Selenium.Edge; using OpenQA.Selenium.Firefox; using OpenQA.Selenium.IE; using OpenQA.Selenium.Safari; namespace AlexanderonTest.NetCoreWebDriverFactory.UnitTests.DriverManager { class FakeWebDriverFactory : IWebDriverFactory { ... ... ... ... public IWebDriver GetWebDriver(Browser browser, WindowSize windowSize = WindowSize.Hd, bool isLocal = true, PlatformType platformType = PlatformType.Any, bool headless = false) { return new FakeWebDriver(); } } }
FakeWebDriver
// <copyright> using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Text; using OpenQA.Selenium; namespace AlexanderonTest.NetCoreWebDriverFactory.UnitTests.DriverManager { internal class FakeWebDriver : IWebDriver { ... public void Quit() { isQuit = true; } public IOptions Manage() { // to simulate the behaviour of a quit WebDriver: a WebDriverException is thrown when a call is made to "driver.Manage().Window.FullScreen()" if (isQuit) { throw new WebDriverException(); } return null; }. ... public string Url { get; set; } public string Title { get; } public string PageSource { get; } public string CurrentWindowHandle { get; } public ReadOnlyCollection<string> WindowHandles { get; } } }
WebDriverManagerTests
Here is the test class. The highlighted lines 23-24 are where I can the calls for my fakes with fully functional instances to visibly see the effects with real WebDriver instances. In this case I use them to validate that my tests work (and fail) as expected, before swapping them to use the fakes.
using System; using AlexanderOnTest.NetCoreWebDriverFactory; using AlexanderOnTest.NetCoreWebDriverFactory.DriverOptionsFactory; using FluentAssertions; using NUnit.Framework; using OpenQA.Selenium; namespace AlexanderonTest.NetCoreWebDriverFactory.UnitTests.DriverManager.Tests { [Category("CI")] public class WebDriverManagerTests { private IWebDriver DriverOne { get; set; } private IWebDriver DriverTwo { get; set; } private IWebDriverFactory WebDriverFactory { get; set; } private IDriverOptionsFactory DriverOptionsFactory { get; set; } private IWebDriverManager WebDriverManager { get; set; } [OneTimeSetUp] public void Prepare() { DriverOptionsFactory = new DefaultDriverOptionsFactory(); WebDriverFactory = new FakeWebDriverFactory(); WebDriverManager = new WebDriverManager(WebDriverFactory, Browser.Firefox); } [Test] public void GetCreatesNewDriver() { DriverOne = WebDriverManager.Get(); DriverOne.Should().NotBeNull(); } [Test] public void GetReturnsTheSameDriver() { DriverOne = WebDriverManager.Get(); DriverTwo = WebDriverManager.Get(); DriverTwo.Should().BeSameAs(DriverOne); } [Test] public void GetAdditionalWebDriverReturnsAnotherInstance() { DriverOne = WebDriverManager.Get(); DriverTwo = WebDriverManager.GetAdditionalWebDriver(); DriverTwo.Should().NotBeSameAs(DriverOne); } [Test] public void GetAdditionalWebDriverDoesNotCreateSingleton() { DriverTwo = WebDriverManager.GetAdditionalWebDriver(); DriverOne = WebDriverManager.Get(); DriverTwo.Should().NotBeSameAs(DriverOne); } [Test] public void QuitHasClosedTheWebDriver() { DriverOne = WebDriverManager.Get(); WebDriverManager.Quit(); Action act = () => DriverOne.Manage().Window.FullScreen(); act.Should().ThrowExactly<WebDriverException>(); } [Test] public void AfterQuittingANewDriverIsCreated() { DriverOne = WebDriverManager.Get(); WebDriverManager.Quit(); DriverTwo = WebDriverManager.Get(); DriverTwo.Should().NotBeSameAs(DriverOne); } [Test] public void QuitOnlyQuitsTheSingletionWebDriver() { DriverOne = WebDriverManager.Get(); DriverTwo = WebDriverManager.GetAdditionalWebDriver(); WebDriverManager.Quit(); Action actOne = () => DriverOne.Manage().Window.FullScreen(); actOne.Should().ThrowExactly<WebDriverException>(); Action actTwo = () => DriverTwo.Manage().Window.FullScreen(); actTwo.Should().NotThrow<WebDriverException>(); } [TearDown] public void Teardown() { DriverOne = WebDriverManager.Quit(); DriverTwo?.Quit(); } } }
So how much difference does it make?
Running the test class using real WebDriver instances:
And running the test class using FakeWebDriver instances:
Look carefully at the units there…..
Real 52 seconds : Fake 25 milliseconds
That means that the fakes complete the test over 2000 times faster!!!!!
Progress made:
- Working IWebDriverManager interface and implementation
- FakeWebDriver and FakeWebDriverFactory instances
- Unit Tests written for the WebDriverManager implementation
- Speed comparison of System tests to Unit tests
Lessons learnt:
- 2000 times speed increase demonstrates the benefits of unit testing.
- Inability to ‘see’ what is happening shows a disadvantage of unit testing
A reminder:
If you want to ask me a question, Twitter is undoubtedly the fastest place to get a response: My Username is @AlexanderOnTest so I am easy to find. My DMs are always open for questions, and I publicise my new blog posts there too.
Next time:
I will be looking at configuration and dependency injection in .NET Core
[Edit 26/6/19 Moved into the new series: Launching WebDrivers in .NET Core the easy way]