Skip to content

Commit

Permalink
Merge pull request #211 from solarmosaic-kflorence/test-stubs-190
Browse files Browse the repository at this point in the history
Fixes #190 / #203: allow control over when stub server starts and stops.
  • Loading branch information
jbwheatley authored Feb 15, 2021
2 parents 5588325 + 827398e commit 7adeb0b
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.example.consumer

import com.itv.scalapact.{ScalaPactMockConfig, ScalaPactMockServer}
import com.itv.scalapact.model.ScalaPactDescription
import org.json4s.DefaultFormats
import org.json4s.native.Serialization._
import org.scalatest.{BeforeAndAfterAll, FunSpec, Matchers}

/** Stands up the stub service with all stubs prior to running tests and shuts it down afterwards. */
class SingletonStubProviderClientSpec extends FunSpec with Matchers with BeforeAndAfterAll {

// The import contains two things:
// 1. The consumer test DSL/Builder
// 2. Helper implicits, for instance, values will automatically be converted
// to Option types where the DSL requires it.
import com.itv.scalapact.ScalaPactForger._

// Import the json and http libraries specified in the build.sbt file
import com.itv.scalapact.circe13._
import com.itv.scalapact.http4s21._

implicit val formats: DefaultFormats.type = DefaultFormats

val CONSUMER = "scala-pact-consumer"
val PROVIDER = "scala-pact-provider"

val people = List("Bob", "Fred", "Harry")

val body: String = write(
Results(
count = 3,
results = people
)
)

// Forge all pacts up front
val pact: ScalaPactDescription = forgePact
.between(CONSUMER)
.and(PROVIDER)
.addInteraction(
interaction
.description("Fetching results")
.given("Results: Bob, Fred, Harry")
.uponReceiving("/results")
.willRespondWith(200, Map("Pact" -> "modifiedRequest"), body)
)
.addInteraction(
interaction
.description("Fetching least secure auth token ever")
.uponReceiving(
method = GET,
path = "/auth_token",
query = None,
headers = Map("Accept" -> "application/json", "Name" -> "Bob"),
body = None,
matchingRules = // When stubbing (during this test or externally), we don't mind
// what the name is, as long as it only contains letters.
headerRegexRule("Name", "^([a-zA-Z]+)$")
)
.willRespondWith(
status = 202,
headers = Map("Content-Type" -> "application/json; charset=UTF-8"),
body = Some("""{"token":"abcABC123"}"""),
matchingRules = // When verifying externally, we don't mind what is in the token
// as long as it contains a token field with an alphanumeric
// value
bodyRegexRule("token", "^([a-zA-Z0-9]+)$")
)
)

lazy val server: ScalaPactMockServer = pact.startServer()
lazy val config: ScalaPactMockConfig = server.config

override def beforeAll(): Unit = {
// Initialize the Pact stub server prior to tests executing.
val _ = server
()
}

override def afterAll(): Unit = {
// Shut down the stub server when tests are finished.
server.stop()
}

describe("Connecting to the Provider service") {
it("should be able to fetch results") {
val results = ProviderClient.fetchResults(config.baseUrl)
results.isDefined shouldEqual true
results.get.count shouldEqual 3
results.get.results.forall(p => people.contains(p)) shouldEqual true
}

it("should be able to get an auth token") {
val token = ProviderClient.fetchAuthToken(config.host, config.port, "Sally")
token.isDefined shouldEqual true
token.get.token shouldEqual "abcABC123"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import org.json4s.DefaultFormats
import org.json4s.native.Serialization._
import org.scalatest.{FunSpec, Matchers}

class ProviderClientSpec extends FunSpec with Matchers {
/** Stands up a stub service per test case. */
class StubPerTestProviderClientSpec extends FunSpec with Matchers {

// The import contains two things:
// 1. The consumer test DSL/Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ private[scalapact] object ScalaPactMock {
test(config)
}

def runConsumerIntegrationTest[A](
strict: Boolean
)(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit
sslContextMap: SslContextMap,
def startServer(
strict: Boolean,
pactDescription: ScalaPactDescriptionFinal
)(implicit
httpClient: IScalaPactHttpClient,
pactReader: IPactReader,
pactWriter: IPactWriter,
httpClient: IScalaPactHttpClient,
pactStubber: IPactStubber
): A = {

pactStubber: IPactStubber,
sslContextMap: SslContextMap
): ScalaPactMockServer = {
val interactionManager: InteractionManager = new InteractionManager

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

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

waitForServerThenTest(server, mockConfig, test, pactDescription)
val mockServer = new ScalaPactMockServer(server, mockConfig)
waitForServer(mockConfig, pactDescription.serverSslContextName)
mockServer
}

def runConsumerIntegrationTest[A](
strict: Boolean
)(pactDescription: ScalaPactDescriptionFinal)(test: ScalaPactMockConfig => A)(implicit
sslContextMap: SslContextMap,
pactReader: IPactReader,
pactWriter: IPactWriter,
httpClient: IScalaPactHttpClient,
pactStubber: IPactStubber
): A = {
val server = startServer(strict, pactDescription)
val result = configuredTestRunner(pactDescription)(server.config)(test)
server.stop()
result
}

private def waitForServerThenTest[A](
server: IPactStubber,
private def waitForServer(
mockConfig: ScalaPactMockConfig,
test: ScalaPactMockConfig => A,
pactDescription: ScalaPactDescriptionFinal
)(implicit pactWriter: IPactWriter, httpClient: IScalaPactHttpClient): A = {
serverSslContextName: Option[String]
)(implicit httpClient: IScalaPactHttpClient): Unit = {
@scala.annotation.tailrec
def rec(attemptsRemaining: Int, intervalMillis: Int): A =
if (isStubReady(mockConfig, pactDescription.serverSslContextName)) {
val result = configuredTestRunner(pactDescription)(mockConfig)(test)

server.shutdown()

result
def rec(attemptsRemaining: Int, intervalMillis: Int): Unit =
if (isStubReady(mockConfig, serverSslContextName)) {
PactLogger.debug("Stub server is ready.")
} else if (attemptsRemaining == 0) {
throw new Exception("Could not connect to stub at: " + mockConfig.baseUrl)
} else {
Expand Down Expand Up @@ -118,3 +129,10 @@ private[scalapact] object ScalaPactMock {
case class ScalaPactMockConfig(protocol: String, host: String, port: Int, outputPath: String) {
val baseUrl: String = protocol + "://" + host + ":" + port.toString
}

class ScalaPactMockServer(
underlying: IPactStubber,
val config: ScalaPactMockConfig
) {
def stop(): Unit = underlying.shutdown()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.itv.scalapact.model

import com.itv.scalapact.shared.IPactStubber
import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig}
import com.itv.scalapact.{ScalaPactContractWriter, ScalaPactMock, ScalaPactMockConfig, ScalaPactMockServer}
import com.itv.scalapact.shared.utils.Maps._
import com.itv.scalapact.shared.http.{IScalaPactHttpClient, IScalaPactHttpClientBuilder, SslContextMap}
import com.itv.scalapact.shared.json.{IPactReader, IPactWriter}
Expand Down Expand Up @@ -37,9 +37,29 @@ class ScalaPactDescription(
): A = {
implicit val client: IScalaPactHttpClient =
httpClientBuilder.build(2.seconds, sslContextName, 1)
ScalaPactMock.runConsumerIntegrationTest(strict)(
finalise
)(test)
ScalaPactMock.runConsumerIntegrationTest(strict)(finalise)(test)
}

/** Starts the `ScalaPactMockServer`, which tests can then be run against. It is important that the server be
* shutdown when no longer needed by invoking `stop()`.
*/
def startServer()(implicit
httpClientBuilder: IScalaPactHttpClientBuilder,
options: ScalaPactOptions,
pactReader: IPactReader,
pactWriter: IPactWriter,
pactStubber: IPactStubber
): ScalaPactMockServer = {
implicit val client: IScalaPactHttpClient =
httpClientBuilder.build(2.seconds, sslContextName, 1)
val pactDescriptionFinal = finalise(options)
val server = ScalaPactMock.startServer(strict, pactDescriptionFinal)
if (pactDescriptionFinal.options.writePactFiles) {
ScalaPactContractWriter.writePactContracts(server.config.outputPath)(pactWriter)(
pactDescriptionFinal.withHeaderForSsl
)
}
server
}

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

0 comments on commit 7adeb0b

Please sign in to comment.