I’ve been recently coding some Cucumber and Spring based tests. Integrating Spring with Cucumber can be a little tricky so I decided to share my findings in this blog post.
Please see GitHub: https://github.com/crossminds/cucumber-examples for a complete working sample project.
For technologies used I assume Java 8, Spring 4+, Maven and Cucumber 3+. Let’s start by coding a service interface – the object under test:
1 2 3 4 5 6 7 8 9 10 |
/** * The calculator service business interface. */ public interface CalculatorService { int sum(int a, int b); int crazyOperation(int a, int b, int c); } |
And an implementation:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Service public class CalculatorServiceImpl implements CalculatorService { public int sum(int a, int b) { return a + b; } public int crazyOperation(int a, int b, int c) { return a * b - c; } } |
It’s a really simple service :-). The service instance is a Spring managed bean. The Spring Java application context class is also pretty straightforward:
1 2 3 4 |
@Configuration @ComponentScan( "com.crossminds.cucumberexample" ) public class ApplicationConfiguration { } |
Now let’s do some more interesting stuff. Cucumber based Java tests typically contain these components:
- Feature files – text files (human readable) that Cucumber interprets to execute the tests. In true TDD these files would come first. They can also be coded by a tester while a developer is working on the glue part.
- Steps classes – they contain Cucumber glue and the actual testing logic.
- If using Spring there’s typically also a Spring test context configuration class. This is strictly speaking not necessary but it can be really useful if you need some test specific Spring managed beans (e.g. a testing context).
- A Runner class – the class that combines all previous components together and executes the tests.
Our feature file looks like this:
1 2 3 4 5 6 7 8 9 |
Feature: Calculator tests Scenario: Test Calculator sum When Test that sum(1, 2) equals 3 And Test that sum(0, 0) equals 0 Scenario: Test Calculator crazyOperation When Test that crazyOperation(1, 2, 3) equals -1 |
There are two scenarios testing the two business methods of the Calculator service. The tests are really rather simple ;-).
The corresponding Steps class looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/** * This is the Steps class containing Cucumber glue for testing the Calculator service. * * If a class contains glue annotations or it implements <code>En<code/> the Cucumber will introspect it and evaluate Spring annotations. * We also don't have to annotate the Steps class with Spring annotations like <code>@Component</code>. */ public class CalculatorSteps implements En { private final CalculatorService calculatorService; @Autowired public CalculatorSteps(CalculatorService calculatorService) { this.calculatorService = calculatorService; When("Test that sum\\({int}, {int}) equals {int}", this::testSum); When("Test that crazyOperation\\({int}, {int}, {int}) equals {int}", this::testCrazyOperation); } private void testSum(int argument1, int argument2, int expectedResult) { assertThat(calculatorService.sum(argument1, argument2)).isEqualTo(expectedResult); } private void testCrazyOperation(int argument1, int argument2, int argument3, int expectedResult) { assertThat(calculatorService.crazyOperation(argument1, argument2, argument3)).isEqualTo(expectedResult); } } |
Now this is getting more interesting as there are a couple of important details:
- The Steps class implements
En
– an interface provided by Cucumber that makes Cucumber introspect it. This interface also defines many useful annotations and methods for defining the glue. - This assures that Spring annotations (for instance
@Autowired
) are interpreted correctly by Cucumber. - The instance of the Calculator business service is injected into the Steps class for testing.
The Spring application context for tests is really simple:
1 2 3 4 5 6 |
/** * Tests Spring application context configuration. */ @Import(ApplicationConfiguration.class) public class TestConfiguration { } |
The most interesting component is the Cucumber runner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * The Cucumber tests runner. * * It's important for the class to implement <code>GlueBase</code> for Cucumber to process the <code>@ContextConfiguration</code> annotation. */ @RunWith(Cucumber.class) @CucumberOptions(glue = { "com.crossminds.cucumberexample.configuration", "com.crossminds.cucumberexample.feature", "com.crossminds.cucumberexample.service.calculator"}, features = { "classpath:/calculator" }) @ContextConfiguration(classes = TestConfiguration.class) public class RunCucumberTest implements GlueBase { } |
The important details here:
- The Runner class implements
GlueBase
(a super interface ofEn
). This causes Cucumber to process the Spring@ContextConfiguration
annotation which references the Spring testing application context. - The glue path contains the Steps classes and the Runner class. It’s important that the Runner class be also contained on the glue path as otherwise the Spring annotations wouldn’t be processed by Cucumber.
That’s all! With this setup Spring context will be processed as expected and dependency injection will work properly.