Testing
Apion uses ScalaTest with async support and provides test utilities for both unit and integration testing.
Test Setup
Section titled “Test Setup”Add ScalaTest to your build.sbt:
libraryDependencies += "org.scalatest" %%% "scalatest" % "3.2.19" % TestBase Test Classes
Section titled “Base Test Classes”Async Tests
Section titled “Async Tests”For testing handlers and middleware that return Future:
import io.github.edadma.apion._import org.scalatest.freespec.AsyncFreeSpecimport org.scalatest.matchers.should.Matchers
class MyTests extends AsyncFreeSpec with Matchers { // MacrotaskExecutor is provided by Apion's package object implicit override def executionContext = scala.scalajs.concurrent.MacrotaskExecutor
"my handler" - { "returns 200" in { myHandler(mockRequest()).map { case Complete(response) => response.status shouldBe 200 case other => fail(s"Expected Complete, got $other") } } }}Unit Testing Handlers
Section titled “Unit Testing Handlers”Test individual handlers with mock requests:
def mockServerRequest( method: String = "GET", url: String = "/", headers: Map[String, String] = Map(),): ServerRequest = { val req = js.Dynamic.literal( method = method, url = url, headers = js.Dictionary(headers.toSeq*), on = (_: String, _: js.Function1[js.Any, Unit]) => js.Dynamic.literal(), socket = js.Dynamic.literal( remoteAddress = "127.0.0.1", remotePort = 12345, ), ) req.asInstanceOf[ServerRequest]}
val request = Request.fromServerRequest(mockServerRequest( method = "POST", url = "/api/users?active=true", headers = Map( "Content-Type" -> "application/json", "Authorization" -> "Bearer token123", ),))Integration Testing
Section titled “Integration Testing”Spin up a real server and test with fetch:
class ApiTests extends AsyncFreeSpec with Matchers with BeforeAndAfterAll {
var httpServer: NodeServer = _ val port = 3001
override def beforeAll(): Unit = { val server = Server() .use(LoggingMiddleware()) .get("/users/:id", request => { val id = request.params("id") Map("id" -> id).asJson })
httpServer = server.listen(port) {} }
override def afterAll(): Unit = { if (httpServer != null) httpServer.close(() => ()) }
"GET /users/:id" - { "returns user" in { fetch(s"http://localhost:$port/users/123") .toFuture .flatMap { response => response.status shouldBe 200 response.json().toFuture } .map { json => val str = js.JSON.stringify(json) str should include("123") } } }}Testing Middleware
Section titled “Testing Middleware”Test that middleware modifies requests or blocks them:
"AuthMiddleware" - { "rejects unauthenticated requests" in { fetch(s"http://localhost:$port/api/protected") .toFuture .map(_.status shouldBe 401) }
"allows authenticated requests" in { val token = AuthMiddleware.createAccessToken("test", Set("user"), config) val options = js.Dynamic.literal( headers = js.Dictionary("Authorization" -> s"Bearer $token") )
fetch(s"http://localhost:$port/api/protected", options) .toFuture .map(_.status shouldBe 200) }}Mock File System
Section titled “Mock File System”For testing StaticMiddleware:
val mockFiles = Map( "public/index.html" -> mockFile("<html>Hello</html>", isDirectory = false, "644"), "public/style.css" -> mockFile("body{}", isDirectory = false, "644"),)
val mockFs = new MockFS(mockFiles)
val middleware = StaticMiddleware("public", StaticMiddleware.Options(), mockFs)- Use different ports for each test suite to avoid conflicts
- Clean up servers in
afterAllto prevent port leaks - For async assertions, return the
Futurefrom the test — ScalaTest handles it - Use
eventuallyblocks for testing asynchronous state changes