Testing

This example is quite simple and basically just an HTTP facade over the database, so we won't test components in isolation. Instead, we'll use integration-testing to check the system as a whole.

In order to have SBT properly recognize our integration tests, we need to add the proper configurations to build.sbt. Please refer to the chapter code on GitHub (https://github.com/PacktPublishing/Learn-Scala-Programming) to see how this is done.

In our integration test, we will let our system run normally (but with the test database) and use an HTTP client to call the API and inspect the responses it will return.

First, we need to prepare our HTTP client and server:

class ServerSpec extends WordSpec with Matchers with BeforeAndAfterAll {
private lazy val client = Http1Client[IO]().unsafeRunSync()
private lazy val configIO = Config.load("test.conf")
private lazy val config = configIO.unsafeRunSync()
private val server: Option[Http4sServer[IO]] = (for {
builder <- new ServerInstance(configIO).create()
} yield builder.start.unsafeRunSync()).compile.last.unsafeRunSync()

Here we create the client we'll be using to query our API by instantiating the Http1Client provided by the http4s library. We also read a test config that overrides database settings so that we can freely modify the data. We're using an in-memory H2 database, which is destroyed after our test finishes so that we don't need to clean up the state after the test. Then we're building a server by re-using ServerInstance. In contrast to the production code, we're starting it with the start method, which returns a server instance. We'll use this instance after the test to shut down the server. 

Please note how we use unsafeRunSync() in multiple places to evaluate the contents of IO. For the server, we're even doing this twice, once for IO and once for Stream[IO, ...]. This is okay to do in the test code as it helps to keep the testing logic concise.

We need to shut down the client and the server after the test:

override def afterAll(): Unit = {
client.shutdown.unsafeRunSync()
server.foreach(_.shutdown.unsafeRunSync())
}

Again, we're running an IO here because we want the have the shutdown happen right now.

Now, let's take a look at one of the test methods:

"create articles" in {
val eggs = Request[IO](method = Method.POST, uri = Uri.unsafeFromString(s"$rootUrl/articles/eggs"))
client.status(eggs).unsafeRunSync() shouldBe Status.NoContent
val chocolate = Request[IO](method = Method.POST, uri = Uri.unsafeFromString(s"$rootUrl/articles/chocolate"))
client.status(chocolate).unsafeRunSync() shouldBe Status.NoContent
val json = client.expect[Json](s"$rootUrl/inventory").unsafeRunSync()
json shouldBe json"""{"eggs" : 0,"chocolate" : 0}"""
}

Here we first create a test request using a factory provided by http4s. We then check that the API returns the correct NoContent status if we send this request with the client we created earlier in this section. We then create the second article by using the same approach. 

Finally, we're using the client to call the URL directly and let it parse the response to the JSON form. Finally, we check that the inventory has a correct state by comparing the JSON response with circe's JSON literal.

For testing other API calls, we could also provide a request body using circe JSON literals. Please refer to the chapter's source code placed on GitHub to see how this is done.

It is absolutely possible to implement the same testing logic using other HTTP clients or even command-line tools. The Http1Client provided by http4s allows for nice syntax and concise expectation definitions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.123.147