是否可以使用页面对象为多个类似屏幕创建可重复使用的通用 Specflow 步骤定义? [英] Is it possible to create reusable and generic Specflow step definitions for multiple similar screens using page objects?

查看:25
本文介绍了是否可以使用页面对象为多个类似屏幕创建可重复使用的通用 Specflow 步骤定义?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发具有许多相似数据表/CRUD 屏幕的应用程序.我使用带有页面对象模式的 Selenium 在应用程序中导航,并使用对象 Mothers 创建预定义的测试数据,特别是对于具有许多输入的表单.

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.

在编写功能文件时,我突然想到这些测试彼此非常相似,为了重用和 DRYness,应该可以概括一些常见的步骤.DataTable 页面对象很简单,因为所有页面的选择器都相同.所以我创建了一个 DataTable 页面对象,它具有过滤、选择行等必要的操作.我通过 Specflow 的上下文注入机制使用了 DI.

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.

示例:

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

和步骤:

[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();
    }
}

问题是,当我尝试制作一个通用的步骤类 WebFormSteps 来处理表单数据以使用不同的 WebForm 填充各种输入时,我找不到好的设计> 页面对象.我想做以下事情:

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

我想过使用接口或抽象类并让 NewUserInputWebForm 页面对象和 UserMother 来实现这些,但是我找不到一种方法来注入正确的具体类型运行.(我考虑将具体的对象 Mothers 放在相关的页面对象中,这意味着对步骤的依赖减少,但我无法证明将对象 Mothers 添加到这样一个不相关的类中,即页面对象).

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();
    }
}

我想到的一件事是使用作用域钩子并将接口注册到正确的对象,但是在这种情况下会有大量的标签和钩子方法.同样,我可以在其他步骤注册类型,例如 When I go to '/Users' 步骤,但这会使事情变得更加复杂.可以使用反射或在步骤定义中指定类型名称:当我创建一个名为validUser"的用户"时,但这会使脆弱的测试变得更加脆弱.此外,测试会变得过于技术化,根本不是黄瓜.

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

推荐答案

您可以使用一个表和一个表示该表中值的数据传输对象来简化所有这些:

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   |

步骤定义将获得一个 Table 对象.您可以使用 TechTalk.SpecFlow.Assist 命名空间中的 CreateInstance() 扩展方法将表映射到字段"列匹配属性名称的对象在课堂上:

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);
}

从表映射的类将是:

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

以及 Selenium 的示例页面对象:

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);
    }
}

如果您想利用对象 Mother 来设置一些默认参数,您可以使用 table.CreateInstance() 方法的不同重载来实现,该方法允许您指定用于创建 UserRow 的新实例的 lambda 表达式,然后可以使用对象 Mother:

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);
}

如果你想参数化传递给对象 Mother 的值,你可以随时添加一个新步骤:

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"

这篇关于是否可以使用页面对象为多个类似屏幕创建可重复使用的通用 Specflow 步骤定义?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆