Leveraging Interfaces and Dependency Injection for Flexible Code: A Practical Specflow Example
Tags: specflow, csharp, automation, testing
Published: 2023-06-03 14:20:58

Introduction:
Interfaces and dependency injection (DI) are powerful tools in software development that promote flexibility, testability, and maintainability. In this blog post, we'll explore a practical example that demonstrates how interfaces and DI can be used effectively. We'll analyze the code, explain its usage, and highlight the benefits it brings to the project.

## Understanding the Code

Let's dive into the provided code snippet, which showcases the usage of interfaces and DI in a Selenium-based login page scenario. The code consists of several classes: `ILoginPage`, `LoginPage`, `ContainerExtension`, `LoginSteps`, `StepBase`, and `Hooks`. We'll discuss each component and its role in the overall implementation.

### `ILoginPage` Interface

The `ILoginPage` interface defines the contract for a login page. It declares two methods: `NavigateToLoginPage()` and `Login(string username, string password)`. This interface establishes the required behavior for any login page implementation.

```csharp
public interface ILoginPage
{
  void NavigateToLoginPage();
  void Login(string username, string password);
}
```

### `LoginPage` Class

The `LoginPage` class implements the `ILoginPage` interface and represents the actual login page. It extends a `BasePage` class and includes private properties that represent various elements on the page, such as the username input field, password input field, and login button. The class also provides implementations for the interface methods.

The `LoginPage` constructor accepts an `IWebDriver` parameter, which is passed from the DI container during object creation. This approach enables loose coupling between the `LoginPage` and the `IWebDriver` implementation, making the class more flexible and easier to test.

```csharp
internal class LoginPage : BasePage, ILoginPage
{
  private IWebElement UserName => Driver.FindElement(By.Id("username"));
  private IWebElement Password => Driver.FindElement(By.Id("password"));
  private IWebElement LoginButton => Driver.FindElement(By.CssSelector("input[value='Login']"));

  public LoginPage(IWebDriver driver) : base(driver)
  {
  }

  public void NavigateToLoginPage()
  {
    var location = typeof(LoginPage).Assembly.Location;
    var parent = Directory.GetParent(location);
    Driver.Navigate().GoToUrl(@$"{parent}\Sample\login.html");
  }
  
  public void Login(string username, string password)
  {
    UserName.SendKeys(username);
    Password.SendKeys(password);
    LoginButton.Click();
  }
}
```

### `ContainerExtension` Class

The `ContainerExtension` class extends the functionality of an `IObjectContainer` (a DI container implementation) by introducing a method named `RegisterTypes<TBase>()`. This method utilizes reflection to scan the assembly for all types derived from `TBase` (in this case, `BasePage`).

For each derived type found, the code checks if there is an interface named `I{derivedType.Name}` (e.g., `ILoginPage` for `LoginPage`). If a matching interface is found, an instance of the derived type is created using the provided arguments and registered with the DI container using the interface as the registration key.

```csharp
public static class ContainerExtension
{
  public static void RegisterTypes<TBase>(this IObjectContainer container, params object[] args) where TBase : class
  {
    var typeOfBase = typeof(TBase);

    var derivedTypes = typeOfBase.Assembly.GetTypes()
      .Where(t => typeOfBase.IsAssignableFrom(t) && t is {IsClass: true, IsAbstract: false});

    Parallel.ForEach(derivedTypes, derivedType =>
    {
      var @interface = derivedType.GetInterfaces().FirstOrDefault

(i => i.Name == $"I{derivedType.Name}");
      if (@interface == null) return;

      var obj = Activator.CreateInstance(derivedType, args);
      container.RegisterInstanceAs(obj, @interface);
    });
  }
}
```

### `LoginSteps` Class

The `LoginSteps` class represents a set of steps defined for a specific behavior-driven development (BDD) framework, such as SpecFlow. It includes a constructor that accepts an `IObjectContainer` and an `ILoginPage` instance, which are resolved through DI.

These steps correspond to scenarios defined in the BDD feature files. For example, the `GivenINavigateToLoginPage()` step calls the `NavigateToLoginPage()` method on the injected `ILoginPage` instance.

```csharp
private readonly ILoginPage _loginPage;

public LoginSteps(IObjectContainer container, ILoginPage loginPage) : base(container)
{
  _loginPage = loginPage;
}

[Given(@"I navigate to login page")]
public void GivenINavigateToLoginPage()
{
  _loginPage.NavigateToLoginPage();
}

[Given(@"I provide '(.*)' and '(.*)'")]
public void GivenIProvide(string username, string password)
{
  _loginPage.Login(username, password);
}
```

### `StepBase` Class

The `StepBase` class serves as a base class for step definitions in a BDD framework. It includes a constructor that accepts an `IObjectContainer`, allowing derived classes to access the DI container.

```csharp
public abstract class StepBase : TechTalk.SpecFlow.Steps
{
  protected readonly IObjectContainer Container;

  protected StepBase(IObjectContainer container)
  {
    Container = container;
  }
}
```

### `Hooks` Class

The `Hooks` class includes methods decorated with SpecFlow attributes, such as `[Before]` and `[After]`. These methods are executed before and after each scenario, providing setup and teardown functionality.

In the `BrowserSetup()` method, a new instance of the Selenium `IWebDriver` is created, and it is registered with the DI container using `RegisterInstanceAs()`. Additionally, `RegisterTypes<BasePage>()` is called on the DI container, which scans the assembly for all types derived from `BasePage` and registers them as instances with their corresponding interfaces.

The `BrowserTearDown()` method disposes of the `IWebDriver` instance retrieved from the DI container.

```csharp
[Binding]
public class Hooks
{
  private readonly IObjectContainer _container;

  protected Hooks(IObjectContainer container)
  {
    _container = container;
  }

  [Before(Order = 0)]
  public void BrowserSetup()
  {
    var driver = Driver.CreateBrowserSession();
    _container.RegisterInstanceAs(driver);
    _container.RegisterTypes<BasePage>(_container.Resolve<IWebDriver>());
  }

  [After(Order = 99)]
  public void BrowserTearDown()
  {
    _container.Resolve<IWebDriver>().Dispose();
  }
}
```

## Usage and Benefits

To use the code sample, follow these steps:

1. Define your own classes implementing the `ILoginPage` interface, representing different login page implementations.
2. Update the `RegisterTypes<BasePage>()` call in the `BrowserSetup()` method of the `Hooks` class to include your custom page implementations.
3. Use the `LoginSteps` class to define step definitions for your BDD scenarios, injecting the `ILoginPage` instance through the constructor.

By leveraging interfaces and DI in this manner, you gain the following benefits:

- **Flexibility**: Interfaces allow you to switch between different login page implementations without modifying the consuming code.
- **Testability**: DI enables easy injection of mock objects for

 testing purposes, facilitating unit testing of classes dependent on the `ILoginPage` interface.
- **Code Reusability**: The `LoginSteps` class serves as a reusable component for defining login-related steps in multiple scenarios, promoting code reuse and reducing duplication.
- **Loose Coupling**: DI decouples classes from specific implementation details, making it easier to replace dependencies and promoting modular, decoupled code.

By incorporating interfaces and DI into your projects, you can achieve cleaner, more modular code that is easier to maintain, test, and extend. The code provided in this blog post demonstrates practical usage and highlights the benefits of these concepts.

Conclusion

Interfaces and dependency injection are powerful tools that enhance code flexibility, testability, and maintainability. By leveraging interfaces to define contracts and DI to inject dependencies, you can write cleaner, modular code that is easier to maintain and test.

The code example showcased the usage of interfaces to define the contract for a login page and how DI can be employed to inject the `ILoginPage` instance into the `LoginSteps` class. Additionally, the code demonstrated how the `ContainerExtension` class extends the DI container's functionality to register implementations based on interfaces.

By adopting these practices in your own projects, you can improve code quality, support future scalability, and promote code reuse.

GitHub Interface Example: [selenium_specflow_di_interface_example](https://github.com/mhossen/SpecflowDependencyDemo/tree/selenium_specflow_di_interface_example)