Leveraging Interfaces and Dependency Injection for Flexible Code: A Practical Specflow Example
mhossen
Posted on June 3, 2023
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);
}
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();
}
}
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);
});
}
}
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);
}
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;
}
}
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();
}
}
Usage and Benefits
To use the code sample, follow these steps:
- Define your own classes implementing the
ILoginPage
interface, representing different login page implementations. - Update the
RegisterTypes<BasePage>()
call in theBrowserSetup()
method of theHooks
class to include your custom page implementations. - Use the
LoginSteps
class to define step definitions for your BDD scenarios, injecting theILoginPage
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
Posted on June 3, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.