Cucumber with Spring Boot 1.4: Dependencies not in

2020-06-02 06:22发布

问题:

I am writing a new app and trying to do BDD using cucumber and Spring Boot 1.4. Working code is as shown below:

@SpringBootApplication
public class Application {
    @Bean
    MyService myService() {
        return new MyService();
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

public class MyService {}

Test code is as shown below:

@RunWith(Cucumber.class)
public class RunFeatures {}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class, loader = SpringApplicationContextLoader.class)
public class MyStepDef {
    @Autowired
    MyService myService;

    @Given("^Some initial condition$")
    public void appIsStarted() throws Throwable {
        if (service == null) throw new Exception("Dependency not injected!");
        System.out.println("App started");
    }

    @Then("^Nothing happens$")
    public void thereShouldBeNoException() throws Throwable {
        System.out.println("Test passed");
    }
}

Feature file is as shown below:

Feature: Test Cucumber with spring
    Scenario: First Scenario
        Given Some initial condition
        Then Nothing happens

When I run the above as is, all works well and dependency (MyService) is injected into MyStepDef with no issues.

If I replace this code:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class, loader = SpringApplicationContextLoader.class)

With the code below (New way to handle it in Spring Boot 1.4):

@RunWith(SpringRunner.class)
@SpringBootTest

Then the dependency (MyService) never gets injected. Am I missing something perhaps?

Thanks in advance for your help!!!

回答1:

I had the same problem. The comment from above directed me to the solution

The problematic code in cucumber-spring seems to be this github.com/cucumber/cucumber-jvm/blob/master/spring/src/main‌​/…

After adding the annotation @ContextConfiguration the tests are working as expected.

So what i've got is the following...

@RunWith(Cucumber.class)
@CucumberOptions(plugin = {"json:target/cucumber.json", "pretty"}, features = "src/test/features")
public class CucumberTest {
}

@ContextConfiguration
@SpringBootTest
public abstract class StepDefs {
}

public class MyStepDefs extends StepDefs {

    @Inject
    Service service;

    @Inject
    Repository repository;

    [...]

}

I hope this helps you further



回答2:

I got it working with Spring Boot 1.5.x and 2.0 and then wrote a blog post to try to clarify this since it's tricky.

First, even if it's obvious, you need to have the right dependencies included in your project (being cucumber-spring the important one here). For example, with Maven:

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>2.3.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>2.3.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-spring</artifactId>
    <version>2.3.1</version>
    <scope>test</scope>
</dependency>

Now, the important part to make it work, summarized:

  • The entry point to your test should be a class annotated with @RunWith(Cucumber.class.
  • This class will use the steps definitions, which are normally in a separated class with annotated methods (@Given, @When, @Then, etc.).
  • The trick is that this class should extend a base class annotated with @SpringBootTest, @RunWith(SpringRunner.class) and any other configuration you need to run your test with Spring Boot. For instance, if you're implementing an integration test without mocking other layers, you should add the webEnvironment configuration and set it to RANDOM_PORT or DEFINED_PORT.

See the diagram and the code skeleton below.

The entry point:

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources/features/bag.feature", plugin = {"pretty", "html:target/cucumber"})
public class BagCucumberIntegrationTest {
}

The Spring Boot base test class:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class SpringBootBaseIntegrationTest {
}

The step definitions class:

@Ignore
public class BagCucumberStepDefinitions extends SpringBootBaseIntegrationTest {
  // @Given, @When, @Then annotated methods
}

This is what you need to make DI work. For the full code example, just check my blog post or the code in GitHub.



回答3:

Prior to Spring Boot 1.4 you can use

@ContextConfiguration(classes = {YourSpringConfiguration.class}, loader = SpringApplicationContextLoader.class)

From Spring Boot 1.4 onwards SpringApplicationContextLoader is deprecated so you should use SpringBootContextLoader.class instead

Really just adding @SpringBootTest (with an optional configuration class) should work on its own, but if you look at the code in cucumber.runtime.java.spring.SpringFactory method annotatedWithSupportedSpringRootTestAnnotations it's not checking for that annotation, which is why simply adding that annotation in conjunction with @SpringBootTest works.

Really the code in cucumber-spring needs to change. I'll see if I can raise an issue as in the Spring docs it states that SpringApplicationContextLoader should only be used if absolutely necessary.I'll try and raise an issue for this for the cucumber spring support.

So as it stands stripwire's answer using a combination of @SpringBootTest and @ContextConfiguration is the best workaround.



回答4:

This is my configuration, you can try it

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@ContextConfiguration(classes = {Application.class})


回答5:

I've got it working in Spring Boot 1.5. I want to share the configuration with you:

pom.xml

<?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"
        ...
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

        ...
    <dependencies>
            ...
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-java</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-spring</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>info.cukes</groupId>
            <artifactId>cucumber-junit</artifactId>
            <version>1.2.5</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
        ...
</project>

Feature file

Feature: User should be greeted

  Background:
    Given The database is empty
    Then All connections are set

  Scenario: Default user is greeted
    Given A default user
    When The application is started
    Then The user should be greeted with "Hello Marc!"

Cucumber hook

@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources", strict = true)
public class CucumberTests {    // Classname should end on *Tests

}

Abstract Spring Configuration

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration
abstract class AbstractSpringConfigurationTest {

}

Glue

class CucumberGlue : AbstractSpringConfigurationTest() {

    @Autowired
    lateinit var restTemplate: TestRestTemplate

    @Autowired
    lateinit var restController: RestController

    @Autowired
    lateinit var personRepository: PersonRepository

    @Autowired
    lateinit var entityManager: EntityManager

    private var result: String? = null

    @Given("^The database is empty$")
    fun the_database_is_empty() {
        personRepository.deleteAll()
    }

    @Then("^All connections are set$")
    fun all_connections_are_set() {
        assertThat(restTemplate).isNotNull()
        assertThat(entityManager).isNotNull()
    }

    @Given("^A default user$")
    fun a_default_user() {
    }

    @When("^The application is started$")
    fun the_application_is_started() {
        result = restController.testGet()
    }

    @Then("^The user should be greeted with \"([^\"]*)\"$")
    fun the_user_should_be_greeted_with(expectedName: String) {
        assertThat(result).isEqualTo(expectedName)
    }

}