Search code examples
c#seleniumautomationinstantiationspecflow

Unnecessary class instantiation repetition


Currently working on building an automation framework using C# and SpecFlow. One of my gripes at the moment is the amount of times I am instantiating a class to access the methods or web elements.

Below is part of the class I instantiate in a separate class method

public class UniversalSelectors : BasePage
{
    public  UniversalSelectors(IObjectContainer container): base(container)  { }

    //iFrame
    public IWebElement iFrame => Driver.FindElement(By.Id("content"));
    //Nav Bar Elements
    public void ClickAdministration()
    {
        Driver.FindElement(By.XPath("//a[@id='administration-menu-item']"));
    }
    public void ClickDevicesMenu()
    {
        Driver.FindElement(By.XPath("//*[@id='manage-devices-menu-item']"));
    }
    public void ClickRemoteMonitoring()
    {
        Driver.FindElement(By.Id("manage-monitoring-menu-item"));
    }
    public void ClickSystemUsers()
    {
        Driver.FindElement(By.XPath("//*[@id='manage-users-menu-header']"));
    }
    //Quick Search Elements
    public IWebElement Quick_Search => Driver.FindElement(By.XPath("//*[@class='pSearch pButton']"));
    public IWebElement Quick_Search_Box => Driver.FindElement(By.XPath("//*[@class='qsbox']"));
    public IWebElement Quick_Search_Button => Driver.FindElement(By.XPath("//*[@class='btn btn-sm btn-success flexigrid-search-button']"));
    public IWebElement Quick_Clear_Button => Driver.FindElement(By.XPath("//*[@class='btn btn-sm btn-success flexigrid-clear-button']"));

And below you can see how i'm instantiating in each method of a different class. But what I want to know is, is this necessary or is there a better way around it?

public void NavigateToRoles()
{
    var universalselectors = new UniversalSelectors(_container);

    universalselectors.ClickAdministration();
    Thread.Sleep(1000);
    universalselectors.ClickSystemUsers();
    Thread.Sleep(1000);
    Roles.Click();
    Thread.Sleep(2000);
}
public void CreateBlankRole()
{
    var universalselectors = new UniversalSelectors(_container);

    Driver.SwitchTo().DefaultContent();
    Driver.SwitchTo().Frame(universalselectors.iFrame);
    universalselectors.ClickNewRecord();
    Thread.Sleep(2000);
    universalselectors.ClickSaveRecord();
    Thread.Sleep(2000);
}

Solution

  • You can eliminate initializing a new object of the same type using dependency injection (see also constructor injection). Within the confines of SpecFlow, this involves creating a new instance of UniversalSelectors in a SpecFlow hook and register it with the SpecFlow object container. Next, any step definition class that needs a page model should include a UniversalSelectors object as one of its constructor arguments, which it passes on to the page model's constructor along with the web driver object.

    Registering the Web Driver and UniversalSelector object with the SpecFlow dependency injection framework (object container):

    [Binding]
    public class SpecFlowHooks
    {
        private readonly IObjectContainer container;
    
        public SpecFlowHooks(IObjectContainer container)
        {
            this.container = container;
        }
    
        [BeforeScenario]
        public void BeforeScenario()
        {
            var driver = new ChromeDriver() | FirefoxDriver() // or whatever web driver you use
    
            // Configure the web driver
    
            var universalSelectors = new UniversalSelectors(driver);
    
            container.RegisterInstanceAs<IWebDriver>(driver);
            container.RegisterInstanceAs(universalSelectors);
        }
    }
    

    Now, add a UniversalSelectors object to the constructor args for your page models:

    public class RolesPageModel
    {
        private readonly IWebDriver Driver;
        private readonly UniversalSelectors universalSelectors;
    
        // other properties
    
        public RolesPageModel(IWebDriver driver, UniversalSelectors universalSelectors)
        {
            Driver = driver;
            this.universalSelectors = universalSelectors;
        }
    
        public void NavigateToRoles()
        {
            universalselectors.ClickAdministration();
            Thread.Sleep(1000);
            universalselectors.ClickSystemUsers();
            Thread.Sleep(1000);
            Roles.Click();
            Thread.Sleep(2000);
        }
    
        public void CreateBlankRole()
        {
            Driver.SwitchTo().DefaultContent();
            Driver.SwitchTo().Frame(universalselectors.iFrame);
            universalselectors.ClickNewRecord();
            Thread.Sleep(2000);
            universalselectors.ClickSaveRecord();
            Thread.Sleep(2000);
        }
    }
    

    And the glue to hold things together lies in your step definitions, where you need to declare the UniversalSelectors argument to those constructors as well:

    [Binding]
    public class RoleSteps
    {
        private readonly RolesPageModel roles;
    
        public Roleteps(IWebDriver driver, UniversalSelectors universalSelectors)
        {
            roles = new RolesPageModel(driver, universalselectors);
        }
    
        [When(@"I create a blank role")]
        public void WhenICreateABlankRole()
        {
            roles.CreateBlankRole();
        }
    }
    

    Addendum

    The real problem, in my opinion, is the UniversalSelectors class itself. The "selectors" (or "locators" in Selenium parlance) can be just as easily declared in the page model that uses the locator. In fact, this is the preferred approach. Pages have common elements, like navigation menus and search bars. These common page elements should be their own page models. Composition rather than inheritance is more useful here:

    public class RolesPageModel
    {
        private readonly IWebDriver Driver;
        private readonly NavigationBarPageModel navBar;
        private readonly SiteSearchBarPageModel searchBar;
    
        // other properties
    
        public RolesPageModel(IWebDriver driver)
        {
            this.driver = driver;
            navBar = new NavigationBarPageModel(driver);
            searchBar = new SiteSearchBarPageModel(driver);
        }
    
        public void NavigateToRoles()
        {
            navBar.GoToRoles();
        }
    

    This simplifies the dependencies for all of your page models, and completely removes the need for UniversalSelectors.