Page Object模型与Cucumber的Gherkin语言兼容吗?

29

使用测试自动化的页面对象模型,我们可以将页面链接起来,如下所示:

WebDriver driver = new WebDriver()
HomePage homePage = new HomePage(driver);
LoginPage loginPage = homePage.GoToLoginPage();
WelcomePage welcomePage = loginPage.Login();
etc
etc
这样做的好处是,如果开发人员更改主页,使其不再链接到登录页,我可以更新我的主页类并查看所有需要更新的测试(带有错误),甚至在运行测试之前就能知道。
然而,对于Gherkin,每一行都会形成一个单独的“步骤”,因此也会有一个单独的方法。因此,如何进行链接呢?
难道唯一的方法是将页面对象类的实例(例如homePage、loginPage等)放入横跨gherkin语句持久存储的位置(例如像specflow POCO或“World”)吗?

1
也许这篇文章可以帮助你。 - troig
这是一篇很棒的帖子。唯一的小问题是,如果使用多个步骤类,页面对象之间传递的实例将不会保留。使用内置的依赖注入可以解决这个问题,但是代码会变得相当啰嗦,因为每个页面对象实例前面都要加上pageWorld。 - Charlie S
1
目前这个测试自动化问题有23个赞(对于一个测试自动化问题来说还是挺高的),但是最佳答案只有4个投票。这暗示了Page Object Model(页面对象模型)与Gherkin(一种BDD测试语言)并不是特别兼容......这让我越来越担心,尤其是在我同时使用这两种方法的时候。我无法不感到主要责任在Cucumber(一个行为驱动开发框架)身上,因为它在一个经过验证的类/方法结构之上放置了一个非常奇怪的结构。 - Charlie S
5个回答

8
好的,经过多位开发专家和自动化测试专家询问后,看来解决方案是继续使用链接的方式 [例如:WelcomePage welcomePage = loginPage.loginWithValidUser(validUser)]
要在步骤之间保留页面对象实例(例如上面示例中的welcomePage),您可以使用依赖注入工具(创建类似于Ruby cucumber的World扩展功能)。
以下是更多信息: https://cukes.info/docs/reference/java-di 然而,大多数项目将受益于依赖注入模块,以更好地组织代码并在步骤定义之间共享状态。
有关SpecFlow(.net官方cucumber实现)的更多信息: http://specflow.org/getting-started/beyond-the-basics/ 最后,我已经创建了一个完整的博客,详细阐述了这一领域的内容,希望能够帮助到人们,因为gherkin/page object交互是我极感兴趣的主题: http://www.seligmanventures.com/dev-blog/test-automation-page-object-model-with-gherkin

1
我以前做过你描述的事情,但实际上我不建议这样做。当您更改导航时,它确实会给您编译错误,但问题在于您正在将操作(例如登录)与关于发生什么事情的断言混合在一起(“我最终进入欢迎页面”)。这使得测试诸如密码错误时会发生什么之类的事情变得很丑陋 - 您最终不得不执行诸如LoginPage loginPage = loginPage.loginButExpectError()之类的操作。这只会越来越混乱。 - Perryn Fowler
1
现在我做的是将操作和完成操作后应该在哪里的断言分开。就像loginPage.login(); onPage(WelcomePage.class)会返回WelcomePage对象,然后你可以在这个对象上执行其他操作。 - Perryn Fowler
Perryn,我知道你所说的无效登录,但只需要添加额外的方法(例如loginWithValidUser()和loginWithInvalidUser()),就可以轻松地解决这个问题。总体而言,链式编程的好处超过了所需的额外方法。此外,两种方法背后的代码可以被提取为私有方法,安静地放在页面对象中。 - Charlie S
在这个话题上经过了很多讨论,我刚刚阅读了这两份官方的SpecFlow文档,它们似乎倾向于这个答案,但并不是最终确定的: https://specflow.org/documentation/Sharing-Data-between-Bindings/ https://specflow.org/documentation/ScenarioContext/ - Charlie S
有趣的是,这个问题的赞数比它的最佳答案要高得多(相差很大)。在stackoverflow上,这非常不寻常。我想这表明Gherkin和POM可能并不特别兼容。 - Charlie S

0

当涉及到大多数网站(其中可以使用url),在我看来,最佳实践是直接使用url而不是一个动作来访问该url。

例如:

# Suggested by OP:
driver = Selenium::Webdriver.for :chrome, prefs: prefs
homepage = Homepage.new(driver)
login = homepage.go_to_login
welcome = login.log_in_as('dave4429')

# My Suggestion:
homepage = Url.new('/')
login = Url.new('/login')
welcome = Url.new('/welcome')

这意味着您可以从URL开始,而不必在每个测试中都从主页开始。您仍然可以使用您建议的方法,但它们将用于其他区域,以确保用户可以通过除URL之外的方式访问页面。

然而,这并不是一站式解决方案。对于移动和桌面应用程序,您唯一的选择可能是通过主屏幕进行操作,在这种情况下,您建议的方法绝对是可行的。

"页面对象本身不应进行验证或断言。这是您的测试的一部分,应始终在测试代码中,而不是在页面对象中。" - Selenium HQ

我给出的示例非常基础,我很可能会将它们包装成模块和类,以实现像这样的编码:

google = Project::Pages::Google.new

google.search_for('Hello, World!')
expect(google.found_result?).to_equal(true)

编辑

此外,您似乎对Cucumber如何与Gherkin配合工作存在误解。

每个步骤可以有多行代码,因为步骤本身是对步骤内操作的描述。

例如:

Given I am logged in as "dave4429"
When I have submitted the "Contact Us" form with the following data:
   | dave4429@example.com | David McBlaine | I want to find out more about your Data Protection services, can I talk to a staff member or get a PDF? |
Then an email should be sent to "support@example.com" with the details specified

“When”的定义可能如下所示:

When(/^I have submitted the "Contact Us" form with the following data:$/) do |table|
  rows = table.raw
  row = rows[0]

  contact_us.fill_form({email: row[0], username: row[1], message: row[2]})
  contact_us.submit_message
  expect(browser.title).to_equal("Message Sent!")
end

这完全取决于您在定义中如何分解步骤。

编辑#2

我也清楚地知道,您想要进行方法链接,例如contact_us.fill_form({email: row[0], username: row[1], message: row[2]}).submit_message,这并不是不可能使用我提出的技术,但是关于每个页面是否应该进行此链接,还是将所有内容包含在一个类或模块中,只能根据您的需求来回答。

我认为,将所有内容放到单个类中会使类过于臃肿,拆分该类将允许测试人员拥有更多的控制,并且将减少编写冗余代码的数量。


0

我最近看到的另一个选项是将页面对象实例存储为静态变量,以便可以从任何类中访问它们。


使用静态方法可以让测试人员访问AUT的任何页面,即使这不是AUT的行为。而使用实例则强制你只编写反映AUT实际行为的测试。 - Charlie S

-1
经过对这个话题的讨论,一个同样可行的替代方案是在使用gherkin的页面对象模式时不返回新页面的实例。您将失去通常使用POM获得的链接优势,但代码读起来会更好,更简单。发布这个替代答案,以便作为测试社区,我们可以投票决定哪种方法更受人们喜爱。

感觉给这个答案打-2分有点严厉了。虽然不是我首选的答案,但绝对是一个可行的替代方案,并且确实减少了复杂性。请评论一下扣分原因,因为这可能有助于提供一个“改进”的答案。 - Charlie S
这是一个不存储通过页面导航返回的页面对象的示例:https://www.testautomationtribe.com/specflow-with-page-object - Charlie S
真的很喜欢这种方法,特别是在有人指出gherkin步骤被编码为独立实体后,发现在步骤开始时创建相关页面对象的实例在语义上非常契合。然而,在尝试实施这种方法后,我意识到你需要在步骤级别保持驱动程序实例的公开(而不是将其封装在页面对象中并在导航之间传递)。有人能想到解决这个问题的方法吗? - Charlie S

-1

使用Cucumber和Selenium可能会有些棘手。我开发了一种模式,涉及到扩展方法到Selenium的IWebDriver接口,允许我使用页面对象导航到特定页面。我使用SpecFlow依赖注入框架注册IWebDriver对象,然后我的步骤定义类可以自由地初始化它们需要的任何页面对象。

将Selenium Web Driver注册到SpecFlow

您只需要插入到before/after场景钩子中来管理Web Driver对象:

[Binding]
public class WebDriverFactory
{
    private readonly IObjectContainer container;

    public WebDriverFactory(IObjectContainer container)
    {
        this.container = container;
    }

    [BeforeScenario]
    public void CreateWebDriver()
    {
        var driver = new ChromeDriver(...);

        // Configure Chrome

        container.RegisterInstanceAs<IWebDriver>(driver);
    }

    [AfterScenario]
    public void DestroyWebDriver()
    {
        var driver = container.Resolve<IWebDriver>();

        if (driver == null)
            return;

        // Capture screenshot if you want
        // var photographer = (ITakeScreenshot)driver;

        driver.Quit();
        driver.Dispose();
    }
}

然后,就是使用IWebDriver接口的一些扩展将步骤定义和页面对象粘合在一起的问题。

Selenium页面对象

保持您的页面对象相互导航。例如,HomePage允许您导航到“创建博客文章”页面,并返回该页面的页面对象:

public class HomePage
{
    private readonly IWebDriver driver;
    private readonly WebDriverWait wait;

    private IWebElement CreatePostLink => driver.FindElement(By.LinkText("Create New Blog Post"));

    public HomePage(IWebDriver driver)
    {
        this.driver = driver;
        wait = new WebDriverWait(driver, 30);
    }

    public AddEditBlogPostPage ClickCreatePostLink()
    {
        CreatePostLink.Click();
        wait.Until(d => d.Title.Contains("Create new blog post"));

        return new AddEditBlogPostPage(driver);
    }
}

随后,当您创建新的博客文章时,AddEditBlogPostPage会返回BlogPostListingPage:

public class AddEditBlogPostPage
{
    private readonly IWebDriver driver;

    private IWebElement Title => driver.FindElement(By.Id("Title"));
    private IWebElement PostDate => driver.FindElement(By.Id("Date"));
    private IWebElement Body => driver.FindElement(By.Id("BodyText"));
    private IWebElement SaveButton => driver.FindElement(By.XPath("//button[contains(., 'Save Blog Post')]"));

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

    public BlogPostListingPage CreateBlogPost(BlogPostDataRow data)
    {
        Title.SendKeys(data.Title);
        PostDate.SendKeys(data.Date.ToShortDateString());
        Body.SendKeys(data.Body);
        SaveButton.Click();

        return new BlogPostListingPage(driver);
    }
}

步骤定义将事物粘合在一起

步骤:

When I create a new blog post:
    | Field | Value                              |
    | Title | Selenium Page Objects and Cucumber |
    | Date  | 11/1/2019                          |
    | Body  | ...                                |

将会有这个定义:

[Binding]
public class BlogPostSteps
{
    private readonly IWebDriver driver;

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

    [When(@"I add a new blog post:")]
    public GivenIAmAddingANewBlogPost(Table table)
    {
        var addBlogPostPage = driver.GoToCreateBlogPostPage();
        var blogPostData = table.CreateInstance<BlogPostDataRow>();

        addBlogPostPage.CreateBlogPost(blogPostData);
    }
}

driver.GoToCreateBlogPostPage();是一个对IWebDriver的扩展方法,它用于从一个页面对象导航到另一个页面对象:

public static class SeleniumPageNavigationExtensions
{
    public static AddEditBlogPostPage GoToCreateBlogPostPage(this IWebDriver driver)
    {
        var homePage = new HomePage(driver);

        return homePage.ClickCreatePostLink();
    }
}

这样做可以让您的页面对象保持“纯净”,避免使用SpecFlow、Cucumber和Gherkin。 您可以在其他不使用Gherkin或行为驱动开发的测试中使用这些扩展方法和页面对象。 这允许轻松重用测试类。 您的测试项目应该与实际应用程序测试一样有目的地进行架构设计。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接