Integration testing an HTTP server with ZIO test suite - scala

I am trying to figure out the idiom for writing an integration test for an Http4s app that supports two end points.
I'm starting the Main app class in a ZManaged by forking it on a new fiber and then doing interruptFork on release of the ZManaged.
I then convert this to a ZLayer and pass it via provideCustomLayerShared() on the whole suite that has multiple testMs .
Am I on the right track here?
It doesn't behave as I expect it to :
Although the httpserver managed in the way mentioned is provided to the suite that encloses both the tests, it is released after the first test and thus the second test fails
The test suite never finishes and just hangs after executing both tests
Apologies for the half baked nature of the code below.
object MainTest extends DefaultRunnableSpec {
def httpServer =
ZManaged
.make(Main.run(List()).fork)(fiber => {
//fiber.join or Fiber.interrupt will not work here, hangs the test
fiber.interruptFork.map(
ex => println(s"stopped with exitCode: $ex")
)
})
.toLayer
val clockDuration = 1.second
//did the httpserver start listening on 8080?
private def isLocalPortInUse(port: Int): ZIO[Clock, Throwable, Unit] = {
IO.effect(new Socket("0.0.0.0", port).close()).retry(Schedule.exponential(clockDuration) && Schedule.recurs(10))
}
override def spec: ZSpec[Environment, Failure] =
suite("MainTest")(
testM("Health check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ <- isLocalPortInUse(8080)
client <- Task(JavaNetClientBuilder[Task](blocker).create)
response <- client.expect[HealthReplyDTO]("http://localhost:8080/health")
expected = HealthReplyDTO("OK")
} yield assert(response) {
equalTo(expected)
}
},
testM("Distances endpoint check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ <- isLocalPortInUse(8080)
client <- Task(JavaNetClientBuilder[Task](blocker).create)
response <- client.expect[DistanceReplyDTO](
Request[Task](method = Method.GET, uri = uri"http://localhost:8080/distances")
.withEntity(DistanceRequestDTO(List("JFK", "LHR")))
)
expected = DistanceReplyDTO(5000)
} yield assert(response) {
equalTo(expected)
}
}
).provideCustomLayerShared(httpServer)
}
Output of the test is that the second test fails while the first succeeds.
And I debugged enough to see that the HTTPServer is already brought down before the second test.
stopped with exitCode: ()
- MainTest
+ Health check
- Distances endpoint check
Fiber failed.
A checked error was not handled.
org.http4s.client.UnexpectedStatus: unexpected HTTP status: 404 Not Found
And whether I run the tests from Intellij on sbt testOnly, the test process keeps hung after all this and I have to manually terminate it.

I think there are two things here:
ZManaged and acquire
The first parameter of ZManaged.make is the acquire function which creates the resource. The problem is that resource acquisition (as well as releasing them) is done uninterruptible. And whenever you do a .fork, the forked fiber inherits its interruptibility from its parent fiber. So the Main.run() part can actually never be interrupted.
Why does it seem to work when you do fiber.interruptFork? interruptFork doesn't actually wait for the fiber to be interrupted. Only interrupt will do that, which is why it will hang the test.
Luckily there is a method which will do exactly what you want: Main.run(List()).forkManaged. This will generate a ZManaged which will start the main function and interrupt it when the resource is released.
Here is some code which demonstrates the problem nicely:
import zio._
import zio.console._
import zio.duration._
object Main extends App {
override def run(args: List[String]): URIO[ZEnv, ExitCode] = for {
// interrupting after normal fork
fiberNormal <- liveASecond("normal").fork
_ <- fiberNormal.interrupt
// forking in acquire, interrupting in relase
_ <- ZManaged.make(liveASecond("acquire").fork)(fiber => fiber.interrupt).use(_ => ZIO.unit)
// fork into a zmanaged
_ <- liveASecond("forkManaged").forkManaged.use(_ => ZIO.unit)
_ <- ZIO.sleep(5.seconds)
} yield ExitCode.success
def liveASecond(name: String) = (for {
_ <- putStrLn(s"born: $name")
_ <- ZIO.sleep(1.seconds)
_ <- putStrLn(s"lived one second: $name")
_ <- putStrLn(s"died: $name")
} yield ()).onInterrupt(putStrLn(s"interrupted: $name"))
}
This will give the output:
born: normal
interrupted: normal
born: acquire
lived one second: acquire
died: acquire
born: forkManaged
interrupted: forkManaged
As you can see, both normal as well as forkManaged get interrupted immediately. But the one forked within acquire runs to completion.
The second test
The second test seems to fail not because the server is down, but because the server seems to be missing the "distances" route on the http4s side. I noticed that you get a 404, which is a HTTP Status Code. If the server were down, you would probably get something like Connection Refused. When you get a 404, some HTTP server is actually answering.
So here my guess would be that the route really is missing. Maybe check for typos in the route definition or maybe the route is just not composed into the main route.

In the end #felher 's Main.run(List()).forkManaged helped solve the first problem.
The second problem about the GET with a body being rejected from inside the integration test was worked-around by changing the method to POST. I did not look further into why the GET was being rejected from inside the test but not when done with a normal curl to the running app.

Related

Integration tests hangs when testing my API in ZIO + HTTP4S

I am having issues testing my first ZIO+HTTP4S application. The test hangs and does not finish.
The code for my App (simplified) is
object Main extends App {
def server: ZIO[AppEnvironment, Throwable, Unit] =
for {
(...)
fullApp <- ZIO.runtime[AppEnvironment].flatMap { implicit rts =>
BlazeServerBuilder[AppTask](ec)
.bindHttp(api.port, api.endpoint)
.withHttpApp(CORS(httpApp))
.serve
.compile[AppTask, AppTask, CatsExitCode]
.drain
}
} yield fullApp
override def run(args: List[String]): ZIO[ZEnv, Nothing, ExitCode] = {
server.provideSomeLayer[ZEnv](appEnvironment).tapError(err => putStrLn(s"Execution failed with: $err")).exitCode
}
}
This is my test code. Note that it is basically copypasted from THIS OTHER STACKOVERFLOW QUESTION
object ApiTest extends DefaultRunnableSpec {
val ec: ExecutionContext = ExecutionContext.global
def httpServer = Main.run(List()).forkManaged.toLayer
val clockDuration = ofSeconds(1)
val blocker = Blocker.liftExecutionContext(ec)
//did the httpserver start listening on 8080?
private def isLocalPortInUse(port: Int): ZIO[Clock, Throwable, Unit] = {
IO.effect {
println("checking for local port in use")
new Socket("0.0.0.0", port).close()
}
.retry(Schedule.linear(clockDuration) && Schedule.recurs(10))
}
override def spec: ZSpec[Environment, Failure] =
suite("MainTest")(
testM("Health check") {
for {
_ <- TestClock.adjust(clockDuration).fork
_ = println("1")
_ <- isLocalPortInUse(8080)
_ = println("2")
client <- Task(JavaNetClientBuilder[Task](blocker).create)
_ = println("3")
response <- client.expect[String]("http://localhost:8080/healthcheck")
_ = println("4")
} yield assert(response)(equalTo(""))
}
).provideCustomLayerShared(httpServer)
}
The problem is that as soon as the server starts, the test stops running and are not executed. The output is
1
checking for local port in use
checking for local port in use
<server init message>
In Suite "MainTest", test "Health check" has taken more than 1 m to execute. If this is not expected, consider using TestAspect.timeout to timeout runaway tests for faster diagnostics.
So as you can see, the tests run OK until the server starts and then does not continue the execution.
Also, how could I perform a POST call instead of a GET one? I'm a bit lost in the HTTP4S/ZIO ecosystem with http4sClients, ZHTTP, BlazeClientBuilders and the like. What would be the easiest way of doing a POST call to my server in a test like the previous one?
Cheers!
edit: I’ve checked that the server works fine while hanging in here, I can do CURL calls from a terminal and it responds correctly. So it seems clear that the problem is that as soon as the server is up it stays in the front, not background, and the tests do not have the chance to finish the execution.
You are advancing the clock by 1 second but your application might require more time to run. Also, your particular test will require infinite time to run because, while unit tests are instantaneous in ZIO, integration tests are not.
Advancing the time of a unit test by 1 second requires theoretically 0 seconds. This might not be enough for the port to become free.
Since you are trying to create an integration test, you should use a real Clock and not the one provided by the test kit.

How to correctly do error handling that involves asynchronous methods

Suppose I have the following methods defined in some service used in my Play application each of which executes some command, on failure, updates database and sends some notification asynchronously.
def thisMightFail(): Future[SomeResult]
def thisUpdatesStatus(): Future[Unit]
def thisSendsNotification(): Future[Unit]
thisUpdatesStatus and thisSendsNotification are independent of each other and are called for error handling like the below. (No further execution is possible on failure)
for {
_ <- Future{ Some process }
result <- thisMightFail() transform {
case Success(v) => v
case Failure(cause) =>
val f = Future.sequence(List(
thisUpdatesStatus(),
thisSendsNotification()
))
Failure(new Exception("Command execution failed", cause))
}
_ <- Future{ Another process that uses "result" }
:
} yield ...
My question is should the f be waited before returning Failure(cause) or is there some better way to handle the error in this kind of situation?
My question is should the f be waited before returning Failure(cause)
or is there some better way to handle the error in this kind of
situation
Currently your code does not handle failure of thisUpdatesStatus() and thisSendsNotification(). are you ok with the failure of these functions call ? I suspect it will not be ok if thisMightFail() fails and you do not update the database. Also user should receive some notification if thisMightFail() fails.
You should use a workflow platform for such cases such as cadence . You can find a nice introduction of cadence here.

unable handle exception from future failure

I want the following code to return a custom message when one of the method, callfuture1() or callfuture2(), throws an exception. My understanding was if either of the future fails, f would be a failed future.
However, when callfuture1 throws an exception. f.onFailure is not executed. Instead I see the call stack stopped at line of code in callFuture1() where exception occurred and a standard internalError is returned. Why does that happen?
val f = for {
x <- callfuture1()
y <- callfuture2()
} yield y
f.onFailure {
//send an internalserver error with some custom message
}
f.map {
//send data back
}
====update====
i see from the responses, that potential issue is that Exception is being thrown outside the Future and hence my code fails to catch that failed future.
So i changed the code such that Exception only occurs inside the future. I still am not able to explain the behavior i am seeing. (I wonder if it has to anything to do with Play framework.)
def controllerfunction(id: String) = Action.async{
val f = for{
x <- callfuture1(id)
y <- callfuture2(x)
} yield y
y.onFailure{case t =>
println("This gets printed");
Ok("shit happened, but i am still ok")}
y.map{resp:String => Ok(resp)}
}
def callfuture1(id: String):Future[Obj1] = {
for {
val1 <- callfuture1.1(id)
val2 <- callfuture1.2(val1)
} yield val2
}
def callfuture1.2:Future[Obj3] = Future{
thrown new Exception("TEST ME");
}
def callfuture 1.1:Future[Obj4] = {...}
def callfuture2: Future[String] = {....}
Expectation.
The method callfuture1.2 throws an exception inside the future, so my expectation is onFailure should be executed, (which does get executed), and the response returned should "Shit happened, but i am still ok"
Actuality
The play framework returns InternalServerError and i see error stack on my console. I see the printlin("This gets printed") is getting executed.
Cant understand what is happening. Any insights?
==== update 2 =====
I verified that the issue only happens when called inside controller of play framework ( i am using play 2.5). As a standalone scala program everthing works as expected. I believe play error handling catches the unhandaled exception and prints the stack trace. I think this should only be happening in development environment.
This can happen if callfuture1 throws "outside of a future".
Your for comprehension is desugared into this:
val f = callfuture1.flatMap{ x =>
callfuture2.map{ y =>
y
}
}
If callfuture2 throws right away (as opposed to returning a failed future), you will still end up with a failed future because callfuture2 is called inside Future.flatMap, which catches exceptions and turns them into failed futures (same for Future.map).
The situation is different for callfuture1: if it throws right away, there is no enclosing Future.map or Future.flatMap to turn it into a failed future.
In general you should try to avoid having a method that returns a Future and can also throw an error.
This means that if callfuture1 does anything that can throw, it should catch that and turn the exception in a failed future that you then return.
UPDATE: Concerning your update about how you expected "Shit happened, but i am still ok" to be returned:
As already hinted by Dima in a comment, Future.onFailure can only be used for side effects. Futures are immutable. If you want to recover from a failed exception, there is no way to modify the original (failed) future and all you can actually do is transform it into a new future.
Have a look at Future.recover. It does exactly what you need, namely it allows to transform an input future by matching the failed result (if any) and transforming it into a successful future. It is the equivalent of a catch clause, but for futures. Concretely what you really meant to do is something like this:
def controllerfunction(id: String) = Action.async{
val f = for{
x <- callfuture1(id)
y <- callfuture2(x)
} yield y
f.map{ resp: String =>
Ok(resp)
}.recover{
case t: Throwable =>
println("This gets printed");
Ok("shit happened, but i am still ok")
}
}
It seems that inside callfuture1() you're not wrapping all your process inside the Future constructor like
def callfuture1(): Future[?] = Future {
val x = ...
x
}
but your code seems to be
def callfuture1(): Future[?] = {
val x = ... // some error happen here
Future(x)
}
so because it's outside the future, your error is throwing directly into your program code

Do Play! 2 Action.async requests kill the future in the background when interrupted?

I've got the following Play! 2 Action.async controller:
def complexAlgorithm(graphId: String) = Action.async { implicit request =>
val f = future {
val data = new ComplexAlgorithm(GraphContext.get(graphId))
data.evaluate
data
}
f.map { result => Ok(Json.generate(result.getRankings.toList.filterNot(o => o.rankScore == 0))) }
}
I've realized that in some cases this computation is actually going to take more than an hour. Since this will be redesigned for production use, I'm OK with waiting for the result in the browser since it's logged anyways.
So my question is does the future above val f get killed if the browser request is interrupted? Say for instance, if the internet disconnects while waiting for a response? Or will it actually complete its calculation (even if hours later)?
Thanks!

Future does not complete reliably in Play Framework

A piece of code using Play Framework 2.3
def signup = Action(parse.json) { implicit request =>
val email = (request.body \ "email").asOpt[String]
val holder =
WS.url("https://foobar.com/foo")
.withAuth("1234","5678",WSAuthScheme.BASIC)
val data = Json.obj(
"email" -> email,
)
holder.post(data)
Ok("OK")
}
This code will perform the post() when run locally on my machine, but not reliably on remote machines. From my understanding the post() call should create and return a future, signup() returns Ok(), the future is run on the default ExecutionContext and at some point completes and is cleaned up. Apparently this is not the case.
I have since changed the function to be an Action.async and am now waiting on the Future:
val res = holder.post(data)
res.map( x => Ok(Json.obj("status" -> "OK"))) recover {
case (e: Exception) =>
InternalServerError(Json.obj("status" -> "Not OK."))
}
This works reliably. Hence, my questions:
Why does the first version not work reliably?
How can I in general "fork off" a long running procedure, without intention of waiting on it, purely for its side-effects, if not like I did in version 1?
It's near impossible to say why the Future isn't completing "reliably", but I'd hazard a guess that there's something wrong with the web service you're trying to reach. SSL problems, timeouts not being reached,.. the possibilities are large without knowing what exception is being thrown (if any?).
If you don't want to wait on the WS call, you can use the onComplete callback:
val res = holder.post(data)
res.onComplete {
case Success(response) =>
// Create some side-effect to the response
case Failure(e) =>
// The `Future` failed.
}