Skip to content

Commit 7adeb0b

Browse files
author
jbwheatley
authored
Merge pull request #211 from solarmosaic-kflorence/test-stubs-190
Fixes #190 / #203: allow control over when stub server starts and stops.
2 parents 5588325 + 827398e commit 7adeb0b

File tree

4 files changed

+164
-26
lines changed

4 files changed

+164
-26
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.example.consumer
2+
3+
import com.itv.scalapact.{ScalaPactMockConfig, ScalaPactMockServer}
4+
import com.itv.scalapact.model.ScalaPactDescription
5+
import org.json4s.DefaultFormats
6+
import org.json4s.native.Serialization._
7+
import org.scalatest.{BeforeAndAfterAll, FunSpec, Matchers}
8+
9+
/** Stands up the stub service with all stubs prior to running tests and shuts it down afterwards. */
10+
class SingletonStubProviderClientSpec extends FunSpec with Matchers with BeforeAndAfterAll {
11+
12+
// The import contains two things:
13+
// 1. The consumer test DSL/Builder
14+
// 2. Helper implicits, for instance, values will automatically be converted
15+
// to Option types where the DSL requires it.
16+
import com.itv.scalapact.ScalaPactForger._
17+
18+
// Import the json and http libraries specified in the build.sbt file
19+
import com.itv.scalapact.circe13._
20+
import com.itv.scalapact.http4s21._
21+
22+
implicit val formats: DefaultFormats.type = DefaultFormats
23+
24+
val CONSUMER = "scala-pact-consumer"
25+
val PROVIDER = "scala-pact-provider"
26+
27+
val people = List("Bob", "Fred", "Harry")
28+
29+
val body: String = write(
30+
Results(
31+
count = 3,
32+
results = people
33+
)
34+
)
35+
36+
// Forge all pacts up front
37+
val pact: ScalaPactDescription = forgePact
38+
.between(CONSUMER)
39+
.and(PROVIDER)
40+
.addInteraction(
41+
interaction
42+
.description("Fetching results")
43+
.given("Results: Bob, Fred, Harry")
44+
.uponReceiving("/results")
45+
.willRespondWith(200, Map("Pact" -> "modifiedRequest"), body)
46+
)
47+
.addInteraction(
48+
interaction
49+
.description("Fetching least secure auth token ever")
50+
.uponReceiving(
51+
method = GET,
52+
path = "/auth_token",
53+
query = None,
54+
headers = Map("Accept" -> "application/json", "Name" -> "Bob"),
55+
body = None,
56+
matchingRules = // When stubbing (during this test or externally), we don't mind
57+
// what the name is, as long as it only contains letters.
58+
headerRegexRule("Name", "^([a-zA-Z]+)$")
59+
)
60+
.willRespondWith(
61+
status = 202,
62+
headers = Map("Content-Type" -> "application/json; charset=UTF-8"),
63+
body = Some("""{"token":"abcABC123"}"""),
64+
matchingRules = // When verifying externally, we don't mind what is in the token
65+
// as long as it contains a token field with an alphanumeric
66+
// value
67+
bodyRegexRule("token", "^([a-zA-Z0-9]+)$")
68+
)
69+
)
70+
71+
lazy val server: ScalaPactMockServer = pact.startServer()
72+
lazy val config: ScalaPactMockConfig = server.config
73+
74+
override def beforeAll(): Unit = {
75+
// Initialize the Pact stub server prior to tests executing.
76+
val _ = server
77+
()
78+
}
79+
80+
override def afterAll(): Unit = {
81+
// Shut down the stub server when tests are finished.
82+
server.stop()
83+
}
84+
85+
describe("Connecting to the Provider service") {
86+
it("should be able to fetch results") {
87+
val results = ProviderClient.fetchResults(config.baseUrl)
88+
results.isDefined shouldEqual true
89+
results.get.count shouldEqual 3
90+
results.get.results.forall(p => people.contains(p)) shouldEqual true
91+
}
92+
93+
it("should be able to get an auth token") {
94+
val token = ProviderClient.fetchAuthToken(config.host, config.port, "Sally")
95+
token.isDefined shouldEqual true
96+
token.get.token shouldEqual "abcABC123"
97+
}
98+
}
99+
}

example/consumer/src/test/scala/com/example/consumer/ProviderClientSpec.scala example/consumer/src/test/scala/com/example/consumer/StubPerTestProviderClientSpec.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import org.json4s.DefaultFormats
44
import org.json4s.native.Serialization._
55
import org.scalatest.{FunSpec, Matchers}
66

7-
class ProviderClientSpec extends FunSpec with Matchers {
7+
/** Stands up a stub service per test case. */
8+
class StubPerTestProviderClientSpec extends FunSpec with Matchers {
89

910
// The import contains two things:
1011
// 1. The consumer test DSL/Builder

scalapact-scalatest/src/main/scala/com/itv/scalapact/ScalaPactMock.scala

+39-21
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,16 @@ private[scalapact] object ScalaPactMock {
2020
test(config)
2121
}
2222

23-
def runConsumerIntegrationTest[A](
24-
strict: Boolean
25-
)(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit
26-
sslContextMap: SslContextMap,
23+
def startServer(
24+
strict: Boolean,
25+
pactDescription: ScalaPactDescriptionFinal
26+
)(implicit
27+
httpClient: IScalaPactHttpClient,
2728
pactReader: IPactReader,
2829
pactWriter: IPactWriter,
29-
httpClient: IScalaPactHttpClient,
30-
pactStubber: IPactStubber
31-
): A = {
32-
30+
pactStubber: IPactStubber,
31+
sslContextMap: SslContextMap
32+
): ScalaPactMockServer = {
3333
val interactionManager: InteractionManager = new InteractionManager
3434

3535
val protocol = pactDescription.serverSslContextName.fold("http")(_ => "https")
@@ -64,23 +64,34 @@ private[scalapact] object ScalaPactMock {
6464

6565
PactLogger.debug("> ScalaPact stub running at: " + mockConfig.baseUrl)
6666

67-
waitForServerThenTest(server, mockConfig, test, pactDescription)
67+
val mockServer = new ScalaPactMockServer(server, mockConfig)
68+
waitForServer(mockConfig, pactDescription.serverSslContextName)
69+
mockServer
70+
}
71+
72+
def runConsumerIntegrationTest[A](
73+
strict: Boolean
74+
)(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit
75+
sslContextMap: SslContextMap,
76+
pactReader: IPactReader,
77+
pactWriter: IPactWriter,
78+
httpClient: IScalaPactHttpClient,
79+
pactStubber: IPactStubber
80+
): A = {
81+
val server = startServer(strict, pactDescription)
82+
val result = configuredTestRunner(pactDescription)(server.config)(test)
83+
server.stop()
84+
result
6885
}
6986

70-
private def waitForServerThenTest[A](
71-
server: IPactStubber,
87+
private def waitForServer(
7288
mockConfig: ScalaPactMockConfig,
73-
test: ScalaPactMockConfig => A,
74-
pactDescription: ScalaPactDescriptionFinal
75-
)(implicit pactWriter: IPactWriter, httpClient: IScalaPactHttpClient): A = {
89+
serverSslContextName: Option[String]
90+
)(implicit httpClient: IScalaPactHttpClient): Unit = {
7691
@scala.annotation.tailrec
77-
def rec(attemptsRemaining: Int, intervalMillis: Int): A =
78-
if (isStubReady(mockConfig, pactDescription.serverSslContextName)) {
79-
val result = configuredTestRunner(pactDescription)(mockConfig)(test)
80-
81-
server.shutdown()
82-
83-
result
92+
def rec(attemptsRemaining: Int, intervalMillis: Int): Unit =
93+
if (isStubReady(mockConfig, serverSslContextName)) {
94+
PactLogger.debug("Stub server is ready.")
8495
} else if (attemptsRemaining == 0) {
8596
throw new Exception("Could not connect to stub at: " + mockConfig.baseUrl)
8697
} else {
@@ -118,3 +129,10 @@ private[scalapact] object ScalaPactMock {
118129
case class ScalaPactMockConfig(protocol: String, host: String, port: Int, outputPath: String) {
119130
val baseUrl: String = protocol + "://" + host + ":" + port.toString
120131
}
132+
133+
class ScalaPactMockServer(
134+
underlying: IPactStubber,
135+
val config: ScalaPactMockConfig
136+
) {
137+
def stop(): Unit = underlying.shutdown()
138+
}

scalapact-scalatest/src/main/scala/com/itv/scalapact/model/ScalaPactDescription.scala

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.itv.scalapact.model
22

33
import com.itv.scalapact.shared.IPactStubber
4-
import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig}
4+
import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig, ScalaPactMockServer}
55
import com.itv.scalapact.shared.utils.Maps._
66
import com.itv.scalapact.shared.http.{IScalaPactHttpClient, IScalaPactHttpClientBuilder, SslContextMap}
77
import com.itv.scalapact.shared.json.{IPactReader, IPactWriter}
@@ -37,9 +37,29 @@ class ScalaPactDescription(
3737
): A = {
3838
implicit val client: IScalaPactHttpClient =
3939
httpClientBuilder.build(2.seconds, sslContextName, 1)
40-
ScalaPactMock.runConsumerIntegrationTest(strict)(
41-
finalise
42-
)(test)
40+
ScalaPactMock.runConsumerIntegrationTest(strict)(finalise)(test)
41+
}
42+
43+
/** Starts the `ScalaPactMockServer`, which tests can then be run against. It is important that the server be
44+
* shutdown when no longer needed by invoking `stop()`.
45+
*/
46+
def startServer()(implicit
47+
httpClientBuilder: IScalaPactHttpClientBuilder,
48+
options: ScalaPactOptions,
49+
pactReader: IPactReader,
50+
pactWriter: IPactWriter,
51+
pactStubber: IPactStubber
52+
): ScalaPactMockServer = {
53+
implicit val client: IScalaPactHttpClient =
54+
httpClientBuilder.build(2.seconds, sslContextName, 1)
55+
val pactDescriptionFinal = finalise(options)
56+
val server = ScalaPactMock.startServer(strict, pactDescriptionFinal)
57+
if (pactDescriptionFinal.options.writePactFiles) {
58+
ScalaPactContractWriter.writePactContracts(server.config.outputPath)(pactWriter)(
59+
pactDescriptionFinal.withHeaderForSsl
60+
)
61+
}
62+
server
4363
}
4464

4565
/** Writes pacts described by this ScalaPactDescription to file without running any consumer tests

0 commit comments

Comments
 (0)