Is it possible to create reusable and generic Specflow step definitions for multiple similar screens using page objects?

只谈情不闲聊 提交于 2020-05-15 10:18:06

问题


I am working on an app with many similar datatable / CRUD screens. I use Selenium with page objects pattern to navigate in the app, and object mothers to create predefined test data especially for forms with many inputs.

While writing feature files, it occurred to me that the tests are so similar to each other and that it should be possible to generalize some common steps for the sake of reuse and DRYness. The DataTable page object was easy, since selectors are the same for all the pages. So I created a DataTable page object that has the necessary operations for filtering, selecting the rows etc. I used DI via Specflow's Context Injection mechanism.

Example:

Scenario: List and search Users
    When I type 'test@test.test' in filter
    Then I should only see items with 'email' = 'test@test.test'

And steps:

[Binding]
public class DataTableSteps
{
    private DataTable _page;
    public DataTableSteps(DataTable page) => _page = page;

    [When(@"I type '(.*)' in filter")]
    public void WhenITypeInFilter(string value) { 
        _page.FilterSearch (value);    
      }
    [Then(@"I should see items with '(.*)' = '(.*)'")]
    public void ThenIShouldSeeAListWhereContains(string column, string value)
    {
        _page.VisibleInTable(column, value).Should().BeTrue();
    }
}

The problem is, I can't find a good design when I try to make a common step class WebFormSteps that handles form data for filling in various inputs with different WebForm page objects. I want to do the following:

Scenario: Add User
    When I go to '/Users'
    When I create a 'validUser' Item  #'validUser' comes from the object mother
    Then 'validUser' should be added

And use the same steps and step definitions for, product for instance:

Scenario: Add Product
    When I go to '/Products'
    When I create a 'validProduct' Item
    Then 'validProduct' should be added

I thought of using interfaces or abstract classes and let NewUserInputWebForm page object and UserMother to implement these, however I could not find a way to inject the right concrete type at run time. (I considered to put the concrete object mothers to the related page objects, that would mean one less dependency to the step, but I couldn't justify to add an object mother to such an unrelated class, i.e. page object).

[Binding]
class WebFormSteps
{
    IWebFormObject formObject;
    IObjectMother objectMother;

    public WebFormSteps(IWebFormObject formObject, IObjectMother objectMother)
    {
        this.formObject = formObject;
        this.objectMother = objectMother;
    }

    [When(@"I create a '([^']*)' item")]
    public void WhenICreate(string exampleName)
    {
        form = objectMother.Create(exampleName);
        formObject.FillForm(form);
        formObject.Submit();
    }
}

One thing that came to my mind is to use a scoped hook and register interface with the right objects, however there will be an abundance of tags and hook methods in that case. Similarly, I can register the type at some other step, for instance When I go to '/Users' step, but it would complicate things more. Reflection may be used or type name can be specified in the step definition: When I create a 'User' named 'validUser', but that would make the brittle tests to be even more brittle. Furthermore, tests would become too technical, not cucumber at all.

Is there a good way to achieve this? I am not even sure if such approach is good practice (including data table) because I was not able to find more than trivial examples on such usage. Should I stick to specialized steps like:

Scenario: Add User
    When I go to '/Users'
    When I create a user named 'validUser'
    Then 'validUser' should be added to users

回答1:


Probably Scenario Outline will help you

Scenario Outline: Add User
    When I go to <Page>
    When I create a user named <Username>
    Then <Username> should be added to users
Examples: 
| Page    | Username |
|"//Users"| "User1"  |
|"//Users"| "User2"  |
|"//Users"| "User3"  |
|"//Smth" | "User1"  |
|"//Smth" | "User2"  |
|"//Smth" | "User3"  |
|.........|..........|



回答2:


You can simplify all of this using a table and a data transfer object representing values from that table:

When I create the following user:
    | Field      | Value |
    | Username   | test  |
    | First name | John  |
    | Last name  | Doe   |

The step definition will get a Table object. You can use the CreateInstance<T>() extension method in the TechTalk.SpecFlow.Assist namespace to map the table to an object where the "Field" column matches property names on the class:

[When(@"I create the following user:")]
public void WhenICreateTheFollowingUser(Table table)
{
    var user = table.CreateInstance<UserRow>();
    var userForm = new AddEditUserPageObject(driver);

    // Send user object to Selenium page object in order to enter
    // data into form fields
    userForm.FillForm(user);
}

The class mapped from the table would be:

public class UserRow
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Username { get; set; }
}

And a sample page object for Selenium:

public class AddEditUserPageObject
{
    private readonly IWebDriver driver;

    public AddEditUserPageObject(IWebDriver driver)
    {
        this.driver = driver;
    }

    public void FillForm(UserRow data)
    {
        Username.SendKeys(data.Username);
        FirstName.SendKeys(data.FirstName);
        LastName.SendKeys(data.LastName);
    }
}

If you want to utilize an object mother in order to set some default parameters, you can do this using a different overload of the table.CreateInstance<T>() method that allows you to specify a lambda expression used to create a new instance of UserRow, which could then use the object mother:

[When(@"I create the following user:")]
public void WhenICreateTheFollowingUser(Table table)
{
    var user = table.CreateInstance<UserRow>(() => objectMother.CreateUser("someExample"));
    var userForm = new AddEditUserPageObject(driver);

    // Send user object to Selenium page object in order to enter
    // data into form fields
    userForm.FillForm(user);
}

And if you wanted to parameterize the value passed to the object mother you can always add a new step:

[When(@"I create a user named ""(.*)"":")]
public void WhenICreateTheFollowingUser(string username, Table table)
{
    var user = table.CreateInstance<UserRow>(() => objectMother.CreateUser(username));
    var userForm = new AddEditUserPageObject(driver);

    // Send user object to Selenium page object in order to enter
    // data into form fields
    userForm.FillForm(user);
}

And the step in your feature file would look like:

When I create a user named "test"


来源:https://stackoverflow.com/questions/58021071/is-it-possible-to-create-reusable-and-generic-specflow-step-definitions-for-mult

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!