问题
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