Skip to main content

HttpTestAspect

HttpTestAspect is a ZIO Test aspect utility that temporarily configures HTTP application modes for testing. It provides composable test aspects to run specific tests under different deployment modes (Dev, Preprod, Prod), allowing verification of mode-dependent handler behavior without affecting other tests. Mode settings automatically restore after each test.

The public API provides three mode aspects:

object HttpTestAspect {
val devMode: TestAspectAtLeastR[Scope]
val preprodMode: TestAspectAtLeastR[Scope]
val prodMode: TestAspectAtLeastR[Scope]
}

Key properties:

  • Test Composition — Acts as a ZIO Test aspect applicable with @@ operator
  • Automatic Restoration — Previous mode automatically restores after test completes
  • Scoped Mode Change — Uses system property to set Mode.current for test duration
  • No Test Mutation — Does not affect other tests in the same suite

Role in Module

HttpTestAspect is the test utility for mode-dependent behavior verification in zio-http-testkit.

Complements: TestServer (mode-dependent routes), TestClient (mode-dependent external calls), Mode (checks current mode in handlers)

Motivation

HTTP applications often have mode-dependent behavior: different error handling in development vs. production, extra diagnostics in staging, stricter validation in production. Testing this behavior requires temporarily switching the application mode for specific tests.

HttpTestAspect solves this by providing composable aspects that set the mode for the duration of a test and restore the previous mode afterward:

test("handles errors in dev mode") {
// Test logic here
assertTrue(Mode.isDev)
} @@ HttpTestAspect.devMode

Use HttpTestAspect when:

  • You want to test mode-specific error handling or resource allocation
  • You need to verify feature flags that depend on the mode
  • Your test suite tests multiple modes and needs isolation

Mode Types

Three modes are available:

  • Mode.Dev — Development mode (default). Typically enables extra diagnostics, verbose error messages, and developer-friendly defaults.
  • Mode.Preprod — Pre-production/staging mode. An intermediate mode between dev and production for testing production-like behavior in a safe environment.
  • Mode.Prod — Production mode. Typically strict validation, minimal error details, optimized performance.

Using HttpTestAspect

To test mode-dependent behavior, apply mode aspects to your tests.

Basic Mode Override

Apply a mode aspect to a test to set the mode for that test only:

zio-http-example-testing/src/test/scala/example/testing/ModeBasicSetup.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object ModeBasicSetup extends ZIOSpecDefault {
def spec = suite("Mode Testing")(
test("development mode enables verbose logging") {
for {
currentMode <- ZIO.succeed(Mode.current)
} yield assertTrue(currentMode == Mode.Dev)
} @@ HttpTestAspect.devMode,

test("production mode disables verbose logging") {
for {
currentMode <- ZIO.succeed(Mode.current)
} yield assertTrue(currentMode == Mode.Prod)
} @@ HttpTestAspect.prodMode,

test("modes are isolated between tests") {
for {
modeInProd <- ZIO.succeed(Mode.current)
} yield assertTrue(modeInProd == Mode.Prod)
} @@ HttpTestAspect.prodMode,
) @@ TestAspect.sequential
}

(source)

Reading the Mode

Inside a test, read the current mode using:

import zio._
import zio.http._

// Full mode value
val mode: Mode = Mode.current

// Convenience checks
val isDev = Mode.isDev
val isPreprod = Mode.isPreprod
val isProd = Mode.isProd

Core Operations

The following examples demonstrate testing different modes.

Testing Dev Mode Behavior

Enable extra diagnostics and verbose error handling:

zio-http-example-testing/src/test/scala/example/testing/TestAspectDevMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectDevMode extends ZIOSpecDefault {
def spec = (test("error response includes stack trace in dev mode") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)

_ <- TestServer.addRoute {
Method.GET / "error" -> handler { (_: Request) =>
(ZIO.fail(new Exception("Something went wrong")): ZIO[Any, Throwable, Response])
.catchAll { err =>
val message = if (Mode.isDev) err.toString else "Internal Server Error"
ZIO.succeed(Response.status(Status.InternalServerError).addHeader("X-Error", message))
}
}
}

response <- client(Request.get(URL.root.port(port) / "error"))
} yield assertTrue(
response.status == Status.InternalServerError,
Mode.isDev
)
}.provide(TestServer.default, Client.default, Scope.default)) @@ HttpTestAspect.devMode
}

(source)

Testing Prod Mode Behavior

Enforce stricter validation and optimized error handling:

zio-http-example-testing/src/test/scala/example/testing/TestAspectProdMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectProdMode extends ZIOSpecDefault {
def spec = (test("request validation is stricter in prod mode") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)

_ <- TestServer.addRoute {
Method.POST / "data" -> handler { (req: Request) =>
val lenientValidation = Mode.isDev
req.body.asString
.map { body =>
if (body.isEmpty && !lenientValidation)
Response.status(Status.BadRequest)
else
Response.ok
}
.catchAll { _ =>
ZIO.succeed(Response.status(Status.BadRequest))
}
}
}

// Send empty body
response <- client(Request.post(URL.root.port(port) / "data", Body.empty))
} yield assertTrue(
response.status == Status.BadRequest,
Mode.isProd
)
}.provide(TestServer.default, Client.default, Scope.default)) @@ HttpTestAspect.prodMode
}

(source)

Testing Preprod Mode Behavior

Test production-like behavior safely in staging:

zio-http-example-testing/src/test/scala/example/testing/TestAspectPreprodMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectPreprodMode extends ZIOSpecDefault {
def spec = (test("preprod mode enables test endpoints") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)

// Routes only available in preprod and dev
testRoutes = if (Mode.isProd) Routes.empty else Routes(
Method.GET / "test" / "reset" -> handler(Response.ok)
)

_ <- TestServer.addRoutes(testRoutes)

response <- client(Request.get(URL.root.port(port) / "test" / "reset"))
} yield assertTrue(
response.status == Status.Ok,
Mode.isPreprod
)
}.provide(TestServer.default, Client.default, Scope.default)) @@ HttpTestAspect.preprodMode
}

(source)

Multiple Mode Tests with Sequential Execution

When testing multiple modes in the same suite, use TestAspect.sequential to avoid race conditions (mode is a JVM-wide setting):

zio-http-example-testing/src/test/scala/example/testing/TestAspectMultiMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectMultiMode extends ZIOSpecDefault {
def spec = suite("Multi-Mode Testing")(
test("dev mode allows verbose output") {
assertTrue(Mode.isDev)
} @@ HttpTestAspect.devMode,

test("preprod mode is intermediate") {
assertTrue(Mode.isPreprod)
} @@ HttpTestAspect.preprodMode,

test("prod mode is strict") {
assertTrue(Mode.isProd)
} @@ HttpTestAspect.prodMode,
) @@ TestAspect.sequential
}

(source)

Common Patterns

Mode-Conditional Routes

Define routes that only exist in certain modes:

import zio._
import zio.http._

val debugRoutes =
if (Mode.isDev || Mode.isPreprod) Routes(
Method.GET / "debug" / "health" -> handler(Response.text("OK"))
)
else Routes.empty

val appRoutes = Routes(
Method.GET / "health" -> handler(Response.ok)
) ++ debugRoutes

Mode-Conditional Error Handling

Customize error responses based on mode:

handler { (_: Request) =>
ZIO.fail(new Exception("Something went wrong"))
.catchAll { err =>
if (Mode.isDev || Mode.isPreprod)
ZIO.succeed(
Response.status(Status.InternalServerError)
.addHeader("X-Error", err.getMessage)
.addHeader("X-Trace", err.getStackTrace.mkString("\n"))
)
else
ZIO.succeed(Response.status(Status.InternalServerError))
}
}

Mode-Conditional Configuration

Adapt server configuration and resources based on mode:

// Use mode to determine resource allocation
val resourcePoolSize = if (Mode.isProd) 16 else 4
val enableVerboseLogging = Mode.isDev

// Configure handlers based on mode
val handlerConfig = if (Mode.isProd) {
// Strict timeout in production
ZIO.succeed(5000)
} else {
// Lenient timeout in development
ZIO.succeed(30000)
}

Testing Feature Flags

Test handlers that use mode-based feature flags:

zio-http-example-testing/src/test/scala/example/testing/TestAspectFeatureFlag.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectFeatureFlag extends ZIOSpecDefault {
def spec = (test("new feature is disabled in prod") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)

_ <- TestServer.addRoute {
Method.GET / "new-feature" -> handler { (_: Request) =>
if (Mode.isProd)
ZIO.succeed(Response.status(Status.NotFound))
else
ZIO.succeed(Response.text("Feature enabled"))
}
}

response <- client(Request.get(URL.root.port(port) / "new-feature"))
} yield assertTrue(
response.status == Status.NotFound,
Mode.isProd
)
}.provide(TestServer.default, Client.default, Scope.default)) @@ HttpTestAspect.prodMode
}

(source)

Integration with Other Types

TestServer — Use mode aspects with TestServer integration tests to verify mode-dependent route behavior:

zio-http-example-testing/src/test/scala/example/testing/TestAspectTestServerMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectTestServerMode extends ZIOSpecDefault {
def spec = (test("server returns different response in prod mode") {
for {
client <- ZIO.service[Client]
port <- ZIO.serviceWithZIO[Server](_.port)

_ <- TestServer.addRoute {
Method.GET / "api" -> handler { (_: Request) =>
if (Mode.isProd)
ZIO.succeed(Response.text("Production response"))
else
ZIO.succeed(Response.text("Development response"))
}
}

response <- client(Request.get(URL.root.port(port) / "api"))
body <- response.body.asString
} yield assertTrue(body == "Production response")
}.provide(TestServer.default, Client.default, Scope.default)) @@ HttpTestAspect.prodMode
}

(source)

TestClient — Use mode aspects when testing handlers that call external services conditionally based on mode:

zio-http-example-testing/src/test/scala/example/testing/TestAspectTestClientMode.scala
package example.testing

import zio._
import zio.http._
import zio.test._

object TestAspectTestClientMode extends ZIOSpecDefault {
def spec = (test("uses mock service in dev mode") {
for {
client <- ZIO.service[Client]

// Configure mock external service
_ <- TestClient.addRoute {
Method.GET / "external" -> handler(Response.json("""{"mock": true}"""))
}

// Call it
response <- client(Request.get(URL.root / "external"))
body <- response.body.asString
} yield assertTrue(body.contains("mock"))
}.provide(TestClient.layer, Scope.default)) @@ HttpTestAspect.devMode
}

(source)

Best Practices

  1. Use TestAspect.sequential when multiple tests in the same suite use different mode aspects to prevent race conditions
  2. Document why modes differ in your handler logic to help future maintainers understand the branching
  3. Test all modes that your application supports to ensure consistent behavior
  4. Keep mode-dependent code simple — complex branching can hide bugs; prefer simple guards over intricate logic
  5. Combine with config — For complex environment-dependent behavior (secrets, database URLs), use a dedicated config service alongside modes

API Reference

The following test aspects and mode query functions comprise the complete public API.

Test Aspects

AspectTypePurpose
HttpTestAspect.devModeTestAspectAtLeastR[Scope]Run test under Dev mode
HttpTestAspect.preprodModeTestAspectAtLeastR[Scope]Run test under Preprod mode
HttpTestAspect.prodModeTestAspectAtLeastR[Scope]Run test under Prod mode

Usage Pattern

Apply all aspects using ZIO Test's @@ operator:

test("my test") {
// test logic
} @@ HttpTestAspect.devMode

Mode Queries in Handlers

See Reading the Mode above for available query functions.

Implementation Details

  • Mechanism — Uses system property "zio.http.mode" to set mode
  • Scope — Mode change is scoped to test duration only
  • Restoration — Previous mode is captured and restored after test
  • Thread-Safe — Mode is JVM-wide; use TestAspect.sequential for multiple mode tests
  • No Manual Cleanup — Automatic restoration means no try-finally needed

See Also