spring - Spring Boot 与 Cucumber 集成
问题描述
我一直在开发一个 Spring Boot 应用程序,我想用 Cucumber 进行测试(用于业务驱动的开发)。我可以使用我的代码成功地运行黄瓜测试,或者我可以启动 spring 上下文,但我似乎无法找到同时进行这两项的方法。我环顾四周,发现了一些似乎都是有这个工作。我遵循与他们相同的前提,即(1)有一个类加载弹簧上下文,(2)有另一个类作为发现黄瓜测试的入口点,最后,(3)一个包含步骤的类使用第一号中引用的上下文扩展类的定义。
这是我的课
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.testcontainers.containers.MySQLContainer;
import com.connor.Application;
/**
* This class should take care of initializing the test container (for the database) and populating it using the
* sql files that run against the actual db, in the actual order they execute. This guarantees these will run
* before we even use a staging region. Once the database is created, it boots the spring context the spring context
* and configures the environment with the db-related info from the test container.
*
* When ran alone, this test successfully performs the above steps and can be used for integration tests.
* However, when this class is extended by other classes (the glue code classes), it does not boot the spring context
* and hence do not work for integration tests.
* @author connor
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = Application.class)
@ContextConfiguration(initializers = SpringContextConfigForCukeUsage.Initializer.class)
//@Ignore //even with this, still need a test method to compile
public /* abstract */ class SpringContextConfigForCukeUsage { //with or without abstract, this doesn't seem to work . . .
/**
* Use with String.format to build the base url string when testing locally.
*/
private static final String LOCAL_BASE_URL_FORMAT_STR = "%s:%d";
/**
* Create the mysql test container as a class rule.
*/
@ClassRule
public static MySQLContainer<?> mysqlDb = new MySQLContainer<>("mysql:latest") //TODO may not want the latest version . . .
.withInitScript("schema.sql");
/**
* The base url for the tests. By passing in here, we allow for testing actual environments, as well as integration tests with the same code.
* If no value is passed, defaults to localhost, and will be combined with port to build base url
*/
@Value("${BASE_URL:locahost}")
private String baseUrl;
/**
* For getting access to the port the spring boot context is running on in subclasses
*/
@LocalServerPort
protected int serverPort;
//Decide whether to use rest template or test res template here
/**
* Used in subclasses to get access to the base url for testing.
* @return
*/
protected String getBaseUrl() {
String baseUrlToReturn;
//if it is local, append the port
if(baseUrl != null && baseUrl.contains("localhost")){
baseUrlToReturn = String.format(LOCAL_BASE_URL_FORMAT_STR, baseUrl, serverPort);
}
//otherwise, return the environment variable, which will always be available on standard http(s) port
else {
baseUrlToReturn = baseUrl;
}
return baseUrlToReturn;
}
/**
* This initalizer class takes care of setting the properties from the test container into environment variables.
* This allows integration tests to be as close to real testing as possible.
* @author connor
*
*/
public static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
//add the url, username, and password to the test context before it starts
TestPropertyValues.of(String.format("spring.datasource.url=%s", mysqlDb.getJdbcUrl()),
String.format("spring.datasource.username=%s", mysqlDb.getUsername()),
String.format("spring.datasource.password=%s", mysqlDb.getPassword()))
.applyTo(applicationContext.getEnvironment());
}
}
/**
* This method is only here to test when running alone for debugging . . .
*/
@Test
public void test() {
//just here for compilation when testing this class by itself-- will not execute if no test here
}
}
这是第二类,具有黄瓜测试入口点的类
import org.junit.runner.RunWith;
import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
/**
* This is the entry point for running the cucumber tests.
* @author connor
*
*/
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/it/resources/AddingAssessment.feature")
public class RunCukesIT {
//intentionally blank -- annotations do all the work
}
最后,这是我的第三堂课,其中包含我的步骤定义:
import cucumber.api.java8.En;
/**
* This class contains the glue code for the "given when then" step definitions.
* This extends the class that bots the spring context, but it does not boot the
* context
* @author connor
*
*/
//even with these annotations, it does not load spring boot context
//@RunWith(SpringRunner.class)
//@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, classes = Application.class)
//@ContextConfiguration(initializers = SpringContextConfigForCukeUsage.Initializer.class)
public class CukeGWT extends SpringContextConfigForCukeUsage implements En {
public CukeGWT() {
Given("we are attempting to add a test for a book", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
System.out.println("\n\n\n\n\n\n\n\n\n\n\n\n" + serverPort); //always 0 (because context doesn't load)
//
System.out.println("Env property is " + System.getenv("local.server.port")); //this also indicates server doesn't start
});
Given("the book has isbn {int}", (Integer int1) -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the author of the book has first name conn", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the author of the book has last name b", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the book has {int} questions", (Integer int1) -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("one of the questions has incorrect answers a hen, a cat", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the question has correct answer a dog", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
When("the api is called", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the api will return a valid response", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the isbn will match", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the author's first name will match", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the author's last name will match", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the question added above will exist", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the book will be unverified", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the author of the book has first name test", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the author of the book has last name testing", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("one of the questions has incorrect answers true", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("the question has correct answer false", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Given("an assessment with valid data is generated", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the api will return an error response", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the http status code will be {int}", (Integer int1) -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
Then("the error message will contain A test already exists for this book", () -> {
// Write code here that turns the phrase above into concrete actions
//throw new cucumber.api.PendingException();
});
}
}
这是我的 pom 供参考的依赖项/版本
<?xml version="1.0" encoding="UTF-8"?>
https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 2.1.3.RELEASE com.connor reading-comprehension-api 0.0 .1-SNAPSHOT reading-comprehension-api 这是用于阅读理解的容器化网络服务
<properties>
<!-- miscellaneous properties -->
<docker.image.prefix>connordocker</docker.image.prefix>
<java.version>1.8</java.version>
<!-- get as much info as we can locally for development -->
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
<!-- versions, please keep in alphabetical order -->
<cucumber.version>4.2.5</cucumber.version>
<testcontainer.version>1.12.4</testcontainer.version>
</properties>
<dependencies>
<!-- bring this in once database is integrated and defined in properties
file -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!-- lets us connect to mysql db -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- lets us use test containers; must use same version as mysql test container
below -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainer.version}</version>
<scope>test</scope>
</dependency>
<!-- mysql specific test container; must use same version as testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainer.version}</version>
<scope>test</scope>
</dependency>
<!-- the below three dependencies are required for cucumber tests -->
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java8</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<version>${cucumber.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- create different profiles for each ci/cd stage -->
<profiles>
<!-- this is ran for every commit -->
<profile>
<id>commit-profile</id>
<activation>
<!-- if we make this active by default, it runs, even when we explicitly
mention another profile -->
<!-- active by default -->
<!-- <activeByDefault>true</activeByDefault> -->
</activation>
<build>
<!-- TODO add compile plugin, surefire plugin here -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<!-- inherit surefire plugin for running unit tests. Overwrite and add
additional configuration to ignore our integration tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- ignore the integration tests (named *IT) which come from src/it/java,
but are placed on classpath by the build helper plugin (IT must be on test
classpath to run, but we don't want them running here) -->
<excludes>
<exclude>**/*IT.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<!-- this is ran on the merge to master -->
<profile>
<id>merge-to-base-branch-profile</id>
<!-- active when stage is merge -Dstage=merge -->
<activation>
<property>
<name>stage</name>
<value>merge</value>
</property>
</activation>
<!-- TODO add enforcer plugin, failsafe plugin, codehaus plugin -->
</profile>
<!-- this is ran and triggered by a tag -->
<profile>
<id>deploy-profile</id>
<!-- active when stage is deploy -Dstage=deploy, should be triggered by
tag from pipeline -->
<activation>
<property>
<name>stage</name>
<value>deploy</value>
</property>
</activation>
<!-- not sure if we want to (or can) trigger deployment to cloud from
maven, or if we can -->
</profile>
<!-- this profile is triggered for running integration tests -->
<profile>
<id>integration-test-profile</id>
<!-- make to skip unit tests so we don't get confused -->
<properties>
<skipTests>true</skipTests>
</properties>
<build>
<plugins>
<!-- this plugin adds folders to the classpath. As of right now, it
takes care of adding the integration tests (and feature files) to test classpath. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<!-- this execution adds the src/it/java to test classpath -->
<execution>
<id>add-integration-test-source</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-source</goal>
</goals>
<configuration>
<sources>
<source>src/it/java</source>
</sources>
</configuration>
</execution>
<!-- this execution adds the src/it/resources to test classpath -->
<execution>
<id>add-integration-test-resource</id>
<phase>generate-test-sources</phase>
<goals>
<goal>add-test-resource</goal>
</goals>
<configuration>
<resources>
<resource>
<directory>src/it/resources</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<!-- this runs the integration tests -->
<!-- NOTE: I have been considering using this for e2e tests once deployment
is done to staging region. This would allow us to reuse code, with the only
change being the base url system property passed in. However, we still have
to deal with populating the region database, and the fact we want our integration
tests to be much more extensive than our e2e tests, which should be very
brief -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<id>integration-tests</id>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<!-- this is needed so we can skip unit tests here, but not integration tests (as both default to skip tests property, which we set to true to ignore unit tests) -->
<skipTests>false</skipTests> <!-- NOTE: In future here, could configure with ${skip.failsafe.tests} or any other variable but skip tests -->
<!-- only run integration tests, which should end in IT -->
<includes>
<include>**/*IT.java</include>
</includes>
<excludes>
<exclude>**/*Test.java</exclude>
</excludes>
<!-- set the environment variable from what is passed in as a system
variable. This allows use with spring boot property injection NOTE: there
is no need to set the jdbc related info here, this comes from test container -->
<environmentVariables>
<!-- set the base url; if not set defaults to localhost for int
testing -->
<BASE_URL>${BASE_URL}</BASE_URL>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
<!-- this plugin builds a pretty(ier) html report from the results of
cucumber tests. This should only be active in this profile, after int tests
have ran -->
<plugin>
<groupId>net.masterthought</groupId>
<artifactId>maven-cucumber-reporting</artifactId>
<version>2.8.0</version>
<!-- we have one execution tag per functionality exposed by the api -->
<executions>
<!-- for the add integration tests -->
<execution>
<id>addCucumberReport</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<projectName>CucumberWebGui</projectName>
<outputDirectory>${project.build.directory}/cucumber-report-html/add</outputDirectory>
<cucumberOutput>${project.build.directory}/cucumber-report/addCucumber.json</cucumberOutput>
</configuration>
</execution>
<!-- for get integration tests -->
<execution>
<id>getCucumberReport</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<projectName>CucumberWebGui</projectName>
<outputDirectory>${project.build.directory}/cucumber-report-html/get</outputDirectory>
<cucumberOutput>${project.build.directory}/cucumber-report/getCucumber.json</cucumberOutput>
</configuration>
</execution>
<!-- for the verify/udpate reports -->
<execution>
<id>updateCucumberReport</id>
<phase>verify</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<projectName>CucumberWebGui</projectName>
<outputDirectory>${project.build.directory}/cucumber-report-html/update</outputDirectory>
<cucumberOutput>${project.build.directory}/cucumber-report/verifyCucumber.json</cucumberOutput>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<!-- NOTE: this is the build tag. Every plugin specified here will always
run, regardless of if a profile is selected, or if there are multiple profiles
selected. -->
<build>
<plugins>
<!-- This is the default spring boot plugin that wraps as jar and allows
running with embedded instance -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>unpack</id>
<phase>package</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<!-- display active profile in compile phase -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-help-plugin</artifactId>
<executions>
<execution>
<id>show-profiles</id>
<phase>compile</phase>
<goals>
<goal>active-profiles</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
可以在以下位置找到源代码:https ://gitlab.com/connorbutch/reading-comprehension-api
解决方案
推荐阅读
- ios - 如何对 largeTitleTextAttributes 颜色属性执行平滑过渡
- sql - DB Cursor FETCH NEXT 可以工作,但 FETCH FIRST 不能
- python - 计算列的平均值 - TypeError:“方法”对象不可下标
- wpf - 如何在 GMap.NET 中使用 NetTopologySuite 的 R-Tree 显示丰富的标记 WPF
- javascript - 使用页面上的添加按钮在 codeigniter 中添加另一个 ckeditor
- vue.js - 尝试编辑使用 vue 编写的其他人的代码:{{ }} 小胡子内的属性值在哪里?
- c++ - STL 容器如何跟踪容器的当前大小超过总大小?
- javascript - 当我使用 Javascript 添加新的 CSS 样式时,那里的 JS 停止工作
- java - J内部框架组件未初始化
- java - 如何在 android 中为 textview/edittext 添加星号红色标记,使其成为必填字段?