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.currentfor 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:
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:
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:
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:
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):
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:
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:
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:
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
- Use
TestAspect.sequentialwhen multiple tests in the same suite use different mode aspects to prevent race conditions - Document why modes differ in your handler logic to help future maintainers understand the branching
- Test all modes that your application supports to ensure consistent behavior
- Keep mode-dependent code simple — complex branching can hide bugs; prefer simple guards over intricate logic
- 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
| Aspect | Type | Purpose |
|---|---|---|
HttpTestAspect.devMode | TestAspectAtLeastR[Scope] | Run test under Dev mode |
HttpTestAspect.preprodMode | TestAspectAtLeastR[Scope] | Run test under Preprod mode |
HttpTestAspect.prodMode | TestAspectAtLeastR[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.sequentialfor multiple mode tests - No Manual Cleanup — Automatic restoration means no try-finally needed
See Also
- Dev / Preprod / Prod Modes — Comprehensive guide to application modes
- TestServer — Integration testing with mode configuration
- TestClient — Mocking external services for mode testing
- Testing HTTP Applications — Comprehensive testing guide