This version is still in development and is not considered stable yet. For the latest stable version, please use spring-cloud-contract 4.2.0!

Using the Stub Runner Boot Application

Spring Cloud Contract Stub Runner Boot is a Spring Boot application that exposes REST endpoints to trigger the messaging labels and to access WireMock servers.

Stub Runner Boot Security

The Stub Runner Boot application is not secured by design - securing it would require to add security to all stubs even if they don’t actually require it. Since this is a testing utility - the server is not intended to be used in production environments.

It is expected that only a trusted client has access to the Stub Runner Boot server. You should not run this application as a Fat Jar or a Docker Image in untrusted locations.

Stub Runner Server

To use the Stub Runner Server, add the following dependency:

compile "org.springframework.cloud:spring-cloud-starter-stub-runner"

Then annotate a class with @EnableStubRunnerServer, build a fat jar, and it is ready to work.

For the properties, see the Stub Runner Spring section.

Stub Runner Server Fat Jar

You can download a standalone JAR from Maven (for example, for version 2.0.1.RELEASE) by running the following commands:

$ wget -O stub-runner.jar 'https://search.maven.org/remotecontent?filepath=org/springframework/cloud/spring-cloud-contract-stub-runner-boot/2.0.1.RELEASE/spring-cloud-contract-stub-runner-boot-2.0.1.RELEASE.jar'
$ java -jar stub-runner.jar --stubrunner.ids=... --stubrunner.repositoryRoot=...

Spring Cloud CLI

Starting from the 1.4.0.RELEASE version of the Spring Cloud CLI project, you can start Stub Runner Boot by running spring cloud stubrunner.

To pass the configuration, you can create a stubrunner.yml file in the current working directory, in a subdirectory called config, or in ~/.spring-cloud. The file could resemble the following example for running stubs installed locally:

Example 1. stubrunner.yml
stubrunner:
  stubsMode: LOCAL
  ids:
    - com.example:beer-api-producer:+:9876

Then you can call spring cloud stubrunner from your terminal window to start the Stub Runner server. It is available at port 8750.

Endpoints

Stub Runner Boot offers two endpoints:

HTTP

For HTTP, Stub Runner Boot makes the following endpoints available:

  • GET /stubs: Returns a list of all running stubs in ivy:integer notation

  • GET /stubs/{ivy}: Returns a port for the given ivy notation (when calling the endpoint ivy can also be artifactId only)

Messaging

For Messaging, Stub Runner Boot makes the following endpoints available:

  • GET /triggers: Returns a list of all running labels in ivy : [ label1, label2 …​] notation

  • POST /triggers/{label}: Runs a trigger with label

  • POST /triggers/{ivy}/{label}: Runs a trigger with a label for the given ivy notation (when calling the endpoint, ivy can also be artifactId only)

Example

The following example shows typical usage of Stub Runner Boot:

@SpringBootTest(classes = StubRunnerBoot, properties = "spring.cloud.zookeeper.enabled=false")
@ActiveProfiles("test")
class StubRunnerBootSpec {

	@Autowired
	StubRunning stubRunning

	@BeforeEach
	void setup() {
		RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning),
				new TriggerController(stubRunning))
	}

	@Test
	void 'should return a list of running stub servers in "full ivy port" notation'() {
		when:
			String response = RestAssuredMockMvc.get('/stubs').body.asString()
		then:
			def root = new JsonSlurper().parseText(response)
			assert root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs' instanceof Integer
	}

	@Test
	void 'should return a port on which a #stubId stub is running'() {
		given:
		def stubIds = ['org.springframework.cloud.contract.verifier.stubs:bootService:+:stubs',
				   'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs',
				   'org.springframework.cloud.contract.verifier.stubs:bootService:+',
				   'org.springframework.cloud.contract.verifier.stubs:bootService',
				   'bootService']
		stubIds.each {
			when:
				def response = RestAssuredMockMvc.get("/stubs/${it}")
			then:
				assert response.statusCode == 200
				assert Integer.valueOf(response.body.asString()) > 0
		}
	}

	@Test
	void 'should return 404 when missing stub was called'() {
		when:
			def response = RestAssuredMockMvc.get("/stubs/a:b:c:d")
		then:
			assert response.statusCode == 404
	}

	@Test
	void 'should return a list of messaging labels that can be triggered when version and classifier are passed'() {
		when:
			String response = RestAssuredMockMvc.get('/triggers').body.asString()
		then:
			def root = new JsonSlurper().parseText(response)
			assert root.'org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs'?.containsAll(["return_book_1"])
	}

	@Test
	void 'should trigger a messaging label'() {
		given:
			StubRunning stubRunning = Mockito.mock(StubRunning)
			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
		when:
			def response = RestAssuredMockMvc.post("/triggers/delete_book")
		then:
			response.statusCode == 200
		and:
			Mockito.verify(stubRunning).trigger('delete_book')
	}

	@Test
	void 'should trigger a messaging label for a stub with #stubId ivy notation'() {
		given:
			StubRunning stubRunning = Mockito.mock(StubRunning)
			RestAssuredMockMvc.standaloneSetup(new HttpStubsController(stubRunning), new TriggerController(stubRunning))
		and:
			def stubIds = ['org.springframework.cloud.contract.verifier.stubs:bootService:stubs', 'org.springframework.cloud.contract.verifier.stubs:bootService', 'bootService']
		stubIds.each {
			when:
				def response = RestAssuredMockMvc.post("/triggers/$it/delete_book")
			then:
				assert response.statusCode == 200
			and:
				Mockito.verify(stubRunning).trigger(it, 'delete_book')
		}

	}

	@Test
	void 'should throw exception when trigger is missing'() {
		when:
		BDDAssertions.thenThrownBy(() -> RestAssuredMockMvc.post("/triggers/missing_label"))
		.hasMessageContaining("Exception occurred while trying to return [missing_label] label.")
		.hasMessageContaining("Available labels are")
		.hasMessageContaining("org.springframework.cloud.contract.verifier.stubs:loanIssuance:0.0.1-SNAPSHOT:stubs=[]")
		.hasMessageContaining("org.springframework.cloud.contract.verifier.stubs:bootService:0.0.1-SNAPSHOT:stubs=")
	}

}

Stub Runner Boot with Service Discovery

One way to use Stub Runner Boot is to use it as a feed of stubs for “smoke tests”. What does that mean? Assume that you do not want to deploy 50 microservices to a test environment in order to see whether your application works. You have already run a suite of tests during the build process, but you would also like to ensure that the packaging of your application works. You can deploy your application to an environment, start it, and run a couple of tests on it to see whether it works. We can call those tests “smoke tests”, because their purpose is to check only a handful of testing scenarios.

The problem with this approach is that, if you use microservices, you most likely also use a service discovery tool. Stub Runner Boot lets you solve this issue by starting the required stubs and registering them in a service discovery tool.

Now assume that we want to start this application so that the stubs get automatically registered. We can do so by running the application with java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar, where ${SYSTEM_PROPS}.

That way, your deployed application can send requests to started WireMock servers through service discovery. Most likely, points 1 through 3 could be set by default in application.yml, because they are not likely to change. That way, you can provide only the list of stubs to download whenever you start the Stub Runner Boot.