使用 Stub Runner Boot 應用程式

Spring Cloud Contract Stub Runner Boot 是一個 Spring Boot 應用程式,它公開 REST 端點以觸發訊息標籤並存取 WireMock 伺服器。

Stub Runner Boot 安全性

Stub Runner Boot 應用程式在設計上並未受到保護 - 保護它需要為所有 Stub 添加安全性,即使它們實際上並不需要。由於這是一個測試工具 - 伺服器不適用於生產環境。

預期只有受信任的客戶端才能存取 Stub Runner Boot 伺服器。您不應在不受信任的位置將此應用程式作為 Fat Jar 或 Docker Image 執行。

Stub Runner 伺服器

若要使用 Stub Runner 伺服器,請新增以下依賴項

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

然後使用 @EnableStubRunnerServer 註解類別,建置 Fat Jar,即可開始使用。

關於屬性,請參閱 Stub Runner Spring 章節。

Stub Runner 伺服器 Fat Jar

您可以從 Maven 下載獨立的 JAR (例如,針對 2.0.1.RELEASE 版本),方法是執行以下命令

$ 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

Spring Cloud CLI 專案的 1.4.0.RELEASE 版本開始,您可以執行 spring cloud stubrunner 來啟動 Stub Runner Boot。

若要傳遞組態,您可以在目前的工作目錄中、名為 config 的子目錄中或 ~/.spring-cloud 中建立 stubrunner.yml 檔案。該檔案可能類似於以下範例,用於執行本機安裝的 Stub。

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

然後,您可以從終端機視窗呼叫 spring cloud stubrunner 以啟動 Stub Runner 伺服器。它在 8750 埠上可用。

端點

Stub Runner Boot 提供兩個端點

HTTP

對於 HTTP,Stub Runner Boot 提供以下端點

  • GET /stubs:以 ivy:integer 標記法傳回所有執行中 Stub 的清單

  • GET /stubs/{ivy}:傳回給定 ivy 標記法的埠 (當呼叫端點時,ivy 也可以僅是 artifactId)

訊息傳遞

對於訊息傳遞,Stub Runner Boot 提供以下端點

  • GET /triggers:以 ivy : [ label1, label2 …​] 標記法傳回所有執行中標籤的清單

  • POST /triggers/{label}:執行具有 label 的觸發器

  • POST /triggers/{ivy}/{label}:針對給定的 ivy 標記法執行具有 label 的觸發器 (當呼叫端點時,ivy 也可以僅是 artifactId)

範例

以下範例顯示 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

使用 Stub Runner Boot 的一種方法是將其用作「冒煙測試」的 Stub 來源。「冒煙測試」是什麼意思?假設您不想將 50 個微服務部署到測試環境中,以查看您的應用程式是否運作。您已經在建置過程中執行了一套測試,但您也希望確保應用程式的封裝運作正常。您可以將應用程式部署到環境中、啟動它,並在其上執行幾個測試,以查看它是否運作。我們可以將這些測試稱為「冒煙測試」,因為它們的目的是僅檢查少數測試情境。

此方法的問題在於,如果您使用微服務,您很可能也使用服務探索工具。Stub Runner Boot 可讓您透過啟動所需的 Stub 並在服務探索工具中註冊它們來解決此問題。

現在假設我們想要啟動此應用程式,以便自動註冊 Stub。我們可以透過使用 java -jar ${SYSTEM_PROPS} stub-runner-boot-eureka-example.jar 執行應用程式來完成此操作,其中 ${SYSTEM_PROPS}

這樣一來,您部署的應用程式可以透過服務探索將請求傳送至已啟動的 WireMock 伺服器。最有可能的是,點 1 到 3 可以在 application.yml 中預設設定,因為它們不太可能變更。這樣一來,您可以在每次啟動 Stub Runner Boot 時,僅提供要下載的 Stub 清單。