The Driver Pattern

June 23, 2023

We're going to take a look at a simple 'Driver Pattern' example. A simple example with a C# SpecFlow project. Think of this as a basic introduction before we take a look at a more complex, practical, example of the Driver Pattern with a Selenium Web Driver in SpecFlow.

The Driver Pattern with SpecFlow

I don't think of the The Driver Pattern in the context of say 'Gang of 4' classic design patterns. It doesn't really fall into any of the 23 patterns that are defined by the 'Gang of 4'. To be honest I find it more logical to think of it just as refactoring. Pulling out code, that might be duplicated, and making it available as it's own class, method, or whatever. Probably better thought of as a pattern in the context of a “reusable solution to a commonly occurring problem" in your software project.

The Problem We're Solving

The structure of our example simply consists of some feature files, step definitions and methods in a calculator application that we want to test.

SpecFlow Project with no driver pattern

The Feature Files describe the tests we want to complete and calls functions in our Step Definition files. Our Step Definitions interact with our application under test – our Calculator application.

Our calculator project provides 3 features:

1. adding

2. subtracting

3. calculating the square root

It's that Square Root feature that we want to focus on here. We need to write tests that pass the square root method a number and confirm that it returns the correct value.

In short….

We create a feature file that will contain thee BDD scenarios
We add the steps in that feature file that call step defintions
Our Step Defintions interact with our Calculator
– each step tests our sqrt feature in our calculator

As we walk through this example we'll see how our step definitions start to get quite complex and end up with a lot of logic. We'll see how the driver pattern is used to extract that logic from the steps. We'll remove that logic and place it in a driver class.

The Starting Point

Here's our simple Calculator class that we want to test:


using System;

namespace CalculatorSpecFlow
{
    public class Calculator
    {
        public int FirstNumber { get; set; }
        public int SecondNumber { get; set; }

        public int Add()
        {
            return FirstNumber + SecondNumber;
        }

        public int Sub()
        {
            return FirstNumber - SecondNumber;
        }

        public double SquareRoot(double number)
        {
            return Math.Sqrt(number);
        }
    }
}

And just for completeness you'd have the interfaced defined as follows…


internal interface ICar
{
    String GetCarModel();

    int GetTopSpeed();
}

What we want to do is check that this 'Math.Sqrt(number)' method returns the correct value.

In our SpecFlow project we define our Scenario


Scenario: Square root of a number
	Given the square root of the number 9 is taken
	Then the sqrt result should be the square root of 9

Then we write a couple of Step Definitions to cover these steps


namespace SpecFlowCalcV2.StepDefinitions
{
    [Binding]
    internal class CalculatorSqrtStepDefinitions
    {
        private readonly Calculator _calculator;
        private int _result;
        private CalcContext _calcContext;
        private double _sqrtResult;
        private double _expSqrtNumber;
  
        public CalculatorSqrtStepDefinitions(Calculator calculator, CalcContext calcContext)
        {
            this._calculator = calculator;
            this._calcContext = calcContext;
        }

        [Given("the square root of the number (.*) is taken")]
        public void WhenTheSquareRootOfTheNumberIsTaken(double number)
        {
            _sqrtResult = _calculator.SquareRoot(number);
            _calcContext.CurrentResult = _sqrtResult;
        }

        [Then("the sqrt result should be the square root of (.*)")]
        public void ThenTheSqrtResultShouldBe(int number)
        {
            //The Babylonian Method for Computing Square Roots
            double root = 1;
            int i = 0;
            while (true)
            {
                i = i + 1;
                root = (number / root + root) / 2;
                if (i == number + 1) { break; }
            }
            // Use result from calculation above
            _expSqrtNumber = root;
            _calcContext.CurrentResult.Should().Be(_expSqrtNumber);
        }
    }
}

What we have now is:

– our application where it calculates the 'actual result'

– our step definition that calculates our 'expected result'

We're calculating the “expected result" with the Babylonian square-root algorithm. Don't worry about this maths (I don't understand it either). In fact, not worrying about the maths behind this is EXACTLY what this whole driver pattern is about. It's about you not having to worry about how this works. We take it out .. put it somewhere else … and call it when we need it. We place this code in a driver and stop worrying about the nuts and bolts contained in the driver.

Where you find yourself shovelling excessive code, functionality and complexity in to the Step Definitions (or hooks)… start thinking 'driver pattern'. When your step definitions are…. 

1. NOT easy to read

2. NOT easy to maintain

3. NOT easy to reuse

Then we need to pull that functionality and complexity out into a driver.

Refactoring with the Driver Pattern

Let's see how we refactor this with the driver pattern then. We'll follow these simple steps:

1. for clarity we create a driver folder (this is actually created by default by SpecFlow)

2a. we create a calss that will contain our driver code (in this instance our sqrt calculation routine)

2b. we remove this unit of code from our step defintions

2c. we place this code in our driver class

3. we inject and then call the driver class from the Step Definition

What this will look like then is..

SpecFlow Project with driver pattern

1 – First we create our Driver folder. Actually this is usually created by SpecFlow when you first create a SpecFlow project.

SpecFlow Calculator Project Drivers


2 – Second we create this driver class for our step definition. Here we're pulling out the CalculateSquareRoot functionality from the Step Definition and putting it in our new CalcDriver class.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace SpecFlowProject2.Drivers
{
    public class CalcDriver
    {

        private double Number { get; set; }
        public double CurrentResult { get; set; }
        public string? CurrentMode { get; set; }

        public double CalculateSquareRoot(double Number)
        {
            //The Babylonian Method for Computing Square Roots
            double root = 1;
            int i = 0;
            while (true)
            {
                i = i + 1;
                root = (Number / root + root) / 2;
                if (i == Number + 1) { break; }
            }

            return root;
        }
    }
}

Note that in this class we've done a few things. We've created the method that calculates the square root (using the Babylonian method). And for good measure we have two public properties called CurrentMode and Current Result (that might contain the mode, scientific or basic, and result of the calculator at a later stage). What we have now is a driver class that contains

a. automation logic ( CalculateSquareRoot method)
b. data (CurrentResult property)
c. state (CurrentMode)

This is typical of the Driver Pattern. It's created as a class that can contain automation logic, data and state information that can be used during your scenario execution.

3 – Next up we modify our Step Definition as follows.


using CalculatorSpecFlow;
using BoDi;
using System.Diagnostics;
using SpecFlowProject2.Drivers;


namespace SpecFlowProject2.StepDefinitions
{
    [Binding]
    public sealed class CalculatorSqrtStepDefinitions
    {
        private readonly Calculator _calculator;
        private int _result;
        private CalcContext _calcContext;
        private double _sqrtResult;
        private double _expSqrtNumber;
        // property to hold our Calc Driver object
        private CalcDriver _calcDriver;  
        
        // context injection used in the constructor to get the Calc Driver
        public CalculatorSqrtStepDefinitions(Calculator calculator, CalcContext calcContext, CalcDriver calcDriver)
        {
            this._calculator = calculator;
            this._calcContext = calcContext;
            // assign our Calc Driver to our private local property
            this._calcDriver = calcDriver;
        }
        [Given("the square root of the number (.*) is taken")]
        public void WhenTheSquareRootOfTheNumberIsTaken(double number)
        {
            _sqrtResult = _calculator.SquareRoot(number);
            _calcContext.CurrentResult = _sqrtResult;
        }

        [Then("the sqrt result should be the square root of (.*)")]
        public void ThenTheSqrtResultShouldBe(int number)
        {
            // Use the Calc Driver to get the EXPECTED result
            _expSqrtNumber = _calcDriver.CalculateSquareRoot(number);

            Assert.Equal(_expSqrtNumber, _calcContext.CurrentResult);
        }
    }
}

The key bits to understand here are …

1. Using Context Injection in the constructor we create an instance of the CalcDriver

2. In the ThenTheSqrtResultShouldBe step we use the CalcDriver to get the 'expected' result

Conceptually I find it easier to think of this line of code as being the driver


    _calcDriver.CalculateSquareRoot(number);

This “drives" our 'automation code' that resides in it's own dedicated class. Possibly not a formally correct way to describe it but it helps me grasp the structure.

Have We Solved Our Problem?

Going back to those issues with our original code. They were that the code was:

1. NOT easy to read

2. NOT easy to maintain

3. NOT easy to reuse

I think you'll agree the following step definition is easier to read than the original step definition that contained all the square root calculation logic.


        [Then("the sqrt result should be the square root of (.*)")]
        public void ThenTheSqrtResultShouldBe(int number)
        {
            _expSqrtNumber = _calcDriver.CalculateSquareRoot(number);
            Assert.Equal(_expSqrtNumber, _calcContext.CurrentResult);
        }

Now with the CalcDriver class containing all the square root calculation logic it makes if far easier to maintain and reuse that code. If we want to update or modify the code we just have to edit our CalcDriver class. If we want to reuse the driver code we ‘just inject’ the CalcDriver in other step definition classes.

When I say “just inject" I’m talking about ‘Context Injection’. If that’s not a familiar topic then you’ll be pleased to know that this is what we’re covering next. And in another upcoming post we’ll look in more detail at the Driver Pattern with the Selenium Web Driver.