BDD - SpecFlow Web UI 测试实践

引言

前面有介绍 Specflow 基于不同 Unit Test Provider (Xunit,MSTest,NUnit,SpecRun) 的实践系列:

BDD - SpecFlow BDD 测试实践 SpecFlow + SpecRun
BDD - SpecFlow BDD 测试实践 SpecFlow + Xunit
BDD - SpecFlow BDD 测试实践 SpecFlow 模板
BDD - SpecFlow BDD 测试实践 SpecFlow + MSTest
BDD - SpecFlow BDD 测试实践 SpecFlow + NUnit

上述实践,只有 BDD - SpecFlow BDD 测试实践 SpecFlow + SpecRun 不管是通过 Test Explore 界面还是通过 VSTest.Console.exe 命令执行测试都会自动生成友好的测试报告,所以这次 Web UI 测试实践我们采用 Sepcflow & SpecRun。

Selenium & Page Object Model Pattern

Selenium 是免费开源的自动化框架,应用于跨浏览器,跨平台的 Web Application。Selenium 结合 SpecFlow 通过 UI (user interface) 用于测试 Web Application。

Page Object Model Pattern 是一种模式,用于提取被测页面成不同的类。这样将页面元素按结构分门别类封装在一起,避免自动化代码混乱,提升代码的可读性,可维护性。

被测 Web Application

我们的实践测试项目是一个 Web 版的简单的计算器实现,Web Calculator, 实现了两个数相加的功能。

在这里插入图片描述

创建测试项目

具体细节可参考 BDD - SpecFlow BDD 测试实践 SpecFlow + SpecRun

创建一个 Class Libary 项目

CalculatorSelenium.SpecFlowSpecRun

在这里插入图片描述

在这里插入图片描述

添加 NuGet Packages

添加最新版本的 SpecFlow,SpecRun.SpecFlow Packages

在这里插入图片描述

添加 Selenium 相关 Packages

选择最新版本即可:
Selenium.Support - Selenium 核心包
Selenium.WebDriver.ChromeDriver - 包含 ChromeDriver,Selenium 能够操纵 Chrome 浏览器

添加 Selenium.Support Package

在这里插入图片描述

会连带安装 Selenium.WebDriver Package

在这里插入图片描述

添加 Selenium.WebDriver.ChromeDriver Package

在这里插入图片描述

在这里插入图片描述

编译 Project,Bin 目录下

在这里插入图片描述

创建 BDD Scenarios

我们设计两个 Scenarios,一个是默认的只是两个初始数字相加,另外一个是多组测试数据组合的 Scenario Outline。

添加一个 Calculator.feature

@Calculator
Feature: Calculator
![Calculator](https://specflowoss.github.io/Calculator-Demo/Calculator.html)
Simple calculator for adding **two** numbers

Scenario: Add two numbers
	Given the first number is 50
	And the second number is 70
	When the two numbers are added
	Then the result should be 120


Scenario Outline: Add two numbers permutations
	Given the first number is <First number>
	And the second number is <Second number>
	When the two numbers are added
	Then the result should be <Expected result>

Examples:
	| First number | Second number | Expected result |
	| 0            | 0             | 0               |
	| -1           | 10            | 9               |
	| 6            | 9             | 15              |

在这里插入图片描述

Browser Driver 启动和退出浏览器

我们配置 browser 行为,执行测试过程中启动和退出 Google Chorme。创建一个 BrowserDriver.cs 类来处理,第一次执行测试时,浏览器会启动,当测试结束时,浏览器自动关闭退出。

using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace CalculatorSelenium.Specs.Drivers
{
    /// <summary>
    /// Manages a browser instance using Selenium
    /// </summary>
    public class BrowserDriver : IDisposable
    {
        private readonly Lazy<IWebDriver> _currentWebDriverLazy;
        private bool _isDisposed;

        public BrowserDriver()
        {
            _currentWebDriverLazy = new Lazy<IWebDriver>(CreateWebDriver);
        }

        /// <summary>
        /// The Selenium IWebDriver instance
        /// </summary>
        public IWebDriver Current => _currentWebDriverLazy.Value;

        /// <summary>
        /// Creates the Selenium web driver (opens a browser)
        /// </summary>
        /// <returns></returns>
        private IWebDriver CreateWebDriver()
        {
            //We use the Chrome browser
            var chromeDriverService = ChromeDriverService.CreateDefaultService();

            var chromeOptions = new ChromeOptions();

            var chromeDriver = new ChromeDriver(chromeDriverService, chromeOptions);

            return chromeDriver;
        }

        /// <summary>
        /// Disposes the Selenium web driver (closing the browser)
        /// </summary>
        public void Dispose()
        {
            if (_isDisposed)
            {
                return;
            }

            if (_currentWebDriverLazy.IsValueCreated)
            {
                Current.Quit();
            }

            _isDisposed = true;
        }
    }
}

在这里插入图片描述

Page Object Model Pattern

利用 Selenium WebDriver,我们可以模拟一个用户跟页面交互。页面上的元素 IDs 用来定位需要操作的字段,比如我们需要输入数据。这里我们简单的模拟一个用户输入计算需要的数字,相加,等待结果,然后进行下一个测试。

CalculatorPageObject.cs
用来提取页面元素及封装对元素的操作。

using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;

namespace CalculatorSelenium.Specs.PageObjects
{
    /// <summary>
    /// Calculator Page Object
    /// </summary>
    public class CalculatorPageObject
    {
        //The URL of the calculator to be opened in the browser
        private const string CalculatorUrl = "https://specflowoss.github.io/Calculator-Demo/Calculator.html";

        //The Selenium web driver to automate the browser
        private readonly IWebDriver _webDriver;
        
        //The default wait time in seconds for wait.Until
        public const int DefaultWaitInSeconds = 5;

        public CalculatorPageObject(IWebDriver webDriver)
        {
            _webDriver = webDriver;
        }

        //Finding elements by ID
        private IWebElement FirstNumberElement => _webDriver.FindElement(By.Id("first-number"));
        private IWebElement SecondNumberElement => _webDriver.FindElement(By.Id("second-number"));
        private IWebElement AddButtonElement => _webDriver.FindElement(By.Id("add-button"));
        private IWebElement ResultElement => _webDriver.FindElement(By.Id("result"));
        private IWebElement ResetButtonElement => _webDriver.FindElement(By.Id("reset-button"));

        public void EnterFirstNumber(string number)
        {
            //Clear text box
            FirstNumberElement.Clear();
            //Enter text
            FirstNumberElement.SendKeys(number);
        }

        public void EnterSecondNumber(string number)
        {
            //Clear text box
            SecondNumberElement.Clear();
            //Enter text
            SecondNumberElement.SendKeys(number);
        }

        public void ClickAdd()
        {
            //Click the add button
            AddButtonElement.Click();
        }

        public void EnsureCalculatorIsOpenAndReset()
        {
            //Open the calculator page in the browser if not opened yet
            if (_webDriver.Url != CalculatorUrl)
            {
                _webDriver.Url = CalculatorUrl;
            }
            //Otherwise reset the calculator by clicking the reset button
            else
            {
                //Click the rest button
                ResetButtonElement.Click();

                //Wait until the result is empty again
                WaitForEmptyResult();
            }
        }

        public string WaitForNonEmptyResult()
        {
            //Wait for the result to be not empty
            return WaitUntil(
                () => ResultElement.GetAttribute("value"),
                result => !string.IsNullOrEmpty(result));
        }

        public string WaitForEmptyResult()
        {
            //Wait for the result to be empty
            return WaitUntil(
                () => ResultElement.GetAttribute("value"),
                result => result == string.Empty);
        }

        /// <summary>
        /// Helper method to wait until the expected result is available on the UI
        /// </summary>
        /// <typeparam name="T">The type of result to retrieve</typeparam>
        /// <param name="getResult">The function to poll the result from the UI</param>
        /// <param name="isResultAccepted">The function to decide if the polled result is accepted</param>
        /// <returns>An accepted result returned from the UI. If the UI does not return an accepted result within the timeout an exception is thrown.</returns>
        private T WaitUntil<T>(Func<T> getResult, Func<T, bool> isResultAccepted) where T: class
        {
            var wait = new WebDriverWait(_webDriver, TimeSpan.FromSeconds(DefaultWaitInSeconds));
            return wait.Until(driver =>
            {
                var result = getResult();
                if (!isResultAccepted(result))
                    return default;

                return result;
            });

        }
    }
}

在这里插入图片描述

实现 Step Definition

我们来实现一下 BDD Feature 文件中的 steps。

CalculatorStepDefinitions.cs
会用到前面创建的 calculatorPageObject 和 Browserdriver

需要添加 FluentAssertions NuGet package, 否则编译出错。

using CalculatorSelenium.Specs.Drivers;
using CalculatorSelenium.Specs.PageObjects;
using FluentAssertions;
using TechTalk.SpecFlow;

namespace CalculatorSelenium.Specs.Steps
{
    [Binding]
    public sealed class CalculatorStepDefinitions
    {
        //Page Object for Calculator
        private readonly CalculatorPageObject _calculatorPageObject;

        public CalculatorStepDefinitions(BrowserDriver browserDriver)
        {
            _calculatorPageObject = new CalculatorPageObject(browserDriver.Current);
        }

        [Given("the first number is (.*)")]
        public void GivenTheFirstNumberIs(int number)
        {
            //delegate to Page Object
            _calculatorPageObject.EnterFirstNumber(number.ToString());
        }

        [Given("the second number is (.*)")]
        public void GivenTheSecondNumberIs(int number)
        {
            //delegate to Page Object
            _calculatorPageObject.EnterSecondNumber(number.ToString());
        }

        [When("the two numbers are added")]
        public void WhenTheTwoNumbersAreAdded()
        {
            //delegate to Page Object
            _calculatorPageObject.ClickAdd();
        }

        [Then("the result should be (.*)")]
        public void ThenTheResultShouldBe(int expectedResult)
        {
            //delegate to Page Object
            var actualResult = _calculatorPageObject.WaitForNonEmptyResult();

            actualResult.Should().Be(expectedResult.ToString());
        }
    }
}

在这里插入图片描述
注意:Then step 是判断结果是否正确。这里会有一个延时,我们需要处理一下这个行为,调用 WaitForNonEmptyResult() 方法。

设置 Hooks

Test Suite level

一次可能有序的执行多个 Scenarios,为了节约时间,避免每个 Scenario 都重新打开一次 Browser,我们可以用一个 browser instance 运行所有的 Scenarios。我们可以设置 Test suite 级别的 Hook,但是我们在执行每个 Scenario 之前重置页面状态注意,这里会有一个缺陷,就是用单个的 browser instance 不能进行并发执行 cases,所以当有大量 Scenarios,不推荐这种做法,并发执行会更快一些。

SharedBrowserHooks.cs

using BoDi;
using CalculatorSelenium.Specs.Drivers;
using TechTalk.SpecFlow;

namespace CalculatorSelenium.Specs.Hooks
{
    /// <summary>
    /// Share the same browser window for all scenarios
    /// </summary>
    /// <remarks>
    /// This makes the sequential execution of scenarios faster (opening a new browser window each time would take more time)
    /// As a tradeoff:
    ///  - we cannot run the tests in parallel
    ///  - we have to "reset" the state of the browser before each scenario
    /// </remarks>
    [Binding]
    public class SharedBrowserHooks
    {
        [BeforeTestRun]
        public static void BeforeTestRun(ObjectContainer testThreadContainer)
        {
            //Initialize a shared BrowserDriver in the global container
            testThreadContainer.BaseContainer.Resolve<BrowserDriver>();
        }
    }
}

Scenarios level

因为我们前面有设置重用 browser instance,所以每个 Scenario 执行前,必须得重置 Web application,即恢复 Web Application 最初状态,我们可以设置 Scenario hook 来处理。

CalculatorHooks.cs

using CalculatorSelenium.Specs.Drivers;
using CalculatorSelenium.Specs.PageObjects;
using TechTalk.SpecFlow;

namespace CalculatorSelenium.Specs.Hooks
{
    /// <summary>
    /// Calculator related hooks
    /// </summary>
    [Binding]
    public class CalculatorHooks
    {
        ///<summary>
        ///  Reset the calculator before each scenario tagged with "Calculator"
        /// </summary>
        [BeforeScenario("Calculator")]
        public static void BeforeScenario(BrowserDriver browserDriver)
        {
            var calculatorPageObject = new CalculatorPageObject(browserDriver.Current);
            calculatorPageObject.EnsureCalculatorIsOpenAndReset();
        }
    }
}

在这里插入图片描述

执行测试

我们可以通过 Test Explore 来执行所有的 Scenarios,因为设置了 Test Suite 级别的 Hooks 共享 browser instance,所以是一个 browser instance 执行完所有的 Scanrios,我们还设置了 Scenarios 级别的 Hooks 重置页面,所以每次执行 Scnarios 前,页面会重置到初始状态。

在这里插入图片描述

Test Report

在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值