DEV Community

mhossen
mhossen

Posted on

Leveraging Interfaces and Dependency Injection for Flexible Code: A Practical Specflow Example

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.

public interface ILoginPage
{
  void NavigateToLoginPage();
  void Login(string username, string password);
}
Enter fullscreen mode Exit fullscreen mode

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.

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

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

  protected StepBase(IObjectContainer container)
  {
    Container = container;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

[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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)