前面有介绍 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 是免费开源的自动化框架,应用于跨浏览器,跨平台的 Web Application。Selenium 结合 SpecFlow 通过 UI (user interface) 用于测试 Web Application。
Page Object Model Pattern 是一种模式,用于提取被测页面成不同的类。这样将页面元素按结构分门别类封装在一起,避免自动化代码混乱,提升代码的可读性,可维护性。
我们的实践测试项目是一个 Web 版的简单的计算器实现,Web Calculator, 实现了两个数相加的功能。
具体细节可参考 BDD - SpecFlow BDD 测试实践 SpecFlow + SpecRun
CalculatorSelenium.SpecFlowSpecRun
选择最新版本即可:
Selenium.Support - Selenium 核心包
Selenium.WebDriver.ChromeDriver - 包含 ChromeDriver,Selenium 能够操纵 Chrome 浏览器
添加 Selenium.Support Package
会连带安装 Selenium.WebDriver Package
添加 Selenium.WebDriver.ChromeDriver Package
编译 Project,Bin 目录下
我们设计两个 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 行为,执行测试过程中启动和退出 Google Chorme。创建一个 BrowserDriver.cs 类来处理,第一次执行测试时,浏览器会启动,当测试结束时,浏览器自动关闭退出。
using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
namespace CalculatorSelenium.Specs.Drivers
{
///
/// Manages a browser instance using Selenium
///
public class BrowserDriver : IDisposable
{
private readonly Lazy<IWebDriver> _currentWebDriverLazy;
private bool _isDisposed;
public BrowserDriver()
{
_currentWebDriverLazy = new Lazy<IWebDriver>(CreateWebDriver);
}
///
/// The Selenium IWebDriver instance
///
public IWebDriver Current => _currentWebDriverLazy.Value;
///
/// Creates the Selenium web driver (opens a browser)
///
///
private IWebDriver CreateWebDriver()
{
//We use the Chrome browser
var chromeDriverService = ChromeDriverService.CreateDefaultService();
var chromeOptions = new ChromeOptions();
var chromeDriver = new ChromeDriver(chromeDriverService, chromeOptions);
return chromeDriver;
}
///
/// Disposes the Selenium web driver (closing the browser)
///
public void Dispose()
{
if (_isDisposed)
{
return;
}
if (_currentWebDriverLazy.IsValueCreated)
{
Current.Quit();
}
_isDisposed = true;
}
}
}
利用 Selenium WebDriver,我们可以模拟一个用户跟页面交互。页面上的元素 IDs 用来定位需要操作的字段,比如我们需要输入数据。这里我们简单的模拟一个用户输入计算需要的数字,相加,等待结果,然后进行下一个测试。
CalculatorPageObject.cs
用来提取页面元素及封装对元素的操作。
using System;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
namespace CalculatorSelenium.Specs.PageObjects
{
///
/// Calculator Page Object
///
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);
}
///
/// Helper method to wait until the expected result is available on the UI
///
/// The type of result to retrieve
/// The function to poll the result from the UI
/// The function to decide if the polled result is accepted
/// An accepted result returned from the UI. If the UI does not return an accepted result within the timeout an exception is thrown.
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;
});
}
}
}
我们来实现一下 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() 方法。
一次可能有序的执行多个 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>();
}
}
}
因为我们前面有设置重用 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
{
///
/// Calculator related hooks
///
[Binding]
public class CalculatorHooks
{
///
/// Reset the calculator before each scenario tagged with "Calculator"
///
[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