Skip to content

Commit

Permalink
Move from mockito to scalamock (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
RustedBones committed Apr 9, 2024
1 parent b02fcca commit 38a61a9
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 72 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ lazy val `pekko-http-metrics-core` = (project in file("core"))
Dependencies.PekkoHttp,
Dependencies.Provided.PekkoStream,
Dependencies.Test.Logback,
Dependencies.Test.Mockito,
Dependencies.Test.PekkoHttpTestkit,
Dependencies.Test.PekkoSlf4j,
Dependencies.Test.PekkoStreamTestkit,
Dependencies.Test.ScalaMock,
Dependencies.Test.ScalaTest
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ import org.apache.pekko.http.scaladsl.server.{RequestContext, RouteResult}
import org.apache.pekko.stream.scaladsl.Keep
import org.apache.pekko.stream.testkit.scaladsl.{TestSink, TestSource}
import org.apache.pekko.testkit.TestKit
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.{mock, when}
import org.scalamock.matchers.ArgCapture.CaptureOne
import org.scalamock.scalatest.MockFactory
import org.scalatest.BeforeAndAfterAll
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpecLike
Expand All @@ -38,23 +37,22 @@ class HttpMetricsSpec
extends TestKit(ActorSystem("HttpMetricsSpec"))
with AnyFlatSpecLike
with Matchers
with MockFactory
with ScalaFutures
with BeforeAndAfterAll {

implicit val ec: ExecutionContext = system.dispatcher

private def anyRequestContext() = any(classOf[RequestContext])
private def anyRequest() = any(classOf[HttpRequest])

abstract class Fixture[T] {
val metricsHandler: HttpMetricsHandler =
mock(classOf[HttpMetricsHandler])
val server: Function[RequestContext, Future[RouteResult]] =
mock(classOf[Function[RequestContext, Future[RouteResult]]])
val handler = mock[HttpMetricsHandler]
val server = mockFunction[RequestContext, Future[RouteResult]]

(handler.onConnection _).expects().returns((): Unit)
(handler.onDisconnection _).expects().returns((): Unit)

val (source, sink) = TestSource
.probe[HttpRequest]
.via(HttpMetrics.meterFlow(metricsHandler).join(HttpMetrics.metricsRouteToFlow(server)))
.via(HttpMetrics.meterFlow(handler).join(HttpMetrics.metricsRouteToFlow(server)))
.toMat(TestSink.probe[HttpResponse])(Keep.both)
.run()

Expand Down Expand Up @@ -97,80 +95,99 @@ class HttpMetricsSpec
}

it should "call the metrics handler on handled requests" in new Fixture {
val request: ArgumentCaptor[HttpRequest] = ArgumentCaptor.forClass(classOf[HttpRequest])
val response: ArgumentCaptor[HttpResponse] = ArgumentCaptor.forClass(classOf[HttpResponse])
when(metricsHandler.onRequest(request.capture()))
.thenAnswer(_.getArgument(0))
val request = CaptureOne[HttpRequest]()
val response = CaptureOne[HttpResponse]()

(handler.onRequest _)
.expects(capture(request))
.onCall { (req: HttpRequest) => req }

when(server.apply(anyRequestContext()))
.thenAnswer(invocation => complete(StatusCodes.OK)(invocation.getArgument[RequestContext](0)))
when(metricsHandler.onResponse(anyRequest(), response.capture()))
.thenAnswer(_.getArgument(1))
server
.expects(*)
.onCall(complete(StatusCodes.OK))

(handler.onResponse _)
.expects(*, capture(response))
.onCall { (_: HttpRequest, resp: HttpResponse) => resp }

val expectedRequest = HttpRequest()
sink.request(1)
source.sendNext(HttpRequest())
source.sendNext(expectedRequest)
sink.expectNext()

source.sendComplete()
sink.expectComplete()

val expected = Marshal(StatusCodes.OK)
val expectedResponse = Marshal(StatusCodes.OK)
.to[HttpResponse]
.futureValue

response.getValue shouldBe expected
request.value shouldBe expectedRequest
response.value shouldBe expectedResponse
}

it should "call the metrics handler on rejected requests" in new Fixture {
val request: ArgumentCaptor[HttpRequest] = ArgumentCaptor.forClass(classOf[HttpRequest])
val response: ArgumentCaptor[HttpResponse] = ArgumentCaptor.forClass(classOf[HttpResponse])
when(metricsHandler.onRequest(request.capture()))
.thenAnswer(_.getArgument(0))
val request = CaptureOne[HttpRequest]()
val response = CaptureOne[HttpResponse]()
(handler.onRequest _)
.expects(capture(request))
.onCall { (req: HttpRequest) => req }

when(server.apply(anyRequestContext()))
.thenAnswer(invocation => reject(invocation.getArgument[RequestContext](0)))
server
.expects(*)
.onCall(reject)

when(metricsHandler.onResponse(anyRequest(), response.capture()))
.thenAnswer(_.getArgument(1))
(handler.onResponse _)
.expects(*, capture(response))
.onCall { (_: HttpRequest, resp: HttpResponse) => resp }

val expectedRequest = HttpRequest()
sink.request(1)
source.sendNext(HttpRequest())
source.sendNext(expectedRequest)
sink.expectNext()

source.sendComplete()
sink.expectComplete()

val expected = Marshal(StatusCodes.NotFound -> "The requested resource could not be found.")
val expectedResponse = Marshal(StatusCodes.NotFound -> "The requested resource could not be found.")
.to[HttpResponse]
.futureValue
.addAttribute(PathLabeler.key, "unhandled")
response.getValue shouldBe expected

request.value shouldBe expectedRequest
response.value shouldBe expectedResponse
}

it should "call the metrics handler on error requests" in new Fixture {
val request: ArgumentCaptor[HttpRequest] = ArgumentCaptor.forClass(classOf[HttpRequest])
val response: ArgumentCaptor[HttpResponse] = ArgumentCaptor.forClass(classOf[HttpResponse])
when(metricsHandler.onRequest(request.capture()))
.thenAnswer(_.getArgument(0))
val request = CaptureOne[HttpRequest]()
val response = CaptureOne[HttpResponse]()
(handler.onRequest _)
.expects(capture(request))
.onCall { (req: HttpRequest) => req }

when(server.apply(anyRequestContext()))
.thenAnswer(invocation => failWith(new Exception("BOOM!"))(invocation.getArgument[RequestContext](0)))
server
.expects(*)
.onCall(failWith(new Exception("BOOM!")))

when(metricsHandler.onResponse(anyRequest(), response.capture()))
.thenAnswer(_.getArgument(1))
(handler.onResponse _)
.expects(*, capture(response))
.onCall { (_: HttpRequest, resp: HttpResponse) => resp }

val expectedRequest = HttpRequest()
sink.request(1)
source.sendNext(HttpRequest())
source.sendNext(expectedRequest)
sink.expectNext()

source.sendComplete()
sink.expectComplete()

val expected = Marshal(StatusCodes.InternalServerError)
val expectedResponse = Marshal(StatusCodes.InternalServerError)
.to[HttpResponse]
.futureValue
.addAttribute(PathLabeler.key, "unhandled")
response.getValue shouldBe expected

request.value shouldBe expectedRequest
response.value shouldBe expectedResponse
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import org.apache.pekko.stream.ClosedShape
import org.apache.pekko.stream.scaladsl.{GraphDSL, RunnableGraph}
import org.apache.pekko.stream.testkit.scaladsl.{TestSink, TestSource}
import org.apache.pekko.testkit.TestKit
import org.mockito.Mockito.{mock, verify, when}
import org.scalamock.scalatest.MockFactory
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.flatspec.AnyFlatSpecLike
import org.scalatest.matchers.should.Matchers
Expand All @@ -31,13 +31,24 @@ class MeterStageSpec
extends TestKit(ActorSystem("MeterStageSpec"))
with AnyFlatSpecLike
with Matchers
with MockFactory
with ScalaFutures {

private val request = HttpRequest()
private val response = HttpResponse()
private val error = new Exception("BOOM!")

trait Fixture {
val handler: HttpMetricsHandler = mock(classOf[HttpMetricsHandler])
val handler = stub[HttpMetricsHandler]

(handler.onConnection _).when().returns((): Unit)
(handler.onDisconnection _).when().returns((): Unit)
(handler.onRequest _).when(request).returns(request)
(handler.onResponse _).when(request, response).returns(response)
(handler.onFailure _).when(request, error).returns(error)
(handler.onFailure _)
.when(request, MeterStage.PrematureCloseException)
.returns(MeterStage.PrematureCloseException)

val (requestIn, requestOut, responseIn, responseOut) = RunnableGraph
.fromGraph(
Expand Down Expand Up @@ -73,22 +84,21 @@ class MeterStageSpec
responseIn.sendComplete()
responseOut.expectComplete()

verify(handler).onConnection()
verify(handler).onDisconnection()
(handler.onConnection _).verify()
(handler.onDisconnection _).verify()
}

it should "call onRequest wen request is offered" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)
requestIn.sendNext(request)
requestOut.expectNext() shouldBe request

when(handler.onResponse(request, response)).thenReturn(response)
responseIn.sendNext(response)
responseOut.expectNext() shouldBe response

(handler.onRequest _).verify(request)
}

it should "flush the stream before stopping" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)
requestIn.sendNext(request)
requestOut.expectNext() shouldBe request

Expand All @@ -97,71 +107,69 @@ class MeterStageSpec
requestOut.expectComplete()

// response should still be accepted
when(handler.onResponse(request, response)).thenReturn(response)
responseIn.sendNext(response)
responseOut.expectNext() shouldBe response

(handler.onRequest _).verify(request)
(handler.onResponse _).verify(request, response)
}

it should "propagate error from request in" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)

requestIn.sendNext(request)
requestOut.expectNext() shouldBe request

val error = new Exception("BOOM!")
requestIn.sendError(error)
requestOut.expectError(error)

(handler.onRequest _).verify(request)
}

it should "propagate error from request out" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)

requestIn.sendNext(request)
requestOut.expectNext() shouldBe request

val error = new Exception("BOOM!")
requestOut.cancel(error)
requestIn.expectCancellation()

(handler.onRequest _).verify(request)
}

it should "terminate and fail pending" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)

requestIn.sendNext(request)
requestIn.sendComplete()
requestOut.expectNext() shouldBe request
requestOut.expectComplete()

when(handler.onFailure(request, MeterStage.PrematureCloseException)).thenReturn(MeterStage.PrematureCloseException)
responseIn.sendComplete()
responseOut.expectComplete()

(handler.onRequest _).verify(request)
(handler.onFailure _).verify(request, MeterStage.PrematureCloseException)
}

it should "propagate error from response in and fail pending" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)

requestIn.sendNext(request)
requestIn.sendComplete()
requestOut.expectNext() shouldBe request
requestOut.expectComplete()

val error = new Exception("BOOM!")
when(handler.onFailure(request, error)).thenReturn(error)
responseIn.sendError(error)
responseOut.expectError(error)

(handler.onRequest _).verify(request)
(handler.onFailure _).verify(request, error)
}

it should "propagate error from response out and fail pending" in new Fixture {
when(handler.onRequest(request)).thenReturn(request)

requestIn.sendNext(request)
requestIn.sendComplete()
requestOut.expectNext() shouldBe request
requestOut.expectComplete()

val error = new Exception("BOOM!")
when(handler.onFailure(request, error)).thenReturn(error)
responseOut.cancel(error)
responseIn.expectCancellation()

(handler.onRequest _).verify(request)
(handler.onFailure _).verify(request, error)
}
}
4 changes: 2 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ object Dependencies {
val DropwizardV5 = "5.0.0"
val Enumeratum = "1.7.3"
val Logback = "1.5.3"
val Mockito = "5.9.0"
val Pekko = "1.0.2"
val PekkoHttp = "1.0.1"
val Prometheus = "0.16.0"
val ScalaCollectionCompat = "2.11.0"
val ScalaLogging = "3.9.5"
val ScalaMock = "6.0.0-M2"
val ScalaTest = "3.2.18"
}

Expand All @@ -35,7 +35,6 @@ object Dependencies {
val DropwizardJvm = "io.dropwizard.metrics" % "metrics-jvm" % Versions.Dropwizard % "test"
val DropwizardV5Jvm = "io.dropwizard.metrics5" % "metrics-jvm" % Versions.DropwizardV5 % "test"
val Logback = "ch.qos.logback" % "logback-classic" % Versions.Logback % "test"
val Mockito = "org.mockito" % "mockito-core" % Versions.Mockito % "test"
val PekkoHttpJson = "org.apache.pekko" %% "pekko-http-spray-json" % Versions.PekkoHttp % "test"
val PekkoHttpTestkit = "org.apache.pekko" %% "pekko-http-testkit" % Versions.PekkoHttp % "test"
val PekkoSlf4j = "org.apache.pekko" %% "pekko-slf4j" % Versions.Pekko % "test"
Expand All @@ -44,6 +43,7 @@ object Dependencies {
val PrometheusHotspot = "io.prometheus" % "simpleclient_hotspot" % Versions.Prometheus % "test"
val ScalaCollectionCompat =
"org.scala-lang.modules" %% "scala-collection-compat" % Versions.ScalaCollectionCompat % "test"
val ScalaMock = "org.scalamock" %% "scalamock" % Versions.ScalaMock % "test"
val ScalaTest = "org.scalatest" %% "scalatest" % Versions.ScalaTest % "test"
}
}

0 comments on commit 38a61a9

Please sign in to comment.