There is an interesting issue with the interaction between springboot and the maven failsafe plugin. Normally both of these plugins have no problems but often in springboot we want to repackage into an executable springboot jar. The normal way to do this is,
Tuesday, 1 December 2020
SpringBoot repackaging and maven.failsafe.plugin
Cucumber set up for SpringBoot and JUnit 4 or JUnit 5
Cucumber has worked well with pure JUnit 4 projects for quite some time. The transition to JUnit 5 does change things slightly but the change isn't particularly bad once things are set up. Here are the versions that I've been using for these tests
SpringBoot: 2.4.0
Cucumber: 6.9.0
maven.surefire.plugin: 2.22.2
maven.failsafe.plugin: 2.22.2
JUnit4
pom.xml
The Pom must include the dependencies as follows (versions have been dropped off here)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-java</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.cucumber</groupId>
<artifactId>cucumber-spring</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
CucumberIT.java
The CucumberIT class is the bootstrap part of the cucumber testing. We see here the JUnit4 @RunWith. The feature files are set here to be in the src/tests/resources/features directory.
import io.cucumber.junit.CucumberOptions;
import io.cucumber.junit.Cucumber;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
tags = "",
plugin = {"pretty", "json:target/cucumber.json"})
public class CucumberIT {}
CucumberSpringContext.java
This is where the spring wiring gets done to make sure that SpringBoot starts. I've also included @AutoConfigureMockMvc for making rest calls to the spring boot service from the Step definitions but you don't have to include that. If you have additional spring configuration you can add @Beans into this class but you need to include the @ContextConfiguration spring annotation too. In previous versions of cucumber before the @CucumberContextConfiguration annotation was available you needed to have a blank cucumber @Before in this class to make sure it was found.
In fact once this is set up it stays the same for JUnit 4 and 5 because it is primarily a spring configuration not a Cucumber one!
import io.cucumber.spring.CucumberContextConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class CucumberContextTestConfiguration {}
Feature file and properties
As previously mentioned the feature files now go into the src/test/resources/features directory which is referenced in the CucumberIT. No other properties are necessary for JUnit4
Running with JUnit5
The transition to JUnit5 can happen in two stages. Firstly just running with JUnit5 can be backwards compatible with all the current JUnit4 annotations and setup with minimal changes. Here are the changes that need to be made.
Also check out details about the surefire and failsafe plugins which have had problems with JUnit5 before versions 2.22.0 because they can't find JUnit5 tests.
pom.xml
The only differences here are that the junit:junit:4 dependency comes out and the JUnit5 dependencies come in. Make sure you include the junit-vintage-engine which is what provides the backwards compatibility for JUnit4 annotations, imports etc
CucumberIT.java
This stays the same
Feature file and properties
The feature file stays as was because it is still directly referenced by the CucumberIT class. However, there is now a warning from Cucumber about the cucumber report. This can be removed by including a new file cucumber.properties in src/test/resources
cucumber.properties
Full JUnit5
Now for fully transitioning to run cucumber with JUnit 5. The test discovery mechanism has changed between JUnit4 and JUnit5 which is the reason for much of the following change. There are no longer any @CucumberOptions so these have to be specified in property files instead.
pom.xml
The junit-vantage-engine dependency has gone and the cucumber-java dependency is replaced with a JUnit5 specific one. Again the specific versions have been ignored here
CucumberIT.java
The JUnit4 annotations are no longer available so we use the new JUnit5 annotation - note the different import path.
Feature file and properties
By default the feature files need to be in the same package as the CucumberIT class (the class annotated with @Cucumber) so they are moved. The cucumber.properties file that we introduced earlier now has to be renamed to junit-platform.properties (but it stays in src/test/resources). Also, because the @CucumberOptions no longer exists we can include the plugin options here too
junit-platform.properties
Friday, 1 March 2019
AWS, Spring, Localstack
Localstack: An implementation of AWS which runs locally with natively or in a docker container
TestContainers: A java library that lets a docker container be run locally for testing
Here TestContainers is used to start the localstack docker image so that the AWS calls can be made against it.
Maven dependencies
Using the v2 dependencies for the AWS library requires bringing in the AWS bom (bill of materials) so that any dependency can just be declared and the bom takes care of getting the correct versions of each dependency. In the example below the S3 and SQS dependencies are configured.Dependency management & dependencies
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.4.11</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>sqs</artifactId>
</dependency>
Test dependencies:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.10.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<version>1.10.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cloud.localstack</groupId>
<artifactId>localstack-utils</artifactId>
<version>0.1.18</version>
<scope>test</scope>
</dependency>
AWS Configuration
The normal spring configuration for aws clients is very straight forward. Here is an example of an S3Client and an SqsClient using the AWS v2 objects.import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.sqs.SqsClient;
@Configuration
public class AwsConfiguration {
@Bean
public S3Client s3Client(){
return S3Client.builder().region(Region.EU_WEST_1).build();
}
@Bean
public SqsClient sqsClient(){
return SqsClient.builder().region(Region.EU_WEST_1).build();
}
}
These objects will use the default AwsCredentialProvider but this can be overridden here.
TestConfiguration
To create the test configuration we need to start Localstack using TestContainers. This test configuration starts the Localstack and then uses it to configure the S3Client and SqsClient to point to localstackimport static org.testcontainers.containers.localstack.LocalStackContainer.Service.S3;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.SQS;
import java.net.URI;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.containers.wait.strategy.DockerHealthcheckWaitStrategy;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProviderChain;
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider;
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
import software.amazon.awssdk.auth.credentials.InstanceProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
import software.amazon.awssdk.services.sqs.SqsClient;
import software.amazon.awssdk.services.sqs.model.CreateQueueRequest;
@TestConfiguration
public class AwsConfigurationTest {
@Bean
public LocalStackContainer localStackContainer() {
LocalStackContainer localStackContainer = new LocalStackContainer().withServices(SQS, S3);
localStackContainer.start();
return localStackContainer;
}
@Bean
public S3Client s3Client() {
final S3Client client = S3Client.builder()
.endpointOverride(URI.create(localStackContainer().getEndpointConfiguration(S3).getServiceEndpoint()))
.build();
client.createBucket(CreateBucketRequest.builder().bucket("test_bucket").build());
return client;
}
@Bean
public SqsClient sqsClient() {
final SqsClient sqs = SqsClient.builder()
.endpointOverride(URI.create(localStackContainer().getEndpointConfiguration(SQS).getServiceEndpoint()))
.build();
sqs.createQueue(CreateQueueRequest.builder().queueName("test_queue").build());
return sqs;
}
}
Tuesday, 6 June 2017
Spring Sleuth
spring.application.name=My Server
Maven
<dependency>
The trace and span ids will now be created and can be seen in the headers.
To log this and make it useful though is one thing but there is a graphical tool which makes this very easy.
Zipkin
Zipkin can be configured so that all Sleuth output is sent there and it allows a view of the interactions so that the times and services called can be seen. To configure and use zipkin just add another dependency,</dependency>
By default everything is logged to localhost:9411 but this can be changed by adding a property
spring.zipkin.baseurl=http://zipkin:9411/
Docker
If you are running with docker you'll need to add a zipkin image into the compose file,- "9411:9411"
Wednesday, 13 August 2014
@Transactional
pom.xml
This dependency is needed to allow transactions<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<scope>compile</scope>
</dependency>
Spring Context
<!-- Define a transaction manager so that the @TransactionConfiguration and @Transactional can be used -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
Proxy Class
Exception Handling
Programmatic Transactions
Friday, 20 June 2014
Spring Web App Template
Folders
Notes: In eclipse create a new java project with the correct maven folder structure. This is the sample folder structure
project_home/src
project_home/src/main
Wednesday, 30 April 2014
Spring Context Unit Testing
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:com/me/my_context.xml" })
public class SpringContextTest implements ApplicationContextAware
{
/**
* The spring context.
*/
private ApplicationContext applicationContext;
/**
* Test the spring wiring.
*/
@Test
public void testSpringWiring()
{
final Object obj = applicationContext.getBean("myBean");
assertTrue(obj instanceof MyBean);
}
/**
* {@inheritDoc}
*/
@Override
public void setApplicationContext(final ApplicationContext applicationContext)
{
this.applicationContext = applicationContext;
}
}
However, where there is interaction with a JNDI resource as part of the spring start up this needs to be modified to make the JNDI resource available. Adding this @Before method will set up a JNDI resource for a MySql database.
/**
* Set up the jndi data source so that the wiring tests still work.
*/
@BeforeClass
public static void setUpClass()
{
// Setup the jndi context and the datasource
try
{
// Create a database connection to satisfy the data loading requirements.
final DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://server:port");
dataSource.setUsername("username");
dataSource.setPassword("password");
final SimpleNamingContextBuilder builder = SimpleNamingContextBuilder.emptyActivatedContextBuilder();
builder.bind("jdbc/MY_JNDI_NAME", dataSource);
builder.activate();
}
catch (final NamingException ex)
{
LOG.error("Error creating JNDI resource", ex);
}
}
Monday, 20 January 2014
Dynamic Spring Message Driven Beans
For dynamic beans things can still be put into the spring configuration but need the destination setting at runtime. This can be done using some spring context as prototype beans so that a new instance of them is created each time and a small bit of wiring in the code.
Spring Context
<!-- it is loaded using a getBean("jmsDestination", requiredTopic") and as such the HOLDING_VALUE is -->
<!-- replaced with whatever is required by the class in question -->
<bean id="jmsDestination" class="com.ibm.mq.jms.MQTopic" scope="prototype">
<constructor-arg value="HOLDING_VALUE" />
</bean>
<!-- Generic MDB Container -->
<bean id="springMdbContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer" scope="prototype">
<property name="connectionFactory" ref="springConnectionFactory" />
<!-- The destination is dynamically set in the factory method but a default is provided here-->
<property name="destination" ref="jmsDestination" />
<!-- The normal MessageListener which is put here is added dynamically in the factory -->
</bean>
Java
In the java code a new destination can be obtained using the bean above and the code,/**
* Create a Destination object.
*
* @param topic The topic to construct the destination with
* @return the Destination created.
*/
public Destination createDestination(final String topic)
{
// Create the destination with the topic string.
return (Destination) applicationContext.getBean("jmsDestination", topic);
}
This creates a new Destination object based on the spring bean. This allows the MQ provided to be changed in the spring configuration without having to change code.
To create a new MDB create a destination as above and then use the code,
/**
* Create a MDB wired by spring.
*
* @param destination The destination topic to listen to.
* @param messageListener The message listener (MDB) to wire
*/
private void wireMdb(final Destination destination, final MessageListener messageListener)
{
// Use spring to wire up the MDB with this destination and a ConnectionFactory
// which is already provided in the spring context.
// The MDB and Destination are set here and then the container is manually started.
final DefaultMessageListenerContainer container =
(DefaultMessageListenerContainer) applicationContext.getBean("springMdbContainer");
container.setDestination(destination);
container.setMessageListener(messageListener);
container.start();
}
It is also possible to change the destination that a container is listening to once it has started. Using the setDestination(...) method does this according to the spring documentation.