原文地址: spring boot整合Cucumber(BDD)
1、新建一个springboot工程工程结构如下:
2、添加pom依赖
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.chhliu</groupId>
- <artifactId>spring-boot-cucumber</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <packaging>jar</packaging>
- <name>spring-boot-cucumber</name>
- <description>Demo project for Spring Boot and Cucumber</description>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.5.6.RELEASE</version>
- <relativePath /> <!-- lookup parent from repository -->
- </parent>
- <properties>
- <cucumber.version>1.2.4</cucumber.version>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.7</java.version>
- </properties>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>info.cukes</groupId>
- <artifactId>cucumber-java</artifactId>
- <version>${cucumber.version}</version>
- </dependency>
- <dependency>
- <groupId>info.cukes</groupId>
- <artifactId>cucumber-core</artifactId>
- <version>${cucumber.version}</version>
- </dependency>
- <dependency>
- <groupId>info.cukes</groupId>
- <artifactId>cucumber-spring</artifactId>
- <version>${cucumber.version}</version>
- </dependency>
- <dependency>
- <groupId>info.cukes</groupId>
- <artifactId>cucumber-junit</artifactId>
- <version>${cucumber.version}</version>
- <exclusions>
- <exclusion>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- <configuration>
- <source>1.7</source>
- <target>1.7</target>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>exec-maven-plugin</artifactId>
- <configuration>
- <source>1.7</source>
- <target>1.7</target>
- </configuration>
- <executions>
- <execution>
- <phase>integration-test</phase>
- <goals>
- <goal>java</goal>
- </goals>
- <configuration>
- <classpathScope>test</classpathScope>
- <mainClass>com.chhliu.test.CucumberTest.java</mainClass>
- <arguments>
- <argument>--plugin</argument>
- <argument>pretty</argument>
- <argument>--glue</argument>
- <argument>src/test/resources/</argument>
- </arguments>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
- </project>
2、编写service接口及其实现类
- package com.chhliu.service;
- /**
- * 模拟登录
- * @author chhliu
- *
- */
- public interface UserInfoServiceI {
- boolean login(String username, String password, String confirmPassword);
- }
- package com.chhliu.service;
- import org.springframework.stereotype.Service;
- @Service("userInfoService")
- public class UserInfoService implements UserInfoServiceI{
- public boolean login(String username, String password, String confirmPassword){
- return (username.equals("chhliu") && password.equals("123456") && confirmPassword.equals("123456"));
- }
- }
3、编写feature文件
- #language: zh-CN
- #"zh-CN": {
- # "but": "*|但是<",
- # "and": "*|而且<|并且<|同时<",
- # "then": "*|那么<",
- # "when": "*|当<",
- # "name": "Chinese simplified",
- # "native": "简体中文",
- # "feature": "功能",
- # "background": "背景",
- # "scenario": "场景|剧本",
- # "scenario_outline": "场景大纲|剧本大纲",
- # "examples": "例子",
- # "given": "*|假如<|假设<|假定<"
- # }
- @bank
- 功能:假如我在银行取钱的时候,如果我登录成功并且输入的密码正确,那么会显示我的银行卡余额,假如余额为50万
- 场景:银行取钱
- 假如:我以"chhliu"登录
- 并且:输入的密码为"123456"
- 当:确认密码也为"123456"时
- 那么:显示银行卡余额为"500000"
4、编写测试类
- package com.chhliu.test;
- import org.junit.runner.RunWith;
- import cucumber.api.CucumberOptions;
- import cucumber.api.junit.Cucumber;
- /**
- * @RunWith(Cucumber.class) 这是一个运行器 ,指用Cucumber来运行测试
- * @CucumberOptions中的features,用于指定我们项目中要运行的feature的目录
- * @CucumberOptions中的format,用于指定我们项目中要运行时生成的报告,并指定之后可以在target目录中找到对应的测试报告
- * @CucumberOptions中的glue,用于指定项目运行时查找实现step定义文件的目录
- *
- * 在实际项目中,随着项目的进行,一个测试工程可能由多个feature文件组成,并且每个feature文件中可能也是由多个scenario组成。默认情况下,
- * 每次运行是运行所有feature中的所有scenario。这样可能导致正常情况下运行一次测试脚本,需要非常长的时间来等待测试结果。
- * 但是实际过程中,测试用例是有优先级等区分的。比如smokeTest、regressionTest等。或者有时候会有特别小部分的用例,比如等级是critical,
- * 这些用例需要长时间运行来监测系统是否没有白页或者页面404等现象。
- * 所以我们必须区分开所有的scenario,可以使我们在启动测试脚本时,可以根据我们需要来运行哪些模块的scenaro。这时我们可以使用Tags
- * 在Cucumber里Tag是直接在Feature、Scenari或Scenario Outline关键字前给feature或scenario添加任意数量的前缀为@的tags,多个tag用空格来分隔
- * @author chhliu
- *
- */
- @RunWith(Cucumber.class)
- @CucumberOptions(plugin = {"json:target/cucumber.json", "pretty"}, features = "src/test/resources")
- public class CucumberTest {
- }
5、运行测试类,并对测试输出的未定义步骤进行完善
- package com.chhliu.test;
- import javax.annotation.Resource;
- import org.junit.Assert;
- import com.chhliu.service.UserInfoServiceI;
- import cucumber.api.java.zh_cn.假如;
- import cucumber.api.java.zh_cn.当;
- import cucumber.api.java.zh_cn.那么;
- public class Cucumber集成spring {
- @Resource(name="userInfoService")
- private UserInfoServiceI service;
- private String username;
- private String password;
- private String confirmPassword;
- @假如("^:我以\"([^\"]*)\"登录$")
- public void 我以_登录(String arg1) throws Throwable {
- this.username = arg1;
- }
- @假如("^:输入的密码为\"([^\"]*)\"$")
- public void 输入的密码为(String arg1) throws Throwable {
- this.password = arg1;
- }
- @当("^:确认密码也为\"([^\"]*)\"时$")
- public void 确认密码也为_时(String arg1) throws Throwable {
- this.confirmPassword = arg1;
- }
- @那么("^:显示银行卡余额为\"([^\"]*)\"$")
- public void 显示银行卡余额为(String arg1) throws Throwable {
- boolean isLogin = service.login(username, password, confirmPassword);
- if(isLogin){
- System.out.println("登录成功!查询余额如下:"+arg1);
- Assert.assertEquals("500000", arg1);
- }
- }
- }
6、在测试步骤上添加注解支持
- @RunWith(SpringJUnit4ClassRunner.class)
- @ContextConfiguration // 不加此注解,bean会注入不进去
- @SpringBootTest // 不加此注解会找不到bean
- public class Cucumber集成spring{
- }
7、测试结果
- 2 Scenarios (2 passed)
- 11 Steps (11 passed)
- 0m0.091s
8、整合注意点
spring boot与Cucumber整合的时候,有个地方需要注意,因为spring boot提倡去xml化,所以传统方式下,Cucumber会读取classpath下的cucumber.xml配置文件来初始化bean的方式,和spring整合后,就不能用这种方式了,需要使用@ContextConfiguration注解来实现类的加载,如果是需要加载配置文件的方式的话,可以如下使用:
- @ContextConfiguration(locations = { "classpath:applicationContext.xml" })
- @ContextConfiguration(classes=SpringBootCucumberApplication.class)
- @ContextConfiguration
特别注意:@ContextConfiguration注解必加,否则会出现bean注入失败
下面,我们从源码来看下为什么会造成这种情况。
该部分涉及的代码都在cucumber-spring包下的SpringFactory类中,重点我们看下下面这个类:
- public void start() {// cucumber测试启动方法
- if (stepClassWithSpringContext != null) {// 如果使用了@ContextConfiguration注解的话,此处不为null
- testContextManager = new CucumberTestContextManager(stepClassWithSpringContext);
- } else {// 否则stepClassWithSpringContext就为null,会进入下面这个分支
- if (beanFactory == null) {
- beanFactory = createFallbackContext();// 这个方法是我们要跟的重点
- }
- }
- notifyContextManagerAboutTestClassStarted();
- if (beanFactory == null || isNewContextCreated()) {
- beanFactory = testContextManager.getBeanFactory();
- for (Class<?> stepClass : stepClasses) {
- registerStepClassBeanDefinition(beanFactory, stepClass);
- }
- }
- GlueCodeContext.INSTANCE.start();
- }
我们在来跟下createFallbackContext方法:
- private ConfigurableListableBeanFactory createFallbackContext() {
- ConfigurableApplicationContext applicationContext;
- if (getClass().getClassLoader().getResource("cucumber.xml") != null) {// 会先根据classpath下的cucumber.xml来初始化<span style="font-family:Arial, Helvetica, sans-serif;">ConfigurableApplicationContext</span>
- applicationContext = new ClassPathXmlApplicationContext("cucumber.xml");
- } else {// 如果没有配置cucumber.xml的话,会new GenericApplicationContext
- applicationContext = new GenericApplicationContext();
- }
- applicationContext.registerShutdownHook();
- ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
- beanFactory.registerScope(GlueCodeScope.NAME, new GlueCodeScope());
- for (Class<?> stepClass : stepClasses) {
- registerStepClassBeanDefinition(beanFactory, stepClass);
- }
- return beanFactory;
- }
最后,来说下GenericApplicationContext这个类,该类会根据Bean的Type类型,然后newInstance实例,但是由于这个类中又注入了其他的类,而注入的类是无法通过new实例的方式来初始化的,所以最后就会注入失败,报空指针了。