Compare commits

...

20 Commits

Author SHA1 Message Date
Tom Hombergs
a18ad84da2 working (but awkward) example of a message provider test 2018-09-10 21:41:49 +02:00
Tom Hombergs
5e93dcb84b updated messaging example for cleaner separation of publishing and converting messages 2018-08-26 22:58:14 +02:00
Tom Hombergs
3b0eb77b5d example for message provider with pact (not working yet) 2018-08-24 11:16:30 +02:00
Tom Hombergs
a9c9faba1d added example for a message consumer test with pact 2018-08-22 21:32:54 +02:00
Tom Hombergs
8672d163e2 updated spring-cloud-contract exampled to current versions 2018-08-16 22:27:41 +02:00
Tom Hombergs
3eefd80192 removed the manual truncating 2018-08-11 21:35:37 +02:00
Tom Hombergs
5a36a2365e updated pact provider example to JUnit 5 and Spring Boot 2 2018-08-11 16:47:03 +02:00
Tom Hombergs
5f675f91a8 changed logging configuration 2018-08-11 00:22:08 +02:00
Tom Hombergs
3559378d1a logging example with Spring Boot and Logback 2018-08-11 00:00:15 +02:00
Tom Hombergs
b72ab0d580 Merge pull request #8 from thombergs/cdc-updates
updated pact-feign example to Spring Boot 2 and JUnit 5
2018-08-10 00:14:21 +02:00
Tom Hombergs
ed45066496 updated pact-feign example to Spring Boot 2 and JUnit 5 2018-08-10 00:09:11 +02:00
Tom Hombergs
ddb3e08ee5 Merge pull request #7 from thombergs/logging-format
removed deprecated examples
2018-08-04 09:21:55 +02:00
Tom Hombergs
888a89e541 removed deprecated examples
added logging example
2018-08-04 09:03:43 +02:00
Tom Hombergs
fb148fcd14 added article link 2018-06-11 22:33:27 +02:00
Tom Hombergs
a07407dc9f changed CI config to skip build on docs-only-changes 2018-06-11 22:15:15 +02:00
Tom Hombergs
5e4c962b21 Merge pull request #6 from thombergs/spring-boot-testing
Spring boot testing
2018-05-27 22:31:06 +02:00
Tom Hombergs
500ee0a04f ignored test because it cannot run on CI 2018-05-27 21:55:26 +02:00
Tom Hombergs
4d67dd44d0 fix 2018-05-27 21:10:15 +02:00
Tom Hombergs
a4d70d5835 Example application for structuring a Spring Boot app 2018-05-27 20:55:24 +02:00
Tom Hombergs
f2b83425ac inital checking for spring-boot-test example project 2018-05-20 20:38:28 +02:00
143 changed files with 2562 additions and 1222 deletions

View File

@@ -0,0 +1,11 @@
<component name="libraryTable">
<library name="Gradle: org.projectlombok:lombok:1.16.20">
<CLASSES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.16.20/ac76d9b956045631d1561a09289cbf472e077c01/lombok-1.16.20.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="jar://$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.16.20/69ebf81bb97bdb3c9581c171762bb4929cb5289c/lombok-1.16.20-sources.jar!/" />
</SOURCES>
</library>
</component>

13
.idea/modules/spring-boot-testing.iml generated Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":spring-boot:spring-boot-testing" external.linked.project.path="$MODULE_DIR$/../../spring-boot/spring-boot-testing" external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="reflectoring.io" external.system.module.version="0.0.1-SNAPSHOT" type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$/../../spring-boot/spring-boot-testing">
<excludeFolder url="file://$MODULE_DIR$/../../spring-boot/spring-boot-testing/.gradle" />
<excludeFolder url="file://$MODULE_DIR$/../../spring-boot/spring-boot-testing/build" />
<excludeFolder url="file://$MODULE_DIR$/../../spring-boot/spring-boot-testing/out" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -1,5 +1,11 @@
before_install:
- chmod +x gradlew
- |
if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(.md)|^(LICENSE)'
then
echo "Not running CI since only docs were changed."
exit
fi
language: java

View File

@@ -1,15 +0,0 @@
# Consumer-Driven-Contract Test for a Feign Consumer
This repo contains an example of consumer-driven-contract testing for a Feign client
that consumes a REST API provided by the module `pact-spring-data-rest-provider`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ConsumerPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -1,46 +0,0 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
ext {
springCloudVersion = 'Dalston.SR2'
}
dependencies {
compile('org.springframework.cloud:spring-cloud-starter-feign')
// locking transitive guava version to work around "java.lang.IllegalAccessError: tried to access method com.google.common.collect.Lists.cartesianProduct"
compile('com.google.guava:guava:22.0')
compile('org.springframework.boot:spring-boot-starter-hateoas')
testCompile group: 'au.com.dius', name: 'pact-jvm-consumer-junit_2.11', version: '3.5.2'
testCompile('org.springframework.boot:spring-boot-starter-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
bootRun{
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5006"]
}

View File

@@ -1,24 +0,0 @@
package com.example.demo;
public class Address {
private long id;
private String street;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}

View File

@@ -1,19 +0,0 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "addresses", path = "/addresses")
public interface AddressClient {
@RequestMapping(method = RequestMethod.GET, path = "/")
Resources<Address> getAddresses();
@RequestMapping(method = RequestMethod.GET, path = "/{id}")
Resource<Address> getAddress(@PathVariable("id") long id);
}

View File

@@ -1,24 +0,0 @@
package com.example.demo;
public class Customer {
private long id;
private String name;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -1,19 +0,0 @@
package com.example.demo;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "customers", path = "/customers_mto")
public interface CustomerClient {
@RequestMapping(method = RequestMethod.GET, value = "/")
Resources<Customer> getCustomers();
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
Resource<Customer> getCustomer(@PathVariable("id") long id);
}

View File

@@ -1,16 +0,0 @@
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.hateoas.config.EnableHypermediaSupport;
@SpringBootApplication
@EnableFeignClients
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -1,15 +0,0 @@
package com.example.demo;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.FULL;
}
}

View File

@@ -1,17 +0,0 @@
server.port: 8081
logging.level.com.example.demo.CustomerClient: DEBUG
logging.level.com.example.demo.AddressClient: DEBUG
logging.level.org.hibernate.SQL: DEBUG
customers:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080
addresses:
ribbon:
eureka:
enabled: false
listOfServers: localhost:8080

View File

@@ -1,137 +0,0 @@
package com.example.demo;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.model.RequestResponsePact;
import org.apache.http.entity.ContentType;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"addresses.ribbon.listOfServers: localhost:8888"
})
public class ConsumerPactVerificationTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("customerServiceProvider", "localhost", 8888, this);
@Autowired
private AddressClient addressClient;
@Pact(state = "a collection of 2 addresses", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressCollectionResourcePact(PactDslWithProvider builder) {
return builder
.given("a collection of 2 addresses")
.uponReceiving("a request to the address collection resource")
.path("/addresses/")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"_embedded\": {\n" +
" \"addresses\": [\n" +
" {\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
" },\n" +
" {\n" +
" \"street\": \"High Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/2/customer\"\n" +
" }\n" +
" }\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses{?page,size,sort}\",\n" +
" \"templated\": true\n" +
" },\n" +
" \"profile\": {\n" +
" \"href\": \"http://localhost:8080/profile/addresses\"\n" +
" },\n" +
" \"search\": {\n" +
" \"href\": \"http://localhost:8080/addresses/search\"\n" +
" }\n" +
" },\n" +
" \"page\": {\n" +
" \"size\": 20,\n" +
" \"totalElements\": 2,\n" +
" \"totalPages\": 1,\n" +
" \"number\": 0\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Pact(state = "a single address", provider = "customerServiceProvider", consumer = "addressClient")
public RequestResponsePact createAddressResourcePact(PactDslWithProvider builder) {
return builder
.given("a single address")
.uponReceiving("a request to the address resource")
.path("/addresses/1")
.method("GET")
.willRespondWith()
.status(200)
.body("{\n" +
" \"street\": \"Elm Street\",\n" +
" \"_links\": {\n" +
" \"self\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"address\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1\"\n" +
" },\n" +
" \"customer\": {\n" +
" \"href\": \"http://localhost:8080/addresses/1/customer\"\n" +
" }\n" +
" }\n" +
"}", "application/hal+json")
.toPact();
}
@Test
@PactVerification(fragment = "createAddressCollectionResourcePact")
public void verifyAddressCollectionPact() {
Resources<Address> addresses = addressClient.getAddresses();
assertThat(addresses).hasSize(2);
}
@Test
@PactVerification(fragment = "createAddressResourcePact")
public void verifyAddressPact() {
Resource<Address> address = addressClient.getAddress(1L);
assertThat(address).isNotNull();
}
}

View File

@@ -1,16 +0,0 @@
# Consumer-Driven-Contract Test for a Spring Data Rest Provider
This repo contains an example of consumer-driven-contract testing for a Spring
Data REST API provider. The corresponding consumer to the contract is
implemented in the module `pact-feign-consumer`.
The contract is created and verified with [Pact](https://docs.pact.io/).
## Companion Blog Post
The Companion Blog Post to this project can be found [here](https://reflectoring.io/consumer-driven-contracts-with-pact-feign-spring-data-rest/).
## Running the application
The interesting part in this code base is the class `ProviderPactVerificationTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -1,43 +0,0 @@
package com.example.demo;
import javax.persistence.*;
@Entity
public class Address {
@GeneratedValue
@Id
private Long id;
@Column
private String street;
@ManyToOne
private Customer customer;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}

View File

@@ -1,14 +0,0 @@
package com.example.demo;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.hateoas.Resources;
import java.util.List;
@RepositoryRestResource(path = "addresses")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
List<Address> findByCustomerId(long customerId);
}

View File

@@ -1,30 +0,0 @@
package com.example.demo;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue
private long id;
@Column
private String name;
public void setId(long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

View File

@@ -1,9 +0,0 @@
package com.example.demo;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
@RepositoryRestResource(path = "customers")
public interface CustomerRepository extends CrudRepository<Customer, Long> {
}

View File

@@ -1,7 +0,0 @@
spring.datasource.url=jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
logging.level.org.hibernate.SQL=OFF

View File

@@ -1,41 +0,0 @@
package com.example.demo;
import au.com.dius.pact.provider.junit.PactRunner;
import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactFolder;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import com.example.framework.DatabaseStateHolder;
import com.example.framework.SpringBootStarter;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
@RunWith(PactRunner.class)
@Provider("customerServiceProvider")
@PactFolder("../pact-feign-consumer/target/pacts")
public class ProviderPactVerificationTest {
@ClassRule
public static SpringBootStarter appStarter = SpringBootStarter.builder()
.withApplicationClass(DemoApplication.class)
.withArgument("--spring.config.location=classpath:/application-pact.properties")
.withDatabaseState("single-address", "/initial-schema.sql", "/single-address.sql")
.withDatabaseState("address-collection", "/initial-schema.sql", "/address-collection.sql")
.build();
@State("a single address")
public void toSingleAddressState() {
DatabaseStateHolder.setCurrentDatabaseState("single-address");
}
@State("a collection of 2 addresses")
public void toAddressCollectionState() {
DatabaseStateHolder.setCurrentDatabaseState("address-collection");
}
@TestTarget
public final Target target = new HttpTarget(8080);
}

View File

@@ -1,32 +0,0 @@
package com.example.framework;
/**
* Defines a state of the database, which is defined by a set of SQL scripts.
*/
public class DatabaseState {
private final String stateName;
private final String[] sqlscripts;
/**
* Constructor.
*
* @param stateName unique name of this database state.
* @param sqlscripts paths to SQL scripts within the classpath. These scripts will be executed to put the
* database into the database state described by this object.
*/
public DatabaseState(String stateName, String... sqlscripts) {
this.stateName = stateName;
this.sqlscripts = sqlscripts;
}
public String getStateName() {
return stateName;
}
public String[] getSqlscripts() {
return sqlscripts;
}
}

View File

@@ -1,36 +0,0 @@
package com.example.framework;
/**
* Holds the current database state.
* <p/>
* TODO: replace the static state variable with a thread-safe alternative. Potentially use a special HTTP header
* that is intercepted by a Bean defined in {@link PactDatabaseStatesAutoConfiguration} and sets the database
* state in a {@link ThreadLocal} variable.
*/
public class DatabaseStateHolder {
private static String currentDatabaseState;
/**
* Sets the database to the state with the specified name.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*
* @param databaseStateName the name of the {@link DatabaseState} to put the database in.
*/
public static void setCurrentDatabaseState(String databaseStateName) {
currentDatabaseState = databaseStateName;
}
/**
* Returns the name of the current {@link DatabaseState}.
* <p/>
* <strong>WARNING:</strong> the database state is not thread safe. If there are multiple threads accessing
* the database in different states at the same time, apocalypse will come!
*/
public static String getCurrentDatabaseState() {
return currentDatabaseState;
}
}

View File

@@ -1,50 +0,0 @@
package com.example.framework;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.util.List;
/**
* Initializes a {@link DataSource} to a specified List of {@link DatabaseState}s. It is assumed that the {@link DataSource}
* can switch between several states.
*/
public class DatabaseStatesInitializer {
private final DataSource dataSource;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param dataSource the {@link DataSource} to execute SQL scripts against.
* @param databaseStates the {@link DatabaseState}s to create within the {@link DataSource}.
*/
public DatabaseStatesInitializer(DataSource dataSource, List<DatabaseState> databaseStates) {
this.dataSource = dataSource;
this.databaseStates = databaseStates;
}
/**
* Executes SQL scripts to initialize the {@link DataSource} with several states.
* <p/>
* For each {@link DatabaseState}, the {@link DatabaseStateHolder} will be called to set the {@link DataSource}
* into that state. Then, the SQL scripts of that {@link DatabaseState} are executed against the {@link DataSource}
* to initialize that state.
*/
@PostConstruct
public void initialize() {
for (DatabaseState databaseState : this.databaseStates) {
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
for (String script : databaseState.getSqlscripts()) {
populator.addScript(new ClassPathResource(script));
}
populator.execute(this.dataSource);
}
}
}

View File

@@ -1,70 +0,0 @@
package com.example.framework;
import au.com.dius.pact.provider.junit.PactRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration AutoConfiguration} that is activated when
* the pact-jvm-provider-junit module is in the classpath.
* </p>
* <p>
* This configuration provides a {@link DataSource} which allows to switch between multiple database states.
* Each database state is defined by a name and a set of SQL scripts which set the database into the desired state.
* The database states are configured via properties:
* <pre>
* pact.databaseStates.&lt;NAME&gt;=/path/to/script1.sql,/path/to/script2.sql,...
* </pre>
* The NAME of the databaseState can be used with {@link DatabaseStateHolder#setCurrentDatabaseState(String)}
* to set the {@link DataSource} into that state.
* </p>
*/
@Configuration
@ConditionalOnClass(PactRunner.class)
@EnableConfigurationProperties(PactProperties.class)
public class PactDatabaseStatesAutoConfiguration {
@Bean
public DataSource dataSource(PactProperties pactProperties) {
AbstractRoutingDataSource dataSource = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseStateHolder.getCurrentDatabaseState();
}
};
Map<Object, Object> targetDataSources = new HashMap<>();
dataSource.setTargetDataSources(targetDataSources);
// create a DataSource for each DatabaseState
for (DatabaseState databaseState : pactProperties.getDatabaseStatesList()) {
DataSource ds = DataSourceBuilder
.create()
.url(String.format("jdbc:h2:mem:%s;DB_CLOSE_ON_EXIT=FALSE", databaseState.getStateName()))
.driverClassName("org.h2.Driver")
.username("sa")
.password("")
.build();
targetDataSources.put(databaseState.getStateName(), ds);
DatabaseStateHolder.setCurrentDatabaseState(databaseState.getStateName());
}
return dataSource;
}
@Bean
public DatabaseStatesInitializer databaseStatesInitializer(DataSource routingDataSource, PactProperties pactProperties) {
return new DatabaseStatesInitializer(routingDataSource, pactProperties.getDatabaseStatesList());
}
}

View File

@@ -1,64 +0,0 @@
package com.example.framework;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Loads the properties "pact.databaseStates.&lt;NAME&gt;" into the Spring environment.
*/
@ConfigurationProperties("pact")
public class PactProperties {
private Map<String, String> databaseStates;
/**
* Retrieves a map with the names of the configured database states as keys and {@link DatabaseState} objects
* as values.
*/
public List<DatabaseState> getDatabaseStatesList() {
List<DatabaseState> databaseStatesList = new ArrayList<>();
for (Map.Entry<String, String> entry : databaseStates.entrySet()) {
String stateName = entry.getKey();
// When reading a property as a Map, as is done for databaseStates, Spring Boot automatically adds numeric
// keys and moves the actual key into the value, separated by a comma. Thus, we have all entries duplicated
// and have to remove the entries with numeric keys.
if (!stateName.matches("^[0-9]+$")) {
String sqlScriptsString = entry.getValue();
String[] sqlScripts = sqlScriptsString.split(",");
databaseStatesList.add(new DatabaseState(stateName, sqlScripts));
}
}
return databaseStatesList;
}
static List<String> toCommandLineArguments(List<DatabaseState> databaseStates) {
List<String> args = new ArrayList<>();
for (DatabaseState databaseState : databaseStates) {
String argString = String.format("--pact.databaseStates.%s=", databaseState.getStateName());
int i = 0;
for (String scriptPath : databaseState.getSqlscripts()) {
argString += String.format("%s", scriptPath);
if (i < databaseState.getSqlscripts().length - 1) {
argString += ",";
}
i++;
}
args.add(argString);
}
return args;
}
public Map<String, String> getDatabaseStates() {
return databaseStates;
}
public void setDatabaseStates(Map<String, String> databaseStates) {
this.databaseStates = databaseStates;
}
}

View File

@@ -1,111 +0,0 @@
package com.example.framework;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import java.util.ArrayList;
import java.util.List;
/**
* <p>
* Starts a Spring Boot application.
* </p>
* <p>
* When included in a JUnit test with the {@link org.junit.ClassRule} annotation as in the example below, the Spring Boot application will be
* started before any of the test methods are run.
* <pre>
* public class MyTest {
*
* &#064;ClassRule
* public static SpringBootStarter starter = SpringBootStarter.builder()
* .withApplicationClass(MyApplication.class)
* ...
* .build();
*
* &#064;Test
* public void test(){
* ...
* }
*
* }
* </pre>
* </p>
*/
public class SpringBootStarter implements TestRule {
private final Class<?> applicationClass;
private final List<String> args;
private final List<DatabaseState> databaseStates;
/**
* Constructor.
*
* @param applicationClass the Spring Boot application class.
* @param databaseStates list containing {@link DatabaseState} objects, each describing a database state
* in form of one or more SQL scripts.
* @param args the command line arguments the application should be started with.
*/
public SpringBootStarter(Class<?> applicationClass, List<DatabaseState> databaseStates, List<String> args) {
this.args = args;
this.applicationClass = applicationClass;
this.databaseStates = databaseStates;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
List<String> args = new ArrayList<>();
args.addAll(SpringBootStarter.this.args);
args.addAll(PactProperties.toCommandLineArguments(SpringBootStarter.this.databaseStates));
ApplicationContext context = SpringApplication.run(SpringBootStarter.this.applicationClass, args.toArray(new String[]{}));
base.evaluate();
SpringApplication.exit(context);
}
};
}
/**
* Creates a builder that provides a fluent API to create a new {@link SpringBootStarter} instance.
*/
public static SpringBootStarterBuilder builder() {
return new SpringBootStarterBuilder();
}
public static class SpringBootStarterBuilder {
private Class<?> applicationClass;
private List<String> args = new ArrayList<>();
private List<DatabaseState> databaseStates = new ArrayList<>();
public SpringBootStarterBuilder withApplicationClass(Class<?> clazz) {
this.applicationClass = clazz;
return this;
}
public SpringBootStarterBuilder withArgument(String argument) {
this.args.add(argument);
return this;
}
public SpringBootStarterBuilder withDatabaseState(String stateName, String... sqlScripts) {
this.databaseStates.add(new DatabaseState(stateName, sqlScripts));
return this;
}
public SpringBootStarter build() {
return new SpringBootStarter(this.applicationClass, this.databaseStates, args);
}
}
}

View File

@@ -1 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.framework.PactDatabaseStatesAutoConfiguration

View File

@@ -1,2 +0,0 @@
insert into address (id, street) values (1, 'Elm Street');
insert into address (id, street) values (2, 'High Street');

View File

@@ -1,6 +0,0 @@
server.port=8080
#spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=none
pact.databaseStates[0]=single-address,/initial-schema.sql,/single-address.sql
pact.databaseStates[1]=address-collection,/initial-schema.sql,/address-collection.sql

View File

@@ -1,11 +0,0 @@
create table CUSTOMER (
id NUMBER,
name VARCHAR
);
create table ADDRESS (
id NUMBER,
customer_id NUMBER,
street VARCHAR,
FOREIGN KEY (customer_id) REFERENCES CUSTOMER(id)
);

View File

@@ -1 +0,0 @@
insert into address (id, street) values (1, 'Elm Street');

7
logging/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Logging Code Examples
## Related Blog Articles
* [Use Logging Levels Consistently](https://reflectoring.io/logging-levels/)
* [Use a Human-Readable Logging Format](https://reflectoring.io/logging-format/)
* [Configuring a Human-Readable Logging Format with Logback and Descriptive Logger](https://reflectoring.io/logging-format-logback/)

20
logging/build.gradle Normal file
View File

@@ -0,0 +1,20 @@
apply plugin: 'java'
buildscript {
repositories {
mavenLocal()
jcenter()
}
}
repositories {
mavenLocal()
jcenter()
}
dependencies {
compile("ch.qos.logback:logback-classic:1.2.3")
compile("io.reflectoring:descriptive-logger:1.0")
testCompile("org.junit.jupiter:junit-jupiter-engine:5.0.1")
}

View File

@@ -0,0 +1,18 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.LoggerFactory;
import org.junit.jupiter.api.Test;
public class LoggingFormatTest {
private MyLogger logger = LoggerFactory.getLogger(MyLogger.class, LoggingFormatTest.class);
@Test
public void testLogPattern(){
Thread.currentThread().setName("very-long-thread-name");
logger.logDebugMessage();
Thread.currentThread().setName("short");
logger.logInfoMessage();
logger.logMessageWithLongId();
}
}

View File

@@ -0,0 +1,19 @@
package io.reflectoring.logging;
import io.reflectoring.descriptivelogger.DescriptiveLogger;
import io.reflectoring.descriptivelogger.LogMessage;
import org.slf4j.event.Level;
@DescriptiveLogger
public interface MyLogger {
@LogMessage(level=Level.DEBUG, message="This is a DEBUG message.", id=14556)
void logDebugMessage();
@LogMessage(level=Level.INFO, message="This is an INFO message.", id=5456)
void logInfoMessage();
@LogMessage(level=Level.ERROR, message="This is an ERROR message with a very long ID.", id=1548654)
void logMessageWithLongId();
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<conversionRule conversionWord="truncatedThread"
converterClass="io.reflectoring.logging.TruncatedThreadConverter" />
<conversionRule conversionWord="truncatedLogger"
converterClass="io.reflectoring.logging.TruncatedLoggerConverter" />
<!-- Appender to log to console -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd} | %d{HH:mm:ss.SSS} | %-20.20thread | %5p | %-25.25logger{25} | %12(ID: %8mdc{id}) | %m%n</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@@ -1,5 +1,3 @@
apply plugin: 'org.springframework.boot'
buildscript {
repositories {
mavenLocal()
@@ -10,18 +8,43 @@ buildscript {
}
}
plugins {
id "au.com.dius.pact" version "3.5.20"
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version '1.0.0.SNAPSHOT'
repositories {
mavenLocal()
jcenter()
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springcloud_version}"
}
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-data-jpa:${springboot_version}")
compile("org.springframework.boot:spring-boot-starter-web:${springboot_version}")
compile("org.springframework.cloud:spring-cloud-starter-feign:1.4.1.RELEASE")
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.cloud:spring-cloud-starter-openfeign')
compile('org.springframework.cloud:spring-cloud-starter-netflix-ribbon')
compile('com.h2database:h2:1.4.196')
testCompile('org.codehaus.groovy:groovy-all:2.4.6')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.11:3.5.2")
testCompile("org.springframework.boot:spring-boot-starter-test:${springboot_version}")
testCompile("au.com.dius:pact-jvm-consumer-junit5_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
pact {
publish {
pactDirectory = 'target/pacts'
pactBrokerUrl = 'URL'
pactBrokerUsername = 'USERNAME'
pactBrokerPassword = 'PASSWORD'
}
}

View File

@@ -1,2 +1,3 @@
springboot_version=1.5.9.RELEASE
verifier_version=1.2.2.RELEASE
springboot_version=2.0.4.RELEASE
springcloud_version=Finchley.SR1
pact_version=3.5.20

View File

@@ -2,10 +2,12 @@ package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
@RibbonClient(name = "userservice", configuration = RibbonConfiguration.class)
public class ConsumerApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,15 @@
package io.reflectoring;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
public class RibbonConfiguration {
@Bean
public IRule ribbonRule(IClientConfig config) {
return new RandomRule();
}
}

View File

@@ -1,6 +1,6 @@
package io.reflectoring;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;

View File

@@ -1,83 +1,85 @@
package io.reflectoring;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactProviderRuleMk2;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.consumer.dsl.PactDslWithProvider;
import au.com.dius.pact.consumer.junit5.PactConsumerTestExt;
import au.com.dius.pact.consumer.junit5.PactTestFor;
import au.com.dius.pact.model.RequestResponsePact;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.*;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = {
// overriding provider address
"userservice.ribbon.listOfServers: localhost:8888"
@ExtendWith(PactConsumerTestExt.class)
@ExtendWith(SpringExtension.class)
@PactTestFor(providerName = "userservice", port = "8888")
@SpringBootTest({
// overriding provider address
"userservice.ribbon.listOfServers: localhost:8888"
})
public class UserServiceConsumerTest {
class UserServiceConsumerTest {
@Rule
public PactProviderRuleMk2 stubProvider = new PactProviderRuleMk2("userservice", "localhost", 8888, this);
@Autowired
private UserClient userClient;
@Autowired
private UserClient userClient;
@Pact(state = "provider accepts a new person", provider = "userservice", consumer = "userclient")
RequestResponsePact createPersonPact(PactDslWithProvider builder) {
// @formatter:off
return builder
.given("provider accepts a new person")
.uponReceiving("a request to POST a person")
.path("/user-service/users")
.method("POST")
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.integerType("id", 42))
.toPact();
// @formatter:on
}
@Pact(state = "provider accepts a new person", provider = "userservice", consumer = "userclient")
public RequestResponsePact createPersonPact(PactDslWithProvider builder) {
return builder
.given("provider accepts a new person")
.uponReceiving("a request to POST a person")
.path("/user-service/users")
.method("POST")
.willRespondWith()
.status(201)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.integerType("id", 42))
.toPact();
}
@Pact(state = "person 42 exists", provider = "userservice", consumer = "userclient")
public RequestResponsePact updatePersonPact(PactDslWithProvider builder) {
return builder
.given("person 42 exists")
.uponReceiving("a request to PUT a person")
.path("/user-service/users/42")
.method("PUT")
.willRespondWith()
.status(200)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.stringType("firstName", "Zaphod")
.stringType("lastName", "Beeblebrox"))
.toPact();
}
@Pact(state = "person 42 exists", provider = "userservice", consumer = "userclient")
RequestResponsePact updatePersonPact(PactDslWithProvider builder) {
// @formatter:off
return builder
.given("person 42 exists")
.uponReceiving("a request to PUT a person")
.path("/user-service/users/42")
.method("PUT")
.willRespondWith()
.status(200)
.matchHeader("Content-Type", "application/json")
.body(new PactDslJsonBody()
.stringType("firstName", "Zaphod")
.stringType("lastName", "Beeblebrox"))
.toPact();
// @formatter:on
}
@Test
@PactVerification(fragment = "createPersonPact")
public void verifyCreatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
IdObject id = userClient.createUser(user);
assertThat(id.getId()).isEqualTo(42);
}
@Test
@PactTestFor(pactMethod = "createPersonPact")
void verifyCreatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
IdObject id = userClient.createUser(user);
assertThat(id.getId()).isEqualTo(42);
}
@Test
@PactVerification(fragment = "updatePersonPact")
public void verifyUpdatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
User updatedUser = userClient.updateUser(42L, user);
assertThat(updatedUser.getFirstName()).isEqualTo("Zaphod");
assertThat(updatedUser.getLastName()).isEqualTo("Beeblebrox");
}
@Test
@PactTestFor(pactMethod = "updatePersonPact")
void verifyUpdatePersonPact() {
User user = new User();
user.setFirstName("Zaphod");
user.setLastName("Beeblebrox");
User updatedUser = userClient.updateUser(42L, user);
assertThat(updatedUser.getFirstName()).isEqualTo("Zaphod");
assertThat(updatedUser.getLastName()).isEqualTo("Beeblebrox");
}
}

View File

@@ -0,0 +1,115 @@
{
"provider": {
"name": "userservice"
},
"consumer": {
"name": "userclient"
},
"interactions": [
{
"description": "a request to PUT a person",
"request": {
"method": "PUT",
"path": "/user-service/users/42"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"firstName": "Zaphod",
"lastName": "Beeblebrox"
},
"matchingRules": {
"body": {
"$.firstName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.lastName": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
},
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json"
}
],
"combine": "AND"
}
}
}
},
"providerStates": [
{
"name": "person 42 exists"
}
]
},
{
"description": "a request to POST a person",
"request": {
"method": "POST",
"path": "/user-service/users"
},
"response": {
"status": 201,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": 42
},
"matchingRules": {
"header": {
"Content-Type": {
"matchers": [
{
"match": "regex",
"regex": "application/json"
}
],
"combine": "AND"
}
},
"body": {
"$.id": {
"matchers": [
{
"match": "integer"
}
],
"combine": "AND"
}
}
}
},
"providerStates": [
{
"name": "provider accepts a new person"
}
]
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.20"
}
}
}

View File

@@ -0,0 +1,15 @@
# Consumer-Driven-Contract Test for a Spring Boot Provider
This repo contains an example of consumer-driven-contract testing for a Spring
Boot API provider. The corresponding consumer to the contract is
implemented in the module `pact-angular`.
The contract is created and verified with [Pact](https://docs.pact.io/).
Before running the build, you need to follow the instructions on the [consumer-side](../pact-angular/)
to create the consumer-driven contract file (pact file).
## Running the application
The interesting part in this code base is the class `UserControllerProviderTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,11 @@
spring:
datasource:
url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
logging.level.org.hibernate.SQL: OFF

View File

@@ -0,0 +1,36 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-amqp')
compile('com.h2database:h2:1.4.196')
compileOnly('org.projectlombok:lombok')
testCompile("au.com.dius:pact-jvm-consumer-junit_2.12:${pact_version}")
testCompile("au.com.dius:pact-jvm-consumer-groovy_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun {
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -0,0 +1,2 @@
springboot_version=2.0.4.RELEASE
pact_version=3.5.20

View File

@@ -0,0 +1,15 @@
package io.reflectoring;
import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@SpringBootApplication
@EnableRabbit
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,59 @@
package io.reflectoring;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessageConsumerConfiguration {
private static final String QUEUE_NAME = "myQueue";
private static final String EXCHANGE_NAME = "myExchange";
@Bean
public TopicExchange receiverExchange() {
return new TopicExchange(EXCHANGE_NAME);
}
@Bean
public Queue queue() {
return new Queue(QUEUE_NAME);
}
@Bean
public Binding binding(Queue eventReceivingQueue, TopicExchange receiverExchange) {
return BindingBuilder
.bind(eventReceivingQueue)
.to(receiverExchange)
.with("*.*");
}
@Bean
public SimpleMessageListenerContainer container(ConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(QUEUE_NAME);
container.setMessageListener(listenerAdapter);
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(UserCreatedMessageConsumer userCreatedMessageConsumer) {
return new MessageListenerAdapter(userCreatedMessageConsumer, "consumeStringMessage");
}
@Bean
public UserCreatedMessageConsumer eventReceiver(ObjectMapper objectMapper) {
return new UserCreatedMessageConsumer(objectMapper);
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class User {
@NotNull
private long id;
@NotNull
private String name;
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UserCreatedMessage {
@NotNull
private String messageUuid;
@NotNull
private User user;
}

View File

@@ -0,0 +1,37 @@
package io.reflectoring;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.io.IOException;
import java.util.Set;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserCreatedMessageConsumer {
private Logger logger = LoggerFactory.getLogger(UserCreatedMessageConsumer.class);
private ObjectMapper objectMapper;
public UserCreatedMessageConsumer(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public void consumeStringMessage(String messageString) throws IOException {
logger.info("Consuming message '{}'", messageString);
UserCreatedMessage message = objectMapper.readValue(messageString, UserCreatedMessage.class);
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<UserCreatedMessage>> violations = validator.validate(message);
if(!violations.isEmpty()){
throw new ConstraintViolationException(violations);
}
// pass message into business use case
}
}

View File

@@ -0,0 +1,59 @@
package io.reflectoring;
import java.io.IOException;
import au.com.dius.pact.consumer.MessagePactBuilder;
import au.com.dius.pact.consumer.MessagePactProviderRule;
import au.com.dius.pact.consumer.Pact;
import au.com.dius.pact.consumer.PactVerification;
import au.com.dius.pact.consumer.dsl.PactDslJsonBody;
import au.com.dius.pact.model.v3.messaging.MessagePact;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserCreatedMessageConsumerTest {
@Rule
public MessagePactProviderRule mockProvider = new MessagePactProviderRule(this);
private byte[] currentMessage;
@Autowired
private UserCreatedMessageConsumer userCreatedMessageConsumer;
@Pact(provider = "userservice", consumer = "userclient")
public MessagePact userCreatedMessagePact(MessagePactBuilder builder) {
PactDslJsonBody body = new PactDslJsonBody();
body.stringType("messageUuid");
body.object("user")
.numberType("id", 42L)
.stringType("name", "Zaphod Beeblebrox")
.closeObject();
// @formatter:off
return builder
.expectsToReceive("a user created message")
.withContent(body)
.toPact();
// @formatter:on
}
@Test
@PactVerification("userCreatedMessagePact")
public void verifyCreatePersonPact() throws IOException {
userCreatedMessageConsumer.consumeStringMessage(new String(this.currentMessage));
}
/**
* This method is called by the Pact framework.
*/
public void setMessage(byte[] message) {
this.currentMessage = message;
}
}

View File

@@ -0,0 +1,59 @@
{
"consumer": {
"name": "userclient"
},
"provider": {
"name": "userservice"
},
"messages": [
{
"description": "a user created message",
"metaData": {
"Content-Type": "application/json; charset=UTF-8"
},
"contents": {
"messageUuid": "string",
"user": {
"name": "Zaphod Beeblebrox",
"id": 42
}
},
"matchingRules": {
"body": {
"$.messageUuid": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.user.id": {
"matchers": [
{
"match": "number"
}
],
"combine": "AND"
},
"$.user.name": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.20"
}
}
}

View File

@@ -0,0 +1,15 @@
# Consumer-Driven-Contract Test for a Spring Boot Provider
This repo contains an example of consumer-driven-contract testing for a Spring
Boot API provider. The corresponding consumer to the contract is
implemented in the module `pact-angular`.
The contract is created and verified with [Pact](https://docs.pact.io/).
Before running the build, you need to follow the instructions on the [consumer-side](../pact-angular/)
to create the consumer-driven contract file (pact file).
## Running the application
The interesting part in this code base is the class `UserControllerProviderTest`.
You can run the tests with `gradlew test` on Windows or `./gradlew test` on Unix.

View File

@@ -0,0 +1,11 @@
spring:
datasource:
url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
logging.level.org.hibernate.SQL: OFF

View File

@@ -0,0 +1,35 @@
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-amqp')
compile('com.h2database:h2:1.4.196')
compileOnly('org.projectlombok:lombok')
testCompile("au.com.dius:pact-jvm-provider-junit_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}
bootRun {
jvmArgs = ["-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005"]
}

View File

@@ -0,0 +1,2 @@
springboot_version=2.0.4.RELEASE
pact_version=3.5.20

View File

@@ -0,0 +1,12 @@
package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

View File

@@ -0,0 +1,36 @@
package io.reflectoring;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
@Configuration
@EnableScheduling
class MessageProviderConfiguration {
@Bean
TopicExchange topicExchange() {
return new TopicExchange("myExchange");
}
@Bean
UserCreatedMessageProvider messageProvider(ObjectMapper objectMapper, UserCreatedMessagePublisher publisher) {
return new UserCreatedMessageProvider(objectMapper, publisher);
}
@Bean
UserCreatedMessagePublisher messagePublisher(RabbitTemplate rabbitTemplate, TopicExchange topicExchange) {
return new UserCreatedMessagePublisher(rabbitTemplate, topicExchange);
}
@Bean
SendMessageJob job(UserCreatedMessageProvider messageProvider) {
return new SendMessageJob(messageProvider);
}
}

View File

@@ -0,0 +1,37 @@
package io.reflectoring;
import java.io.IOException;
import java.util.Random;
import java.util.UUID;
import org.springframework.scheduling.annotation.Scheduled;
class SendMessageJob {
private Random random = new Random();
private UserCreatedMessageProvider messageProvider;
SendMessageJob(UserCreatedMessageProvider messageProvider) {
this.messageProvider = messageProvider;
}
/**
* This scheduled job simulates the "real" business logic that should produce messages.
*/
@Scheduled(fixedDelay = 1000)
void sendUserCreatedMessage() {
try {
UserCreatedMessage userCreatedMessage = UserCreatedMessage.builder()
.messageUuid(UUID.randomUUID().toString())
.user(User.builder()
.id(random.nextLong())
.name("Zaphpod Beeblebrox")
.build())
.build();
messageProvider.sendUserCreatedMessage(userCreatedMessage);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,18 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
class User {
@NotNull
private long id;
@NotNull
private String name;
}

View File

@@ -0,0 +1,18 @@
package io.reflectoring;
import javax.validation.constraints.NotNull;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
class UserCreatedMessage {
@NotNull
private String messageUuid;
@NotNull
private User user;
}

View File

@@ -0,0 +1,31 @@
package io.reflectoring;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Takes a {@link UserCreatedMessage}, converts it to a {@link String} and sends it to be published.
*/
class UserCreatedMessageProvider {
private Logger logger = LoggerFactory.getLogger(UserCreatedMessageProvider.class);
private ObjectMapper objectMapper;
private UserCreatedMessagePublisher userCreatedMessagePublisher;
UserCreatedMessageProvider(ObjectMapper objectMapper, UserCreatedMessagePublisher userCreatedMessagePublisher) {
this.objectMapper = objectMapper;
this.userCreatedMessagePublisher = userCreatedMessagePublisher;
}
void sendUserCreatedMessage(UserCreatedMessage message) throws IOException {
String stringMessage = objectMapper.writeValueAsString(message);
userCreatedMessagePublisher.publishMessage(stringMessage, "user.created");
logger.info("Published message '{}'", stringMessage);
}
}

View File

@@ -0,0 +1,24 @@
package io.reflectoring;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* Publishes a String message to RabbitMQ.
*/
class UserCreatedMessagePublisher {
private RabbitTemplate rabbitTemplate;
private TopicExchange topicExchange;
UserCreatedMessagePublisher(RabbitTemplate rabbitTemplate, TopicExchange topicExchange) {
this.rabbitTemplate = rabbitTemplate;
this.topicExchange = topicExchange;
}
void publishMessage(String message, String routingKey) {
rabbitTemplate.convertAndSend(topicExchange.getName(), routingKey, message);
}
}

View File

@@ -0,0 +1,35 @@
package io.reflectoring;
import java.util.List;
import au.com.dius.pact.model.Interaction;
import au.com.dius.pact.model.ProviderState;
import au.com.dius.pact.provider.ConsumerInfo;
import au.com.dius.pact.provider.ProviderInfo;
import au.com.dius.pact.provider.ProviderVerifier;
import au.com.dius.pact.provider.junit.target.AmqpTarget;
import org.jetbrains.annotations.NotNull;
public class CustomAmqpTarget extends AmqpTarget {
public CustomAmqpTarget(List<String> packagesToScan) {
super(packagesToScan);
}
@NotNull
@Override
protected ProviderVerifier setupVerifier(Interaction interaction, ProviderInfo provider, ConsumerInfo consumer) {
ProviderVerifier verifier = new CustomProviderVerifier(getPackagesToScan());
setupReporters(verifier, provider.getName(), interaction.getDescription());
verifier.initialiseReporters(provider);
verifier.reportVerificationForConsumer(consumer, provider);
if (!interaction.getProviderStates().isEmpty()) {
for (ProviderState state : interaction.getProviderStates()) {
verifier.reportStateForInteraction(state.getName(), provider, consumer, true);
}
}
verifier.reportInteractionDescription(interaction);
return verifier;
}
}

View File

@@ -0,0 +1,62 @@
package io.reflectoring;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import au.com.dius.pact.model.Interaction;
import au.com.dius.pact.model.v3.messaging.Message;
import au.com.dius.pact.provider.ConsumerInfo;
import au.com.dius.pact.provider.PactVerifyProvider;
import au.com.dius.pact.provider.ProviderInfo;
import au.com.dius.pact.provider.ProviderVerifier;
import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ConfigurationBuilder;
public class CustomProviderVerifier extends ProviderVerifier {
private List<String> packagesToScan;
public CustomProviderVerifier(List<String> packagesToScan) {
this.packagesToScan = packagesToScan;
}
@Override
public boolean verifyResponseByInvokingProviderMethods(ProviderInfo providerInfo, ConsumerInfo consumer,
Object interaction, String interactionMessage, Map failures) {
try {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
.setScanners(new MethodAnnotationsScanner())
.forPackages(packagesToScan.toArray(new String[]{}));
Reflections reflections = new Reflections(configurationBuilder);
Set<Method> methodsAnnotatedWith = reflections.getMethodsAnnotatedWith(PactVerifyProvider.class);
Set<Method> providerMethods = methodsAnnotatedWith.stream()
.filter(m -> {
PactVerifyProvider annotation = m.getAnnotation(PactVerifyProvider.class);
return annotation.value().equals(((Interaction)interaction).getDescription());
})
.collect(Collectors.toSet());
if (providerMethods.isEmpty()) {
throw new RuntimeException("No annotated methods were found for interaction " +
"'${interaction.description}'. You need to provide a method annotated with " +
"@PactVerifyProvider(\"${interaction.description}\") that returns the message contents.");
} else {
if (interaction instanceof Message) {
verifyMessagePact(providerMethods, (Message) interaction, interactionMessage, failures);
} else {
throw new RuntimeException("only supports Message interactions!");
}
}
} catch (Exception e) {
throw new RuntimeException("verification failed", e);
}
return true;
}
}

View File

@@ -0,0 +1,57 @@
package io.reflectoring;
import java.io.IOException;
import java.util.Collections;
import java.util.UUID;
import au.com.dius.pact.provider.PactVerifyProvider;
import au.com.dius.pact.provider.junit.PactRunner;
import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.loader.PactFolder;
import au.com.dius.pact.provider.junit.target.AmqpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Mockito.*;
@RunWith(PactRunner.class)
@Provider("userservice")
@PactFolder("../pact-message-consumer/target/pacts")
public class UserCreatedMessageProviderTest {
@TestTarget
public final Target target = new CustomAmqpTarget(Collections.singletonList("io.reflectoring"));
private UserCreatedMessagePublisher publisher = Mockito.mock(UserCreatedMessagePublisher.class);
private UserCreatedMessageProvider messageProvider = new UserCreatedMessageProvider(new ObjectMapper(), publisher);
@PactVerifyProvider("a user created message")
public String verifyUserCreatedMessage() throws IOException {
// given
doNothing().when(publisher).publishMessage(any(String.class), eq("user.created"));
// when
UserCreatedMessage message = UserCreatedMessage.builder()
.messageUuid(UUID.randomUUID().toString())
.user(User.builder()
.id(42L)
.name("Zaphod Beeblebrox")
.build())
.build();
messageProvider.sendUserCreatedMessage(message);
// then
ArgumentCaptor<String> messageCapture = ArgumentCaptor.forClass(String.class);
verify(publisher, times(1)).publishMessage(messageCapture.capture(), eq("user.created"));
// returning the message
return messageCapture.getValue();
}
}

View File

@@ -0,0 +1,59 @@
{
"consumer": {
"name": "userclient"
},
"provider": {
"name": "userservice"
},
"messages": [
{
"description": "a user created message",
"metaData": {
"Content-Type": "application/json; charset=UTF-8"
},
"contents": {
"messageUuid": "string",
"user": {
"name": "Zaphod Beeblebrox",
"id": 42
}
},
"matchingRules": {
"body": {
"$.messageUuid": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
},
"$.user.id": {
"matchers": [
{
"match": "number"
}
],
"combine": "AND"
},
"$.user.name": {
"matchers": [
{
"match": "type"
}
],
"combine": "AND"
}
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
},
"pact-jvm": {
"version": "3.5.20"
}
}
}

View File

@@ -1,18 +1,16 @@
buildscript {
ext {
springBootVersion = '1.5.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot_version}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
@@ -26,8 +24,7 @@ dependencies {
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-web')
compile('com.h2database:h2:1.4.196')
testCompile('au.com.dius:pact-jvm-provider-spring_2.12:3.5.11')
testCompile('junit:junit:4.12')
testCompile("au.com.dius:pact-jvm-provider-junit5_2.12:${pact_version}")
testCompile('org.springframework.boot:spring-boot-starter-test')
}

View File

@@ -0,0 +1,2 @@
springboot_version=2.0.4.RELEASE
pact_version=3.5.20

View File

@@ -26,7 +26,7 @@ public class UserController {
@PutMapping(path = "/user-service/users/{id}")
public ResponseEntity<User> updateUser(@RequestBody @Valid User user, @PathVariable long id) {
User userFromDb = userRepository.findOne(id);
User userFromDb = userRepository.findById(id).get();
userFromDb.updateFrom(user);
userFromDb = userRepository.save(userFromDb);
return ResponseEntity.ok(userFromDb);
@@ -34,7 +34,7 @@ public class UserController {
@GetMapping(path = "/user-service/users/{id}")
public ResponseEntity<User> getUser(@PathVariable("id") Long id) {
return ResponseEntity.ok(userRepository.findOne(id));
return ResponseEntity.ok(userRepository.findById(id).get());
}

View File

@@ -1,44 +1,54 @@
package io.reflectoring;
import java.util.Optional;
import au.com.dius.pact.provider.junit.Provider;
import au.com.dius.pact.provider.junit.State;
import au.com.dius.pact.provider.junit.loader.PactFolder;
import au.com.dius.pact.provider.junit.target.HttpTarget;
import au.com.dius.pact.provider.junit.target.Target;
import au.com.dius.pact.provider.junit.target.TestTarget;
import au.com.dius.pact.provider.spring.SpringRestPactRunner;
import io.reflectoring.User;
import io.reflectoring.UserRepository;
import org.junit.runner.RunWith;
import au.com.dius.pact.provider.junit5.HttpTestTarget;
import au.com.dius.pact.provider.junit5.PactVerificationContext;
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.when;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
@RunWith(SpringRestPactRunner.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = {
"server.port=8080"
})
@Provider("userservice")
@PactFolder("../pact-angular/pacts")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = {
"server.port=8080"
})
public class UserControllerProviderTest {
@MockBean
private UserRepository userRepository;
@MockBean
private UserRepository userRepository;
@TestTarget
public final Target target = new HttpTarget(8080);
@BeforeEach
void setupTestTarget(PactVerificationContext context) {
context.setTarget(new HttpTestTarget("localhost", 8080, "/"));
}
@State({"provider accepts a new person",
"person 42 exists"})
public void toCreatePersonState() {
User user = new User();
user.setId(42L);
user.setFirstName("Arthur");
user.setLastName("Dent");
when(userRepository.findOne(eq(42L))).thenReturn(user);
when(userRepository.save(any(User.class))).thenReturn(user);
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@State({"provider accepts a new person",
"person 42 exists"})
public void toCreatePersonState() {
User user = new User();
user.setId(42L);
user.setFirstName("Arthur");
user.setLastName("Dent");
when(userRepository.findById(eq(42L))).thenReturn(Optional.of(user));
when(userRepository.save(any(User.class))).thenReturn(user);
}
}

View File

@@ -7,16 +7,20 @@ include 'spring-cloud:sleuth-upstream-service'
include 'spring-cloud:spring-cloud-contract-provider'
include 'spring-cloud:spring-cloud-contract-consumer'
include 'deprecated:pact-feign-consumer' // must run before pact-spring-data-rest-provider
include 'deprecated:pact-spring-data-rest-provider'
include 'pact:pact-spring-provider'
include 'pact:pact-feign-consumer'
include 'pact:pact-message-consumer'
include 'pact:pact-message-provider'
include 'spring-boot:rabbitmq-event-brokering'
include 'spring-boot:modular:security-module'
include 'spring-boot:modular:booking-module'
include 'spring-boot:modular:application'
include 'spring-boot:spring-boot-testing'
include 'spring-boot:spring-boot-logging'
include 'logging'
include 'junit:conditions'

View File

@@ -0,0 +1,26 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

View File

@@ -0,0 +1,31 @@
buildscript {
ext {
springBootVersion = '2.0.4.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
group = 'io.reflectoring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.junit.jupiter:junit-jupiter-engine:5.2.0')
}

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#Tue Feb 06 12:27:20 CET 2018
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-bin.zip

172
spring-boot/spring-boot-logging/gradlew vendored Normal file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env sh
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save ( ) {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@@ -0,0 +1,84 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1 @@
rootProject.name = 'spring-boot-logging'

View File

@@ -0,0 +1,12 @@
package io.reflectoring.springbootlogging;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootLoggingApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootLoggingApplication.class, args);
}
}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<springProfile name="dev">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{HH:mm:ss.SSS} | %5p | %logger{25} | %m%n
</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="staging">
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{yyyy-MM-dd};%d{HH:mm:ss.SSS};%t;%5p;%logger{25};%m%n
</pattern>
<charset>utf8</charset>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,23 @@
package io.reflectoring.springbootlogging;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties =
"spring.profiles.active=dev"
)
class LoggingWithDevProfileTest {
private Logger logger = LoggerFactory.getLogger(LoggingWithDevProfileTest.class);
@Test
void test() {
logger.info("This is a test");
}
}

View File

@@ -0,0 +1,23 @@
package io.reflectoring.springbootlogging;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties =
"spring.profiles.active=staging"
)
class LoggingWithStagingProfileTest {
private Logger logger = LoggerFactory.getLogger(LoggingWithStagingProfileTest.class);
@Test
void test() {
logger.info("This is a test");
}
}

View File

@@ -0,0 +1,21 @@
package io.reflectoring.springbootlogging;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class LoggingWithoutProfileTest {
private Logger logger = LoggerFactory.getLogger(LoggingWithoutProfileTest.class);
@Test
void test() {
logger.info("This is a test");
}
}

View File

@@ -1,4 +1,4 @@
package com.example.demo;
package io.reflectoring.springbootlogging;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -7,7 +7,7 @@ import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoApplicationTests {
public class SpringBootLoggingApplicationTests {
@Test
public void contextLoads() {

View File

@@ -0,0 +1,27 @@
.gradle
/build/
!gradle/wrapper/gradle-wrapper.jar
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
/out/
### NetBeans ###
/nbproject/private/
/build/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

View File

@@ -0,0 +1,4 @@
# Structuring a Spring Boot App into Modules and Layers
## Companion Blog Article
The companion blog article to this repository can be found [here](https://reflectoring.io/testing-verticals-and-layers-spring-boot/).

Some files were not shown because too many files have changed in this diff Show More