• 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              |
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    Browser Driver 启动和退出浏览器

    我们配置 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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    在这里插入图片描述

    Page Object Model Pattern

    利用 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;
                });
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110

    在这里插入图片描述

    实现 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());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    在这里插入图片描述
    注意: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>();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    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
    {
        /// 
        /// 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();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述

    执行测试

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

    在这里插入图片描述

    Test Report

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    重拾html5
    linux安装宝塔
    从阿里云迁移Redis到AWS的规划和前期准备
    三. 操作系统 (6分) [理解|计算]
    zKSync 2.0的合约和事务
    2.6基数排序(桶排序)
    【校招VIP】高校陌生人活动|产品的竞品和需求分析
    分页查询的SQL优化
    大数据下一代变革之必研究数据湖技术Hudi原理实战双管齐下-下
    拓世法宝AI智能直播一体机,快速搭建品牌矩阵,开启扩张新里程
  • 原文地址:https://blog.csdn.net/wumingxiaoyao/article/details/127701040