diff --git a/README.md b/README.md index 4d78b55..f1bedee 100644 --- a/README.md +++ b/README.md @@ -562,11 +562,10 @@ versions. 1. Publish artifacts automatically as part of the build process instead of doing it manually. 2. Finish the "Composable pagelets" implementation and documentation (it is currently unfinished and untested). -3. Add automated tests that check the actual timing of the BigPipe streaming. -4. Add support for pagelet priorities. -5. Add support for only rendering content that's visible. -6. Add support for monitoring hooks for each pagelet. -7. Turn the sample apps into Activator templates +3. Add support for pagelet priorities. +4. Add support for only rendering content that's visible. +5. Add support for monitoring hooks for each pagelet. +6. Turn the sample apps into Activator templates # License diff --git a/build.sbt b/build.sbt index 7b616c4..37de086 100644 --- a/build.sbt +++ b/build.sbt @@ -15,13 +15,13 @@ lazy val sampleAppCommon = (project in file("sample-app-common")) lazy val sampleAppScala = (project in file("sample-app-scala")) .settings(sampleAppScalaSettings:_*) .enablePlugins(PlayScala) - .dependsOn(bigPipe, sampleAppCommon) + .dependsOn(bigPipe, sampleAppCommon % "test->test;compile->compile") // The Java sample app lazy val sampleAppJava = (project in file("sample-app-java")) .settings(sampleAppJavaSettings:_*) .enablePlugins(PlayJava) - .dependsOn(bigPipe, sampleAppCommon) + .dependsOn(bigPipe, sampleAppCommon % "test->test;compile->compile") // The root project lazy val root = (project in file(".")) @@ -51,6 +51,7 @@ lazy val sampleAppCommonSettings = Seq( name := "sample-app-common", libraryDependencies ++= Seq( "com.typesafe.play" %% "play" % play.core.PlayVersion.current, + ws % Test, specs2 % Test ) ) ++ commonSettings ++ streamingTemplateSettings diff --git a/sample-app-java/test/IntegrationSpec.scala b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeSpec.scala similarity index 61% rename from sample-app-java/test/IntegrationSpec.scala rename to sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeSpec.scala index 52fa342..4518875 100644 --- a/sample-app-java/test/IntegrationSpec.scala +++ b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeSpec.scala @@ -1,45 +1,37 @@ -import org.specs2.mutable.Specification -import play.api.test.WithBrowser -import scala.collection.JavaConverters._ +package com.ybrikman.ping -class IntegrationSpec extends Specification { - "Application" should { +import play.api.test.WithBrowser - "render the page without BigPipe" in new WithBrowser { +/** + * End-to-end tests of BigPipe functionality. The Scala and Java sample apps can extend this trait to run all the tests + * in it. + */ +trait BaseBigPipeSpec extends PingSpecification { + "The sample app" should { + "render the page without BigPipe" in new WithBrowser(app = createTestComponents().app) { browser.goTo(s"http://localhost:$port/withoutBigPipe") browser.$("#profile .id").getTexts.get(0) must equalTo("profile") } - "render the page client-side with BigPipe" in new WithBrowser { + "render the page client-side with BigPipe" in new WithBrowser(app = createTestComponents().app) { browser.goTo(s"http://localhost:$port/withBigPipe") browser.$("#profile .id").getTexts.get(0) must equalTo("profile") } - "render the page server-side with BigPipe" in new WithBrowser { + "render the page server-side with BigPipe" in new WithBrowser(app = createTestComponents().app) { browser.goTo(s"http://localhost:$port/serverSideRendering") browser.$("#profile .id").getTexts.get(0) must equalTo("profile") } - "render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser { + "render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser(app = createTestComponents().app) { browser.goTo(s"http://localhost:$port/clientSideTemplating") browser.$("#profile .id").getTexts.get(0) must equalTo("profile") } - "handle errors while rendering with BigPipe" in new WithBrowser { + "handle errors while rendering with BigPipe" in new WithBrowser(app = createTestComponents().app) { browser.goTo(s"http://localhost:$port/errorHandling") browser.$("#profile .id").getTexts.get(0) must equalTo("profile") browser.$("#feed .id").getTexts.get(0) must equalTo("error") } - - "dedupe remote calls" in new WithBrowser { - browser.goTo(s"http://localhost:$port/dedupe") - val values = browser.$(".id").getTexts.asScala - - // First 3 values should be the same since they were de-duped, fourth should be different - values must have size 4 - values(0) mustEqual values(1) - values(1) mustEqual values(2) - values(1) mustNotEqual values(3) - } } } diff --git a/sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala new file mode 100644 index 0000000..5b39876 --- /dev/null +++ b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseBigPipeTimingSpec.scala @@ -0,0 +1,223 @@ +package com.ybrikman.ping + +import com.ybrikman.ping.TimingHelper._ +import com.ybrikman.ping.CustomRoutes._ +import com.ybrikman.ping.javaapi.bigpipe.PageletRenderOptions +import com.ybrikman.ping.scalaapi.bigpipe.HtmlStreamImplicits._ +import com.ybrikman.ping.scalaapi.bigpipe._ +import data.FutureUtil +import play.api.libs.concurrent.Execution.Implicits._ +import play.api.libs.iteratee.{Enumerator, Iteratee} +import play.api.libs.ws.WSClient +import play.api.mvc.{Action, Results} +import play.api.routing.Router +import play.api.routing.sird._ + +import scala.concurrent.{ExecutionContext, Future} + +/** + * Tests that BigPipe is actually streaming data as soon as it's available and that chunks are not blocked anywhere. + * The Scala and Java sample apps can extend this trait to run all the tests in it. + */ +trait BaseBigPipeTimingSpec extends PingSpecification { + "BigPipe streaming" should { + "Send down the data in-order, only after all of it is available, without BigPipe" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { + val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withoutBigPipe") + chunkTimings must not be empty + + // Time-to-first-byte: make sure the first chunk was sent back after the maxDelay (within a tolerance of + // ToleranceInMillis) + val firstChunk = chunkTimings(0) + firstChunk.content mustEqual FirstChunkContent + firstChunk.timeElapsed must beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) + + // Make sure the contents for each pagelet were sent back exactly once + val pageletContentTiming = PageletIndices.flatMap { index => + chunkTimings.filter(_.content.contains(content(index))) + } + pageletContentTiming must have size PageletIndices.size + + // Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of + // ToleranceInMillis) + val expectedTimingMatchers = PageletIndices.map { index => + beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) + } + pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder + } + + "Send down the data out-of-order, as soon as any of it is available, with client-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { + val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeClientSide") + chunkTimings must not be empty + + // Time-to-first-byte: make sure the first chunk was sent back almost immediately + val firstChunk = chunkTimings(0) + firstChunk.content mustEqual FirstChunkContent + firstChunk.timeElapsed must beLessThan(ToleranceInMillis) + + // Placeholders: make sure all the placeholders were sent back almost immediately and exactly once + val pageletPlaceholderTiming = PageletIndices.flatMap { index => + chunkTimings.filter(_.content.contains(placeholder(id(index)))) + } + pageletPlaceholderTiming must have size PageletIndices.size + pageletPlaceholderTiming.map(_.timeElapsed) must contain(beLessThan(ToleranceInMillis)).forall + + // Make sure the contents for each pagelet were sent back exactly once + val pageletContentTiming = PageletIndices.flatMap { index => + chunkTimings.filter(_.content.contains(content(index))) + } + pageletContentTiming must have size PageletIndices.size + + // Check that contents for each pagelet were delayed by no more and no less than + // DELAY_MULTIPLIER_IN_MILLIS (within a tolerance of ToleranceInMillis) + val expectedTimingMatchers = PageletIndices.map { index => + val expecteDelay = delay(index) + beGreaterThan(expecteDelay - ToleranceInMillis) and beLessThan(expecteDelay + ToleranceInMillis) + } + pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder + } + + "Send down the data in-order, as soon as it's available, with server-side streaming" in new WithWarmedUpPingTestServer(createTestComponents(withRouterToTestTimings)) { + val chunkTimings = getTimings(components.wsClient, s"http://localhost:$port/withBigPipeServerSide") + chunkTimings must not be empty + + // Time-to-first-byte: make sure the first chunk was sent back almost immediately + val firstChunk = chunkTimings(0) + firstChunk.content mustEqual FirstChunkContent + firstChunk.timeElapsed must beLessThan(ToleranceInMillis) + + // Make sure the contents for each pagelet were sent back exactly once + val pageletContentTiming = PageletIndices.flatMap { index => + chunkTimings.filter(_.content.contains(content(index))) + } + pageletContentTiming must have size PageletIndices.size + + // Check that contents for each pagelet were delayed by the slowest pagelet and no more (within a tolerance of + // ToleranceInMillis) + val expectedTimingMatchers = PageletIndices.map { index => + beGreaterThan(maxDelay - ToleranceInMillis) and beLessThan(maxDelay + ToleranceInMillis) + } + pageletContentTiming.map(_.timeElapsed) must contain(eachOf(expectedTimingMatchers:_*)).inOrder + } + } + + private def getTimings(wsClient: WSClient, url: String): Seq[Timing] = { + val initialTimings = Timings() + val (_, bodyEnumerator) = await(wsClient.url(url).getStream()) + + val checkTiming = Iteratee.fold[Array[Byte], Timings](initialTimings) { (timings, chunk) => + timings.addChunk(chunk) + } + + val timings = await(bodyEnumerator.run(checkTiming)) + + // Useful for debugging + println(s"Timings and content for url $url:\n") + timings.chunkTimings.foreach(timing => println(s"----- ${timing.timeElapsed} ms -----\n\n${timing.content}\n\n")) + + timings.chunkTimings + } +} + +object TimingHelper { + val PageletIndices = 1 until 5 + + // Each pagelet id will have this prefix to make it easier to find in the stream of data + val IdPrefix = "pagelet_id_" + + // The contents of each pagelet will have this prefix to make it easier to find in the stream of data + val ContentPrefix = "pagelet_content_" + + // The placeholder for each pagelet will have this prefix to make it easier to find in the stream of data + val PlaceHolderPrefix = "pagelet_placeholder_" + + // The contents of the very first chunk that should be sent back by each page + val FirstChunkContent = "first_chunk_content" + + // Each pagelet will be delayed by this many milliseconds + val DelayMultiplierInMillis = 3000L + + // With tests running in parallel, things may get a bit delayed, so check all timings within this tolerance + val ToleranceInMillis = DelayMultiplierInMillis / 10 + + def id(index: Int): String = { + s"$IdPrefix$index" + } + + def content(index: Int): String = { + s"$ContentPrefix$index" + } + + def delay(index: Int): Long = { + (PageletIndices.size - index) * DelayMultiplierInMillis + } + + def maxDelay: Long = { + delay(PageletIndices.head) + } + + def placeholder(id: String): String = { + s"$PlaceHolderPrefix$id" + } +} + +case class Timings(startTime: Long = System.currentTimeMillis(), chunkTimings: Seq[Timing] = Seq.empty) { + def addChunk(contents: Array[Byte]): Timings = { + val timeElapsed = System.currentTimeMillis() - startTime + copy(chunkTimings = chunkTimings :+ Timing(new String(contents, "UTF-8"), timeElapsed)) + } +} + +case class Timing(content: String, timeElapsed: Long) + +class MockTextPagelet(id: String, content: Future[String]) extends TextPagelet(id, content) { + override def renderPlaceholder(implicit ec: ExecutionContext): HtmlStream = { + HtmlStream.fromHtml(com.ybrikman.bigpipe.html.pageletServerSide(placeholder(id), PageletConstants.EmptyContent)) + } +} + +object CustomRoutes { + def withRouterToTestTimings: Option[RouterComponents => Router] = { + def createRoutes(routerComponents: RouterComponents): Router = { + val futureUtil = new FutureUtil(routerComponents.actorSystem) + + Router.from { + case GET(p"/withoutBigPipe") => Action.async { + val futures = mockRemoteServiceCalls(futureUtil).map(_._2) + Future.sequence(futures).map { contents => + Results.Ok.chunked(Enumerator(FirstChunkContent).andThen(Enumerator(contents:_*))) + } + } + case GET(p"/withBigPipeClientSide") => Action { + val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) } + Results.Ok.chunked(renderPagelets(PageletRenderOptions.ClientSide, pagelets)) + } + case GET(p"/withBigPipeServerSide") => Action { + val pagelets = mockRemoteServiceCalls(futureUtil).map { case (id, data) => new MockTextPagelet(id, data) } + Results.Ok.chunked(renderPagelets(PageletRenderOptions.ServerSide, pagelets)) + } + case GET(p"/warmup") => Action { + Results.Ok("warmup") + } + } + } + + Option(createRoutes _) + } + + // Generate a series of Futures that represent remote calls. The Futures are returned in reverse order, from slowest + // to fastest, as a way to demonstrate the advantages of out-of-order client-side rendering. + private def mockRemoteServiceCalls(futureUtil: FutureUtil): Seq[(String, Future[String])] = { + PageletIndices.map { index => + id(index) -> futureUtil.timeout(content(index), delay(index)) + } + } + + private def renderPagelets(renderOptions: PageletRenderOptions, pagelets: Seq[Pagelet]): HtmlStream = { + val bigPipe = new BigPipe(renderOptions, pagelets:_*) + bigPipe.render { renderedPagelets => + pagelets.foldLeft(HtmlStream.fromString(FirstChunkContent)) { (stream, pagelet) => + stream.andThen(renderedPagelets(pagelet.id)) + } + } + } +} diff --git a/sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala new file mode 100644 index 0000000..54f5c9c --- /dev/null +++ b/sample-app-common/src/test/scala/com/ybrikman/ping/BaseDedupeSpec.scala @@ -0,0 +1,23 @@ +package com.ybrikman.ping + +import play.api.test.WithBrowser +import scala.collection.JavaConverters._ + +/** + * An end-to-end test of the de-duping cache. The Scala and Java sample apps can extend this trait to run all the tests + * in it. + */ +trait BaseDedupeSpec extends PingSpecification { + "The Deduping controller" should { + "dedupe remote calls" in new WithBrowser(app = createTestComponents().app) { + browser.goTo(s"http://localhost:$port/dedupe") + val values = browser.$(".id").getTexts.asScala + + // First 3 values should be the same since they were de-duped, fourth should be different + values must have size 4 + values(0) mustEqual values(1) + values(1) mustEqual values(2) + values(1) mustNotEqual values(3) + } + } +} diff --git a/sample-app-common/src/test/scala/com/ybrikman/ping/PingSpecification.scala b/sample-app-common/src/test/scala/com/ybrikman/ping/PingSpecification.scala new file mode 100644 index 0000000..6eba2b4 --- /dev/null +++ b/sample-app-common/src/test/scala/com/ybrikman/ping/PingSpecification.scala @@ -0,0 +1,61 @@ +package com.ybrikman.ping + +import akka.actor.ActorSystem +import org.specs2.execute.{AsResult, Result} +import play.api.Application +import play.api.libs.ws.WSClient +import play.api.routing.Router +import play.api.test.{PlaySpecification, DefaultAwaitTimeout, FutureAwaits, WithServer} + +trait PingSpecification extends PlaySpecification with PingTestComponentsProvider + +trait PingTestComponentsProvider { + def createTestComponents(customRoutes: Option[RouterComponents => Router] = None): PingTestComponents +} + +/** + * Common class that abstracts away whether the underlying app uses Java or Scala and how it's initialized. + * + * @param app + * @param wsClient + */ +case class PingTestComponents(app: Application, wsClient: WSClient) + +/** + * A bit of an ugly hack. Some test cases need custom routing. One in particular has a custom action that depends on + * access to the ActorSystem. I can't figure out an easy way to solve this that works with both run-time dependency + * injection (for Java apps) and compile-time dependency injection (for Scala apps), so this is an ugly workaround. + * + * @param actorSystem + */ +case class RouterComponents(actorSystem: ActorSystem) + +/** + * An extension of specs2 "Around" that can be used to fire up a test server in one line. + * + * @param components + */ +abstract class WithPingTestServer(val components: PingTestComponents) extends WithServer(app = components.app) + +/** + * Same as WithPingTestServer, except this one hits the /warmup URL a bunch of times before running the test. This is + * useful to ensure the app is fully up and running so that tests sensitive to timing are not thrown off by bootup + * and initialization routines. + * + * @param components + */ +abstract class WithWarmedUpPingTestServer(components: PingTestComponents) extends WithPingTestServer(components) with FutureAwaits with DefaultAwaitTimeout { + override def around[T](t: => T)(implicit evidence$3: AsResult[T]): Result = { + super.around { + warmup() + t + } + } + + // Make sure the server is warmed up so our timing is not thrown off by bootup and initialization routines + private def warmup(): Unit = { + (0 until 15).foreach { _ => + await(components.wsClient.url(s"http://localhost:$port/warmup").get()) + } + } +} \ No newline at end of file diff --git a/sample-app-java/test/com/ybrikman/ping/PingJavaTestComponents.scala b/sample-app-java/test/com/ybrikman/ping/PingJavaTestComponents.scala new file mode 100644 index 0000000..48e25fa --- /dev/null +++ b/sample-app-java/test/com/ybrikman/ping/PingJavaTestComponents.scala @@ -0,0 +1,32 @@ +package com.ybrikman.ping + +import akka.actor.ActorSystem +import play.api.libs.ws.WSClient +import play.api.mvc.{RequestHeader, Handler} +import play.api.routing.Router +import play.api.test.{FakeRequest, FakeApplication} + +trait PingJavaTestComponents extends PingTestComponentsProvider { + override def createTestComponents(customRoutes: Option[(RouterComponents) => Router]): PingTestComponents = { + val initialApp = FakeApplication() + val actorSystem = initialApp.injector.instanceOf(classOf[ActorSystem]) + val wsClient = initialApp.injector.instanceOf(classOf[WSClient]) + + val routes = customRoutes + .map(f => f(RouterComponents(actorSystem))) + .map(routerToPartialFunction) + .getOrElse(PartialFunction.empty) + + val app = initialApp.copy(withRoutes = routes) + + PingTestComponents(app, wsClient) + } + + private def routerToPartialFunction(router: Router): PartialFunction[(String, String), Handler] = { + val toRequestHeader: PartialFunction[(String, String), RequestHeader] = { + case (method, path) => FakeRequest(method, path) + } + + toRequestHeader.andThen(router.routes) + } +} diff --git a/sample-app-java/test/com/ybrikman/ping/Tests.scala b/sample-app-java/test/com/ybrikman/ping/Tests.scala new file mode 100644 index 0000000..97f6a33 --- /dev/null +++ b/sample-app-java/test/com/ybrikman/ping/Tests.scala @@ -0,0 +1,7 @@ +package com.ybrikman.ping + +class DedupeSpec extends BaseDedupeSpec with PingJavaTestComponents + +class BigPipeSpec extends BaseBigPipeSpec with PingJavaTestComponents + +class BigPipeTimingSpec extends BaseBigPipeTimingSpec with PingJavaTestComponents diff --git a/sample-app-scala/app/loader/PingApplicationLoader.scala b/sample-app-scala/app/loader/PingApplicationLoader.scala index aa03fb9..fe20904 100644 --- a/sample-app-scala/app/loader/PingApplicationLoader.scala +++ b/sample-app-scala/app/loader/PingApplicationLoader.scala @@ -43,7 +43,7 @@ class PingComponents(context: Context) extends BuiltInComponentsFromContext(cont val assets = new Assets(httpErrorHandler) - override val router: Router = new Routes( + val routes = new Routes( httpErrorHandler, withoutBigPipe, withBigPipe, @@ -52,6 +52,8 @@ class PingComponents(context: Context) extends BuiltInComponentsFromContext(cont mock, assets) + override val router: Router = routes + val cacheFilter = new CacheFilter(cache) override lazy val httpFilters = Seq(cacheFilter) } diff --git a/sample-app-scala/test/IntegrationSpec.scala b/sample-app-scala/test/IntegrationSpec.scala deleted file mode 100644 index 5b27c18..0000000 --- a/sample-app-scala/test/IntegrationSpec.scala +++ /dev/null @@ -1,53 +0,0 @@ -import loader.PingApplicationLoader -import org.specs2.mutable._ -import play.api.test._ -import play.api.{Application, ApplicationLoader, Environment, Mode} -import scala.collection.JavaConverters._ - -class IntegrationSpec extends Specification { - - // Without this, the WithBrowser helper uses Guice to load your app - def app: Application = { - val context = ApplicationLoader.createContext(new Environment(new java.io.File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test)) - new PingApplicationLoader().load(context) - } - - "Application" should { - "render the page without BigPipe" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/withoutBigPipe") - browser.$("#profile .id").getTexts.get(0) must equalTo("profile") - } - - "render the page client-side with BigPipe" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/withBigPipe") - browser.$("#profile .id").getTexts.get(0) must equalTo("profile") - } - - "render the page server-side with BigPipe" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/serverSideRendering") - browser.$("#profile .id").getTexts.get(0) must equalTo("profile") - } - - "render the page client-side with BigPipe and Mustache.js JavaScript templates" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/clientSideTemplating") - browser.$("#profile .id").getTexts.get(0) must equalTo("profile") - } - - "handle errors while rendering with BigPipe" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/errorHandling") - browser.$("#profile .id").getTexts.get(0) must equalTo("profile") - browser.$("#feed .id").getTexts.get(0) must equalTo("error") - } - - "dedupe remote calls" in new WithBrowser(app = app) { - browser.goTo(s"http://localhost:$port/dedupe") - val values = browser.$(".id").getTexts.asScala - - // First 3 values should be the same since they were de-duped, fourth should be different - values must have size 4 - values(0) mustEqual values(1) - values(1) mustEqual values(2) - values(1) mustNotEqual values(3) - } - } -} diff --git a/sample-app-scala/test/com/ybrikman/ping/PingScalaTestComponents.scala b/sample-app-scala/test/com/ybrikman/ping/PingScalaTestComponents.scala new file mode 100644 index 0000000..9ad5b8c --- /dev/null +++ b/sample-app-scala/test/com/ybrikman/ping/PingScalaTestComponents.scala @@ -0,0 +1,22 @@ +package com.ybrikman.ping + +import loader.PingComponents +import play.api.routing.Router +import play.api.{Mode, Environment, ApplicationLoader} + +trait PingScalaTestComponents extends PingTestComponentsProvider { + + override def createTestComponents(customRoutes: Option[(RouterComponents) => Router]): PingTestComponents = { + val components = new PingComponentsForTest(createContext, customRoutes) + PingTestComponents(components.application, components.wsClient) + } + + private def createContext: ApplicationLoader.Context = { + val env = new Environment(new java.io.File("."), ApplicationLoader.getClass.getClassLoader, Mode.Test) + ApplicationLoader.createContext(env) + } +} + +class PingComponentsForTest(context: ApplicationLoader.Context, customRoutes: Option[(RouterComponents) => Router]) extends PingComponents(context) { + override val router: Router = customRoutes.map(f => f(RouterComponents(actorSystem))).getOrElse(routes) +} diff --git a/sample-app-scala/test/com/ybrikman/ping/Tests.scala b/sample-app-scala/test/com/ybrikman/ping/Tests.scala new file mode 100644 index 0000000..eaa9b91 --- /dev/null +++ b/sample-app-scala/test/com/ybrikman/ping/Tests.scala @@ -0,0 +1,7 @@ +package com.ybrikman.ping + +class DedupeSpec extends BaseDedupeSpec with PingScalaTestComponents + +class BigPipeSpec extends BaseBigPipeSpec with PingScalaTestComponents + +class BigPipeTimingSpec extends BaseBigPipeTimingSpec with PingScalaTestComponents \ No newline at end of file