Search code examples
spring-bootcucumberspring-profilesspring-context

Spring Cucumber ActiveProfiles annotation not working with CucumberContext


I'm working on a project where we have a component which consists:

  • core
  • connector to external system 1
  • connector to external system 2

The connectors are mutually exlusive (if connector1 is active, connector2 is always inactive and vice versa). The core and a single connector are autowired on startup of the ApplicationContext. Which connector is instantiated is based on a value in the spring application properties.

We're writing integration tests using spring-cucumber (v6.2.2). For each external system, I want to run a set of cucumber tests. I've created 2 testsets using annotations on the cucumber scenario's which allows me to seperate the tests for connector1 and connector2.

The problem I'm having is that I need both testsets to run with a different spring profile, so I can use a different configuration. I can't find how to do this.

Current implementation (with a single profile):

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.3.4.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-java</artifactId>
    <version>6.2.2</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-junit</artifactId>
    <version>6.2.2</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>io.cucumber</groupId>
    <artifactId>cucumber-spring</artifactId>
    <version>6.2.2</version>
    <scope>test</scope>
</dependency>

CucumberConnector1IT.java

package omitted.for.company.rules;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(
        features = { "classpath:feature/" },
        glue = { "omitted.for.company.rules.cucumber.step" },
        plugin = { "pretty", "json:target/cucumber-report/cucumber.json",
                "html:target/cucumber-report/cucumber.html" },
        tags = "@Connector1 and not @ignore" // tags make sure only applicable tests are run
)
public class CucumberConnector1IT {
}

CucumberConnector2IT.java

package omitted.for.company.rules;

import io.cucumber.junit.Cucumber;
import io.cucumber.junit.CucumberOptions;
import org.junit.runner.RunWith;

@RunWith(Cucumber.class)
@CucumberOptions(
        features = { "classpath:feature/" },
        glue = { "omitted.for.company.rules.cucumber.step" },
        plugin = { "pretty", "json:target/cucumber-report/cucumber.json",
                "html:target/cucumber-report/cucumber.html" },
        tags = "@Connector2 and not @ignore" // tags make sure only applicable tests are run
)
public class CucumberConnector2IT {
}

StepInitializer.java

package omitted.for.company.rules.steps;

import io.cucumber.java.Before;
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("test") // I can only get this to work if the @ActiveProfiles and @CucumberContextConfiguration annotations are on the same class
@CucumberContextConfiguration
public class StepInitializer {

    // mock some beans to use in cucumber test
    @MockBean
    private EventProducer eventProducer; 
    @MockBean
    private RestInterface restInterface;

    @Before
    public void setup() {
    }
}

So far everything works. But what I need now is to put the @ActiveProfiles() annotation on a different class than the @CucumberContextConfiguration. If I can do this then I can annotate the correct step classes with the required profiles.

Problem is that I don't understand the spring annotations well enough to know which ones I can move and which ones I cannot. I found this example of exactly what I'm trying to do (spring-cucumber-profiles repo, notice the location of the @ActiveProfiles annotation here) . Unfortunately, it uses an older version of cucumber-spring (v5.6.0). That version doesn't yet have the @CucumberContextConfiguration annotation and does some magic with the spring context according to the documentation (Release notes of cucumber-spring). I tried to checkout the example repo and upgrade it to v6.2.2 but couldn't get it working with the new version.

If anyone spots what I'm doing wrong in my own examples, or has the possibility to get the example repo working with version 6.2.2 of cucumber-spring that would be much appreciated.

Thanks in advance! :)


Solution

  • I've solved the issue by separating the packages a bit, and creating separate StepInitializer classes for both testsets.

    Current setup:

    Test runner:

    package omitted.for.company.rules;
    
    import io.cucumber.junit.Cucumber;
    import io.cucumber.junit.CucumberOptions;
    import org.junit.runner.RunWith;
    
    @RunWith(Cucumber.class)
    @CucumberOptions(
            features = { "classpath:feature/" },
            extraGlue = { "omitted.for.company.rules.cucumber.step.common" }, // used extraGlue instead of glue
            plugin = { "pretty", "json:target/cucumber-report/cucumber.json",
                    "html:target/cucumber-report/cucumber.html" },
            tags = "@Connector1 and not @ignore" // tags make sure only applicable tests are run
    )
    public class CucumberConnector1IT {
    }
    

    Context Configuration:

    package omitted.for.company.rules.steps;
    
    import io.cucumber.java.Before;
    import io.cucumber.spring.CucumberContextConfiguration;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.test.context.ActiveProfiles;
    
    @SpringBootTest
    @ActiveProfiles({ "test", "test-connector1" })
    @CucumberContextConfiguration
    public class Connector1StepInitializer {
    
        // mock some beans to use in cucumber test
        @MockBean
        private EventProducer eventProducer; 
        @MockBean
        private RestInterface restInterface;
    
        @Autowired
        private ApplicationContext applicationContext;
    
        @Autowired
        private Environment environment;
    
        @Before
        public void setup() {
            assertThat(applicationContext).isNotNull();
            assertThat(environment.getActiveProfiles()).containsOnly("test","test-connector1");
        }
    }
    

    Both connectors/test runners have their own runner class and their own ContextConfiguration class.

    It's very important that the classes containing the @CucumberContextConfiguration annotation are not in the shared glue package (as provided in the extraGlue property in the @CucumberOptions annotation).

    Package structure looks like this:

    ├───common
    │   └───step                                // Contains shared steps. This path should be in the 'extraGlue' field of the runner classes
    ├───connector1
    │   │   CucumberConnector1IT.java           // Runner 1
    │   └───step
    │           Connector1Steps.java            // Specific steps
    │           Connector1StepInitializer.java  // has @ActiveProfiles and @CucumberContextConfiguration annotations, use to mock beans
    └───connector2
        │   CucumberConnector1IT.java           // Runner 2
        └───step
                Connector2Steps.java            // Specific steps
                Connector2StepInitializer.java  // has @ActiveProfiles and @CucumberContextConfiguration annotations, use to mock beans
    

    This way I can still use different spring profiles :).