Skip to content

Commit

Permalink
generate dependents from PACT contracts
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Mar 10, 2024
1 parent de5f24c commit e1dda1a
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 35 deletions.
34 changes: 18 additions & 16 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,35 @@ The `build` of this service includes a test that generates a `.json` file for th

== How Does It Work?

The principal is rather simple:
_Gather data during test / build and combine that data in a seperate task._
The principle is relatively straightforward: _gather data during testing / building and combine this data in a Gradle task._

Tests for classes that act as representatives of dependencies (e.g.
The application's description is explicitly defined in a test, allowing for the generation of all hard-coded descriptions from one place: link:src/test/kotlin/application/ArchitectureDocumentationTests.kt[ArchitectureDocumentationTests]

The _dependencies_ of this application are generated by the tests of the classes that serve as proxies for these dependencies.
For example,
link:src/test/kotlin/application/external/BackendService2ClientTests.kt[BackendService2ClientTests] for
link:src/main/kotlin/application/external/BackendService2Client.kt[BackendService2Client] or
link:src/test/kotlin/application/persistence/SomeStoreTests.kt[SomeStoreTests] for
link:src/main/kotlin/application/persistence/SomeStore.kt[SomeStore]
)
will generate `.json` descriptions of those dependencies.
In addition, those tests collect information about the called endpoints of HTTP dependencies and store them separately as `.jsonl` (JSON List) files.
link:src/main/kotlin/application/external/BackendService2Client.kt[BackendService2Client], or
link:src/test/kotlin/application/persistence/AuditLogRepositoryTests.kt[AuditLogRepositoryTests] for
link:src/main/kotlin/application/persistence/AuditLogRepository.kt[AuditLogRepository].
Additionally, tests for HTTP-based dependencies gather information about the endpoints called from the WireMock interactions.

The _dependents_ of this application are generated from PACT contracts, as seen in link:src/test/kotlin/application/ContractTests.kt[ContractTests].
Their descriptions are derived based on the contract's consumer name.
This mapping process will throw an exception if there is no description available for a new consumer.
Failing the test in cases of "unmapped" consumers ensures the list of dependents is accurate and less prone to errors.

The _dependents_ of this application are currently hard-coded in a higher level test.
Their `.json` files, together with the information about the application itself, is generated by the higher-level
link:src/test/kotlin/application/ArchitectureDocumentationTests.kt[ArchitectureDocumentationTests].
For those not utilizing PACT, dependents can be manually specified, akin to the method used for describing the application.

All the collected data is stored as JSON in the `build/architecture-documentation` folder.
All collected data is stored in JSON format within the `build/architecture-documentation` folder.

During the Gradle `build` this data is then processed by a custom task to generate the overall application description JSON file.
This is the file, which will be pushed to the documentation repository.
During the Gradle `build`, this data is processed by a custom task designed to generate the comprehensive application description JSON file.
This file is then uploaded to the documentation repository.

== Future Improvements

As of now, this is just the beginning.
There are several possible sources of data, to make the generated application description more dynamic and complete:

* We could use the information from PACT or Spring Cloud Contracts contract files to generate the list of _dependents_.
* We could involve parts of the application configuration to enrich the data about 3rd party systems (e.g. hostnames).
* For databases, we could include the type of SQL database based on the used JDBC driver.
* For NoSQL databases, we could detect which ones are used based on dependencies and configuration properties.
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ dependencies {
implementation("com.fasterxml.jackson.core:jackson-core")
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
testImplementation("au.com.dius.pact.provider:junit5:4.6.7")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.wiremock:wiremock-standalone:3.4.1")
}
Expand Down
19 changes: 19 additions & 0 deletions src/main/kotlin/application/api/SearchController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package application.api

import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/search")
class SearchController {

@PostMapping
fun post(@RequestBody request: SearchRequest): List<SearchResult> {
return emptyList()
}

data class SearchRequest(val userName: String?)
data class SearchResult(val id: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package application.persistence
import org.springframework.stereotype.Component

@Component
class SomeStore {
class AuditLogRepository {

fun save(something: Any) {
TODO("some kind of implementation")
Expand Down
18 changes: 1 addition & 17 deletions src/test/kotlin/application/ArchitectureDocumentationTests.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package application

import application.documentation.ArchitectureDocumentation.createOrReplaceApplication
import application.documentation.ArchitectureDocumentation.createOrReplaceDependent
import application.documentation.ApplicationDescription
import application.documentation.ArchitectureDocumentation.createOrReplaceApplication
import application.documentation.ComponentType.BACKEND
import application.documentation.ComponentType.FRONTEND
import application.documentation.DependentDescription
import application.documentation.Distance.OWNED
import org.junit.jupiter.api.Test

Expand All @@ -24,17 +21,4 @@ class ArchitectureDocumentationTests {
)
)
}

@Test
fun `generate dependents description`() {
createOrReplaceDependent(
DependentDescription(
id = "frontend",
groupId = "application",
systemId = "platform",
type = FRONTEND,
distanceFromUs = OWNED,
)
)
}
}
67 changes: 67 additions & 0 deletions src/test/kotlin/application/ContractTests.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package application

import application.documentation.ArchitectureDocumentation.createOrReplaceDependent
import application.documentation.ComponentType
import application.documentation.DependentDescription
import application.documentation.Distance
import au.com.dius.pact.provider.IConsumerInfo
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 au.com.dius.pact.provider.junitsupport.Provider
import au.com.dius.pact.provider.junitsupport.State
import au.com.dius.pact.provider.junitsupport.VerificationReports
import au.com.dius.pact.provider.junitsupport.loader.PactFolder
import org.apache.hc.core5.http.HttpRequest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
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.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort

@TestInstance(PER_CLASS)
@Provider("backend-service-1")
@PactFolder("src/test/pacts")
@VerificationReports("console")
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ExtendWith(PactVerificationInvocationContextProvider::class)
internal class ContractTests {

@BeforeEach
fun setTarget(context: PactVerificationContext, @LocalServerPort port: Int) {
context.target = HttpTestTarget("localhost", port)
}

@TestTemplate
fun `consumer contract tests`(context: PactVerificationContext, request: HttpRequest) {
createOrReplaceDependent(dependentDescription(context.consumer))
context.verifyInteraction()
}

@State("search returns empty result")
fun noOpStates() {
// nothing to do
}

private fun dependentDescription(consumer: IConsumerInfo): DependentDescription =
when (consumer.name) {
"frontend" -> DependentDescription(
id = "frontend",
groupId = "application",
systemId = "platform",
type = ComponentType.FRONTEND,
distanceFromUs = Distance.OWNED
)

"external-service-x" -> DependentDescription(
id = "external-service-x",
type = ComponentType.BACKEND,
distanceFromUs = Distance.EXTERNAL
)

else -> error("Unmapped dependent [${consumer.name}]! Please add a mapping here.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import application.documentation.DependencyDescription
import application.documentation.Distance.OWNED
import org.junit.jupiter.api.Test

class SomeStoreTests {
class AuditLogRepositoryTests {

private val description = DependencyDescription(
id = "backend-service-1-database",
Expand Down
43 changes: 43 additions & 0 deletions src/test/pacts/external-service-x-backend-service-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"consumer": {
"name": "external-service-x"
},
"interactions": [
{
"description": "execute simple search #1",
"providerStates": [
{
"name": "search returns empty result"
}
],
"request": {
"headers": {
"Accept": "application/json"
},
"method": "POST",
"path": "/search",
"body": {
"userName": "[email protected]"
}
},
"response": {
"body": [],
"headers": {
"Content-Type": "application/json"
},
"status": 200
}
}
],
"metadata": {
"pact-jvm": {
"version": "4.6.7"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "backend-service-1"
}
}
68 changes: 68 additions & 0 deletions src/test/pacts/frontend-backend-service-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"consumer": {
"name": "frontend"
},
"interactions": [
{
"description": "execute simple search #1",
"providerStates": [
{
"name": "search returns empty result"
}
],
"request": {
"headers": {
"Accept": "application/json"
},
"method": "POST",
"path": "/search",
"body": {
"userName": "[email protected]"
}
},
"response": {
"body": [],
"headers": {
"Content-Type": "application/json"
},
"status": 200
}
},
{
"description": "execute simple search #2",
"providerStates": [
{
"name": "search returns empty result"
}
],
"request": {
"headers": {
"Accept": "application/json"
},
"method": "POST",
"path": "/search",
"body": {
"userName": "[email protected]"
}
},
"response": {
"body": [],
"headers": {
"Content-Type": "application/json"
},
"status": 200
}
}
],
"metadata": {
"pact-jvm": {
"version": "4.6.7"
},
"pactSpecification": {
"version": "3.0.0"
}
},
"provider": {
"name": "backend-service-1"
}
}

0 comments on commit e1dda1a

Please sign in to comment.