首页 > 解决方案 > 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

标签: springspring-bootcucumberbddspring-boot-test

解决方案


推荐阅读