65.9K
CodeProject 正在变化。 阅读更多。
Home

测试架构设计评估系统 - 基于SpecFlow的测试

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2017年5月21日

Ms-PL

5分钟阅读

viewsIcon

5325

我们将使用之前介绍的评估框架来评估基于 SpecFlow 的测试。了解评分和评分理由。

引言

在我之前的文章《测试架构设计评估系统》中,我向您介绍了用于系统测试架构设计的八项标准。为了全面理解该系统,我将使用它来评估几个实际案例,为每个标准分配评分,并解释我的理由。我们将要评估的第三种测试是基于 SpecFlow 的测试。如果您不熟悉 SpecFlow,可以查看我的SpecFlow 系列

基于 SpecFlow 的测试

功能和场景

SpecFlow 使用 Gherkin DSL 来描述系统的行为,使用人类可读的语法。它使用所谓的“规范”,其中测试场景通过 Gherkin 语句进行描述。在构建时,DSL 被编译成 MSTest 测试。

Feature: Create Purchase in Amazon
	In order to receive a book online
	As a client
	I want to be able to choose it through the browser and pay for it online

@testingFramework
Scenario: Create Successful Purchase 
When Billing Country Is United States with American Express Card
	When I navigate to "/Selenium-Testing-Cookbook-Gundecha-Unmesh/dp/1849515743"
	And I click the 'buy now' button
	And then I click the 'proceed to checkout' button
	#When the login page loads
	And I login with email = "g3984159@trbvm.com" and pass = "ASDFG_12345"
	#When the shipping address page loads
	And I type full name = "John Smith", 
	country = "United States", Adress = "950 Avenue of the Americas", 
	city = "New Your City", state = "New Your", 
	zip = "10001-2121" and phone = "00164644885569"
	And I choose to fill different billing, full name = "John Smith", 
	country = "United States", Adress = "950 Avenue of the Americas", 
	city = "New Your City", state = "New Your", 
	zip = "10001-2121" and phone = "00164644885569"
	And click shipping address page 'continue' button
	And click shipping payment top 'continue' button
	Then assert that order total price = "40.49"

绑定方法

您还需要为场景中使用的每个语句定义绑定方法。否则,测试将失败。这些绑定定义在标记了 Bindings 属性的标准 C# 类中。每个步骤方法都标记有步骤类型属性,其中包含该步骤的正则表达式模式。在步骤方法内部,调用页面逻辑。通过这种方式,每个步骤定义并执行测试工作流的一小部分。通过这种测试方法,不存在编写标准的 MSTest 类。

[Binding]
public class CreatePurchaseSteps
{
    [When(@"I navigate to ""([^""]*)""")]
    public void NavigateToItemUrl(string itemUrl)
    {
        var itemPage = UnityContainerFactory.GetContainer().Resolve<ItemPage>();
        itemPage.Navigate(itemUrl);
    }
        
    [When(@"I click the 'buy now' button")]
    public void ClickBuyNowButtonItemPage()
    {
        var itemPage = UnityContainerFactory.GetContainer().Resolve<ItemPage>();
        itemPage.ClickBuyNowButton();
    }

    [When(@"then I click the 'proceed to checkout' button")]
    public void ClickProceedToCheckoutButtonPreviewShoppingCartPage()
    {
        var previewShoppingCartPage = 
        UnityContainerFactory.GetContainer().Resolve<PreviewShoppingCartPage>();
        previewShoppingCartPage.ClickProceedToCheckoutButton();
    }
        
    [When(@"the login page loads")]
    public void SignInPageLoads()
    {
        var signInPage = UnityContainerFactory.GetContainer().Resolve<SignInPage>();
        signInPage.WaitForPageToLoad();
    }
        
    [When(@"I login with email = ""([^""]*)"" and pass = ""([^""]*)""")]
    public void LoginWithEmailAndPass(string email, string password)
    {
        var signInPage = UnityContainerFactory.GetContainer().Resolve<SignInPage>();
        signInPage.Login(email, password);
    }

    [When(@"the shipping address page loads")]
    public void ShippingPageLoads()
    {
        var shippingAddressPage = 
        UnityContainerFactory.GetContainer().Resolve<ShippingAddressPage>();
        shippingAddressPage.WaitForPageToLoad();
    }
        
    [When(@"I type full name = ""([^""]*)"", country = ""([^""]*)"", 
    Adress = ""([^""]*)"", 
    city = ""([^""]*)"", 
    state = ""([^""]*)"", 
    zip = ""([^""]*)"" and 
    phone = ""([^""]*)""")]
    public void FillShippingInfo(string fullName, string country, 
    string address, string state, string city, string zip, string phone)
    {
        var shippingAddressPage = 
        UnityContainerFactory.GetContainer().Resolve<ShippingAddressPage>();
        var clientPurchaseInfo = new ClientPurchaseInfo(
            new ClientAddressInfo()
            {
                FullName = fullName,
                Country = country,
                Address1 = address,
                State = state,
                City = city,
                Zip = zip,
                Phone = phone
            });
        shippingAddressPage.FillShippingInfo(clientPurchaseInfo);
    }
        
    [When(@"I choose to fill different billing, full name = 
    ""([^""]*)"", country = ""([^""]*)"", Adress = ""([^""]*)"", 
    city = ""([^""]*)"", state = ""([^""]*)"", zip = ""([^""]*)"" and 
    phone = ""([^""]*)""")]
    public void FillDifferentBillingInfo(string fullName, 
    string country, string address, string state, string city, string zip, string phone)
    {
        var shippingAddressPage = 
        UnityContainerFactory.GetContainer().Resolve<ShippingAddressPage>();
        var shippingPaymentPage = 
        UnityContainerFactory.GetContainer().Resolve<ShippingPaymentPage>();
        var clientPurchaseInfo = new ClientPurchaseInfo(
            new ClientAddressInfo()
            {
                FullName = fullName,
                Country = country,
                Address1 = address,
                State = state,
                City = city,
                Zip = zip,
                Phone = phone
            });
        shippingAddressPage.ClickDifferentBillingCheckBox(clientPurchaseInfo);
        shippingAddressPage.ClickContinueButton();
        shippingPaymentPage.ClickBottomContinueButton();
        shippingAddressPage.FillBillingInfo(clientPurchaseInfo);
    }
        
    [When(@"click shipping address page 'continue' button")]
    public void ClickContinueButtonShippingAddressPage()
    {
        var shippingAddressPage = 
        UnityContainerFactory.GetContainer().Resolve<ShippingAddressPage>();
        shippingAddressPage.ClickContinueButton();
    }
        
    [When(@"click shipping payment top 'continue' button")]
    public void WhenClickTopPaymentButton()
    {
        var shippingPaymentPage = PerfectSystemTestsDesign.Base.UnityContainerFactory.
        GetContainer().Resolve<ShippingPaymentPage>();
        shippingPaymentPage.ClickTopContinueButton();
    }
        
    [Then(@"assert that order total price = ""([^""]*)""")]
    public void AssertOrderTotalPrice(string itemPrice)
    {
        var placeOrderPage = PerfectSystemTestsDesign.Base.
        UnityContainerFactory.GetContainer().Resolve<PlaceOrderPage>();
        double totalPrice = double.Parse(itemPrice);
        placeOrderPage.AssertOrderTotalPrice(totalPrice);
    }
}

钩子方法

有所谓的“钩子类”,可以在运行、功能、代码块或步骤级别定义不同的预/后执行逻辑。例如,在这里,我们在每个测试之前启动一个浏览器,并将所有需要的页面注册为该测试的单例。之后,我们关闭浏览器。

[Binding]
public sealed class SpecflowHooks
{
    [BeforeTestRun]
    public static void BeforeTestRun()
    {
        Driver.StartBrowser(BrowserTypes.Chrome);
        UnityContainerFactory.GetContainer().RegisterType
        <ItemPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <PreviewShoppingCartPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <SignInPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingAddressPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingPaymentPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <PlaceOrderPage>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ItemPageBuyBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ItemPageNavigationBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <PlaceOrderPageAssertFinalAmountsBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <PreviewShoppingCartPageProceedBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingAddressPageContinueBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingAddressPageFillDifferentBillingBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingAddressPageFillShippingBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShippingPaymentPageContinueBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <SignInPageLoginBehaviour>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterType
        <ShoppingCart>(new ContainerControlledLifetimeManager());
        UnityContainerFactory.GetContainer().RegisterInstance
        <IWebDriver>(PerfectSystemTestsDesign.Core.Driver.Browser);
    }

    [AfterTestRun]
    public static void AfterTestRun()
    {
        Driver.StopBrowser();
    }

    [BeforeScenario]
    public static void BeforeScenario()
    {
    }

    [AfterScenario]
    public static void AfterScenario()
    {
    }
}

数据驱动测试示例表

Specflow 通过数据表支持数据驱动测试 - 为表中的每一行生成一个新测试。请记住,我是手动格式化表格的。Visual Studio 对 Gherkin 的支持尚未达到所需水平。

Scenario Outline: Successfully Convert Seconds to Minutes Table
	When I navigate to Seconds to Minutes Page
	And type seconds for <seconds>
	Then assert that <minutes> minutes are displayed as answer
Examples:
| seconds						| minutes   | 
| 1 day, 1 hour, 1 second       | 1500		| 
| 5 days, 3 minutes 			| 7203		| 
| 4 hours						| 240		| 
| 180 seconds     				| 3			| 

将多个参数传递给步骤

此表将传递类似动态对象列表的内容给我们的绑定方法。顺便说一句,它实际上是使用动态 C# 对象实现的。

Scenario: Add Amazon Products with Affiliate Codes
	When add products
	| Url                                      | AffilicateCode |
	| /dp/B00TSUGXKE/ref=ods_gw_d_h1_tab_fd_c3 | affiliate3     |
	| /dp/B00KC6I06S/ref=fs_ods_fs_tab_al      | affiliate4     |
	| /dp/B0189XYY0Q/ref=fs_ods_fs_tab_ts      | affiliate5     |
	| /dp/B018Y22C2Y/ref=fs_ods_fs_tab_fk      | affiliate6     |

这是我们在测试中使用 SpecFlow 的参数表的方式。正如我所指出的,它会创建一个动态对象列表,我们可以遍历它们。

[When(@"add products")]
public void NavigateToItemUrl(Table productsTable)
{
    var itemPage = UnityContainerFactory.GetContainer().Resolve<ItemPage>();
    IEnumerable<dynamic> products = productsTable.CreateDynamicSet();
    foreach (var product in products)
    {
        itemPage.Navigate(string.Concat(product.Url, "?", product.AffilicateCode));
        itemPage.ClickBuyNowButton();
    }
}

评估基于 SpecFlow 的测试 - 评估系统

可维护性

关于行为的所有提及也适用于此方法。但是,由于存在额外的绑定文件,评分有所降低。此外,用户无法在功能文件中使用现有的标签和常量,这会导致硬编码数据和复制粘贴开发。

 基于 Facade 的测试
可维护性3
可读性 
代码复杂度指数 
可用性 
灵活性 
学习曲线 
最少知识 

可读性

SpecFlow 的主要优势在于可读性。所有步骤都通过 Gherkin DSL 以人类可读的语法进行描述。即使是基本的初始化方法也通过几句话进行描述,这使得它们对用户更有意义。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数 
可用性 
灵活性 
学习曲线 
最少知识 

代码复杂度指数

这里的代码复杂度指数并不完全准确,因为 Visual Studio 不支持计算 Gherkin 文件的代码度量。绑定类的指数被标记为“非常好”。可能是因为没有基类。但是,它们依赖于多个类,如页面、行为或 Facade。我认为您会同意我,如果我们能够计算 Gherkin 文件的度量,由于上述原因,它们不会做得很好,因此我将整体评分降低了一分,仅标记为“好”。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数3
可用性 
灵活性 
学习曲线 
最少知识 

可用性

此参数的评分被评为“非常差”,因为在用户可以使用其测试中的任何步骤之前(假设他/她正在为新功能从头开始编写新测试),需要创建大量新类。SpecFlow 与 Visual Studio 的集成很差,编写时建议的步骤(IntelliSense)并不十分有用。如果您需要定义几个具有常见起始词的动作(您应该在绑定类中定义不同的重写方法 + 使用自定义正则表达式模式),那将是一个挑战。如果您使用示例表生成测试,则应手动格式化表格,如果您想使其可读。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数3
可用性1
灵活性 
学习曲线 
最少知识 

灵活性

评分仅为“好”,因为为了让 SpecFlow 的 API 支持附加步骤,您需要在绑定类中创建包装方法,并使用自定义正则表达式。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数3
可用性1
灵活性4
学习曲线 
最少知识 

学习曲线

我猜写新测试会比只使用页面对象的方法更难,尤其是在没有使用 SpecFlow 句子步骤的现有测试的情况下。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数3
可用性1
灵活性4
学习曲线3
最少知识 

最少知识原则

您只将必需的参数传递给具体的绑定方法。因此,评分被标记为“优秀”。

 基于 Facade 的测试
可维护性3
可读性5
代码复杂度指数3
可用性1
灵活性4
学习曲线3
最少知识5

您可以观看我关于该系统的会议演讲,或下载完整的幻灯片

设计与架构

文章《测试架构设计评估系统 - 基于 SpecFlow 的测试》首次出现在Automate The Planet

所有图片均从 DepositPhotos.com 购买,不可免费下载和使用。

许可协议

© . All rights reserved.