持续交付技术4:手把手教你实战BDD

文摘   2024-07-22 22:13   广东  

“开发软件系统最困难的部分就是准确说明开发什么” 。看一下下面的开发场景:

场景一:业务分析人员觉得自己分析的需求已经写的很清晰了,并且跟技术人员进行了足够的沟通,可是开发完做Desk check的时候,发现所开发的功能还是跟期望有差距。

场景二:开发团队辛辛苦苦开发完一个功能,满怀信心的去给产品经理/客户展示的时候,才发现原来客户需求的功能不是这样的。
这些场景是不是似曾相识?为什么会这样?

第一个场景是开发团队内部技术人员跟需求分析人员的理解有偏差,导致大家理解的需求其实是不一样的;第二个场景是开发团队没有真正理解产品经理/客户所提出来的真实需求,导致开发的产品跟需求不一致。其实,产生这两个不一致的真正原因是因为不同角色有着不同的领域知识,说着不同的语言,大家在沟通的时候,如果都用自己领域语言,必然会产生沟通代沟,导致理解的不一致性。领域知识不同、语言不通导致沟通障碍,这个客观存在的问题该如何解决呢?BDD正是为此而生

一、BDD概念


BDD:将业务需求转化为高质量的软件

在软件开发的广阔世界中,行为驱动开发(Behavior Driven Development,简称BDD)逐渐成为一种重要的开发实践,因为它提倡将业务需求作为开发的驱动力,而不是仅仅关注代码本身。这篇文章将详细介绍BDD的核心概念、方法和价值。

BDD的基本理念

BDD是一种敏捷软件开发的技术,它倡导使用简洁易懂的语言来描述软件的期望行为,这有助于所有团队成员(包括业务人员、开发人员、测试人员等)共享对软件功能的理解。在BDD中,一个重要的工具是“场景”(Scenarios),它描述了给定某种情况下,软件应该如何行为。

BDD的提出者Dan North强调BDD不是关于测试的,它是在应用程序存在之前,写出用例与期望,从而描述应用程序的行为,并且促使在项目中的人们彼此互相沟通。

要给BDD下个清晰易懂的定义很难,包括大师们也这么认为,这里试着总结以下几点:

  1. 关注的是业务领域,而不是技术:BDD强调用领域特定语言(DSL, domain specific language)描述用户行为,定义业务需求,而不会关心系统的技术实现。

  2. 不是工具,强调的是一种协作方式:BDD要求各个角色共同参与系统行为的挖掘和定义,以实现对业务价值的一致理解。

  3. 不是关于测试的:BDD源自TDD,又不同于TDD,重点不是关于测试的,但可以指导更好的做自动化测试。

  4. 全栈敏捷方法:BDD促使团队所有角色从需求到最后的测试验证,进行高度的协作和沟通,以交付最有价值的功能。

BDD 中的常见概念

在BDD中,常用的概念如下:

  • Epic: 史诗,一般指一个版本或一批功能更新

  • Feature: 特性,一般指一个功能点,如登录,添加商品,查询商品等,在测试中对应一个测试套件

  • Scenario:场景,即Story,一个明确的场景,对应一个测试用例

  • Step: 步骤,测试步骤有Given/When/Then三种

  • Given: 假设,给定数据或前置条件,对应测试中的setup

  • When: 当…时,对应一个测试步骤

  • Then: 然后,即期望结果,对应一个测试断言

  • And: 同上,可以用于Given/When/Then后

BDD的优势

BDD的优点在于它提供了一种方式,使得所有的团队成员都能对软件的需求有一个共同的理解。通过使用简洁易懂的语言来描述软件的行为,非技术人员也能理解并参与到软件开发过程中。此外,由于测试是在编写代码之前就编写好的,因此可以确保所有的功能都能正常工作,并且随着开发的进行,团队可以持续地对软件进行验证。

BDD的挑战

虽然BDD有很多优点,但是它也有一些挑战,比如需要投入时间来编写和维护测试,以及需要所有的团队成员都理解并采纳BDD的理念等。

尽管如此,BDD仍然是一个非常有价值的开发实践,它使得软件开发更加以用户为中心,有助于提高软件的质量,并且使得团队能够更加有效地进行协作。无论你是一名开发人员,测试人员,还是产品经理,都应该考虑在你的工作中采用BDD。

BDD测试在CI/CD 阶段的位置

在CI/CD(持续集成/持续部署)流程中,BDD(行为驱动开发)测试持续被执行并且在各个阶段都起到关键性的角色。

以下是经典的CI/CD流程中,BDD测试可能出现的位置:

  1. 持续集成(Continuous Integration)阶段:

    这是开发者提交代码更改到代码存储库的阶段,所有新的代码更改都会自动触发构建和测试过程。在这个阶段,BDD测试(以及单元测试和集成测试)通常会运行,以确保新代码的更改或新增功能没有引入新的错误或改变了预期的行为。

  2. 持续交付(Continuous Delivery)阶段:

    在这个阶段,所有的提交都会通过实际生产环境的完全自动化的测试。在这个阶段,BDD测试可以用来验证工作流程和业务场景,保证软件的行为与预期一致。

  3. 持续部署(Continuous Deployment)阶段:

  4. 这个阶段指的是自动将更新的应用版本部署到生产环境的过程。虽然BDD测试通常在持续部署之前就完成了,但是如果你在部署之后有进行任何形式的后期验证(例如: 实时监控,日志分析等),那么这些验证应该再次验证所有预期的行为仍然按预期工作。

整个CI/CD流程中,BDD测试有助于快速捕捉和修复软件错误,并确保应用符合预期的行为。如果发现测试失败或行为不匹配,CI/CD流程会被中断,从而避免将错误的代码推送到生产环境。

二、BDD实施


BDD实施步骤

  1. 定义用户故事:使用简短的故事描述,例如“As a [type of user], I want [some goal] so that [some benefit]”。这样可以清晰地了解用户需要和软件的目标。

  2. 创建场景:通过对故事进行细化和拆分,可以创建多个场景。每个场景都应包括特定的输入和期望输出。

  3. 撰写特性文件:将用户故事和场景集成到文档中,例如Gherkin语言格式。

  4. 实现测试代码:编写测试代码以实现每个场景的期望输出。测试代码应该直接映射到故事和场景。

  5. 运行测试:运行测试以确保软件满足行为和需求。

  6. 自动化测试:将测试代码集成到自动化测试套件中,以便在以后进行快速回归测试。

  7. 确认BDD的价值:BDD的价值在于通过聚焦于用户需求和行为,帮助开发团队快速反馈和快速迭代。因此,每个BDD实践都应该评估它是否实现了这些目标。

BDD实施工具

BDD不只是测试, 但是BDD可以很好的指导测试。
在实际的场景中, 实现BDD 主要包括两个部分:

  1. 使用通用领域语言编写规格

  2. 实现规格的自动化测试

可以帮助实现BDD 的工具有:

  1. Cucumber: Cucumber 是最常用的 BDD 工具之一,它使用简洁的自然语言(Gherkin)编写测试用例,并支持多种编程语言和测试框架。

  2. JBehave: JBehave 是另一个流行的 BDD 工具,它使用类似 Gherkin 的语法编写测试用例,支持 Java 平台的自动化测试。

  3. SpecFlow: SpecFlow 是为 .NET 平台开发的 BDD 工具,它能够将 Gherkin 语法的测试用例与 .NET 代码结合起来执行。

  4. Behave: Behave 是一个 Python BDD 工具,它使用 Gherkin 语法编写测试用例并支持 Python 测试框架。

  5. Jasmine: Jasmine 是一个用于 JavaScript 的 BDD 框架,它提供了简洁的语法和丰富的断言库,适用于前端开发的 BDD 测试。

  6. Robot Framework: Robot Framework 是一个通用的自动化测试框架,它支持 BDD 风格的测试,并提供了许多扩展库和插件。

三、用Cucumber实现BDD示例


Cucumber框架

BDD的java实现可以使用Cucumber框架。

Cucumber是一个支持行为驱动的开发的开源工具。更准确地说,Cucumber可以定义为一个测试框架,由简单的英语文本驱动。它作为文档、自动化测试和开发帮助。

它可以在以下步骤中描述:Cucumber读取在要素文件中以纯英语文本编写的代码;它找到步骤定义中完全匹配的每个步骤。

Cucumber相对其他工具的优点

  • Cucumber支持不同的语言,例如Java、.net、Ruby

  • 它充当业务与技术间桥梁的角色。可以通过在纯英文文本中创建一个测试用例来实现这一点。

  • 它允许在不知道任何代码的情况下编写测试脚本,它允许非程序员参与。

  • 它以端到端测试框架为目的

  • 由于简单的测试脚本架构,Cucumber提供了代码可重用性

1)引入maven依赖

  1. <dependency>

  2. <groupId>io.cucumber</groupId>

  3. <artifactId>cucumber-java</artifactId>

  4. <version>6.7.0</version>

  5. <scope>test</scope>

  6. </dependency>

  7. <dependency>

  8. <groupId>io.cucumber</groupId>

  9. <artifactId>cucumber-junit</artifactId>

  10. <version>6.7.0</version>

  11. <scope>test</scope>

  12. </dependency>

2)创建.feature文件

.feature文件是一种使用Gherkin语言编写的文件,它描述了软件系统的行为和需求,Gherkin是一种简单的英语文本语言,它有助于工具--Cucumber解释和执行测试脚本。

写入Cucumber测试的文件称为Featurefiles。对于每个被测功能,建议应该有一个单独的feature file。feature file的扩展名必须为“.feature”。可以根据需要创建任意数量的feature file。为了具有有组织的结构,每个feature应当具有一个feature file。例如:

用于feature的名称,featurefile的命名约定取决于个人的选择。在Cucumber中没有关于名字的基本规则。一个简单的feature file由以下关键字/部分组成:

Background :通常具有在每个场景运行之前要设置什么的指令。但是,它在“Before”hook之后执行。因此,当我们想要设置Web浏览器或者我们想要建立数据库连接时,这时最佳的运用代码的方式。示例:

Feature:待测功能的名称。

Description(可选):描述测试中的功能。

Scenario:测试场景。

Given:在执行测试步骤之前的先决条件。

When:为了执行下一步骤,应该匹配的特定条件。

Then:如果满足WHEN中提到的条件,应该会发生什么。

3)创建step定义文件

package Annotation;
import io.cucumber.java.en.Given;import io.cucumber.java.en.Then;import io.cucumber.java.en.When;import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;
public class Annotation {WebDriver driver = null;@Given("I am on CSDN login page")public void goToCsdn() {System.setProperty("webdriver.chrome.driver","src/test/resources/chromedriver.exe");driver=new ChromeDriver();driver.navigate().to("https://passport.csdn.net/account/login?ref=toolbar");}
@When("I enter username as \"(.*)\"$")public void enterUsername(String arg1) {driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[1]/span[4]")).click();driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[1]/div/input")).sendKeys(arg1);}
@When ("I enter password as \"(.*)\"$")public void enterPassword(String arg1) {driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[2]/div/input")).sendKeys(arg1);driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[4]/button")).click();}
@Then("Login should fail$")public void checkFail() {if(driver.getCurrentUrl().equalsIgnoreCase("http://my.csdn.net/my/mycsdn")){System.out.println("Test1 Pass");}else {System.out.println("Test1 Failed");}driver.close();}
@Then("Login should successful")public void checkSuccessful() {if(driver.findElement(By.xpath("//*[@id=\"WAF_NC_WRAPPER\"]")).isEnabled()){System.out.println("Test1 Pass");}else {System.out.println("Test1 Failed");}}
@Then("Relogin option should be available$")public void checkRelogin() {if(driver.getCurrentUrl().equalsIgnoreCase("http://my.csdn.net/my/mycsdn")){System.out.println("Test2 Pass"); }else {System.out.println("Test2 Failed");}
driver.close();}}

4) 创建一个runner 类文件

@CucumberOptions(features = "src/test/java/Annotation/test.feature")

此路径需要配置 feature的文件路径,否则无法执行

package Annotation;
import io.cucumber.junit.Cucumber;import io.cucumber.junit.CucumberOptions;import org.junit.runner.RunWith;
@RunWith(Cucumber.class)@CucumberOptions(features = "src/test/java/Annotation/test.feature")public class runTest {}

2.6. Scenario Outline简介

Scenario Outline基本上用表中的值替换变量/关键字。表中的每一行都被认为是一个场景。继续使用登录功能的例子。到目前为止,一直在执行一个场景:提供正确的用户名,登录成功。

现在,假设我们要检查所有三种可能的输入类型的登录是否成功,这三种类型的输入是用户名,电子邮件地址或电话号码。为了实现这一点,将需要写三个不同的场景,其中每个场景将随输入类型而变化,登录成功。在这种情况下,不使用Scenario Outline 需要这样写:

Scenario:
Given user navigates to Facebook
When I enter correct username and password
Then login should be successful

语句是相同的,只有输入参数(用户名/电子邮件地址/电话号码)正在改变。

所以这里我们需要用,Scenario Outline。当用Scenario Outline定义任何场景时,可以指定一个测试场景,在它的底部,提供一些输入。场景将执行与提供的输入数一样多的次数。(类似于ddt 测试驱动,可以有多个数据注入)

Scenario Outline写法:

Feature: Scenario Outline
Scenario Outline: Login functionality for a social networking site.
Given user navigates to CSDN
When I enter Username as "<username>" and Password as "<password>"
Then login should be unsuccessful
Examples:
|username |password |
|username1 |password1 |
|username2 |password2 |
|username3 |password3 |

测试类:

package Annotation;import io.cucumber.java.en.Given;import io.cucumber.java.en.Then;import io.cucumber.java.en.When;import org.openqa.selenium.By;import org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;public class OutLine {WebDriver driver = null;@Given("user navigates to CSDN$")public void goToFacebook() {System.setProperty("webdriver.chrome.driver","src/test/resources/chromedriver.exe");driver=new ChromeDriver();driver.navigate().to("https://passport.csdn.net/account/login?ref=toolbar");}@When("I enter Username as \"([^\"]*)\" and Password as \"([^\"]*)\"$")public void I_enter_Username_as_and_Password_as(String arg1, String arg2) throws InterruptedException {driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[1]/span[4]")).click();Thread.sleep(2000);driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[1]/div/input")).sendKeys(arg1);driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[2]/div/input")).sendKeys(arg2);driver.findElement(By.xpath("/html/body/div[2]/div/div[2]/div[2]/div[1]/div/div[2]/div/div[4]/button")).click();}@Then("login should be unsuccessful$")public void validateRelogin() {if(driver.getCurrentUrl().equalsIgnoreCase("http://my.csdn.net/my/mycsdn")){System.out.println("Test Pass");}else {System.out.println("Test Failed");}}}

   注意:在上面的代码中,必须定义一个具有两个输入参数的函数:一个用户名和另一个用于密码。因此,对于Examples标记中提供的每组输入,将执行GIVEN,WHEN和THEN的设置。


研发效能方法论
分享内容的四大方向:研发效能和软件工程方法论,软件工程技术,平台工程设计,通用五力