Akka TestKit - toolkit for testing Actor systems
TestKit configuration system and Akka extensions for customizing test behavior, timeouts, and environment settings.
Akka extension for accessing TestKit settings and configuration.
object TestKitExtension extends ExtensionId[TestKitSettings] with ExtensionIdProvider {
def apply(system: ActorSystem): TestKitSettings
def apply(system: ExtendedActorSystem): TestKitSettings
def createExtension(system: ExtendedActorSystem): TestKitSettings
def lookup(): ExtensionId[_ <: Extension] = TestKitExtension
}Configuration settings class for TestKit behavior and timeouts.
class TestKitSettings(val config: Config) extends Extension {
import TestKitSettings._
// Core timing settings
val TestTimeFactor: Double
val SingleExpectDefaultTimeout: FiniteDuration
val MultiExpectDefaultTimeout: FiniteDuration
val ExpectNoMessageDefaultTimeout: FiniteDuration
val TestEventFilterLeeway: FiniteDuration
val DefaultTimeout: Timeout
// Utility methods
def dilated(duration: FiniteDuration): FiniteDuration
}
object TestKitSettings {
def apply(system: ActorSystem): TestKitSettings = TestKitExtension(system)
}akka.test {
# Time factor for dilating timeouts in tests
# Set higher for slow test environments (CI, etc.)
timefactor = 1.0
# Default timeout for single message expectations
single-expect-default = 3s
# Default timeout for multi-message expectations
multi-expect-default = 3s
# Default timeout for expectNoMessage calls
expect-no-message-default = 100ms
# Extra time to wait for expected log events in EventFilter
filter-leeway = 3s
# Default dispatcher for test actors
default-timeout = 5s
}Accessing Configuration:
import akka.testkit.{TestKit, TestKitExtension}
class ConfigurationTest extends TestKit(ActorSystem("TestSystem")) {
val settings = TestKitExtension(system)
"TestKit configuration" should {
"provide access to timing settings" in {
settings.TestTimeFactor should be >= 1.0
settings.SingleExpectDefaultTimeout should be > Duration.Zero
settings.MultiExpectDefaultTimeout should be > Duration.Zero
settings.ExpectNoMessageDefaultTimeout should be > Duration.Zero
settings.TestEventFilterLeeway should be > Duration.Zero
settings.DefaultTimeout.duration should be > Duration.Zero
}
"dilate durations correctly" in {
val original = 1.second
val dilated = settings.dilated(original)
dilated should be >= original
dilated should equal(Duration.fromNanos((original.toNanos * settings.TestTimeFactor).toLong))
}
"be accessible through extension" in {
val settingsViaExtension = TestKitExtension(system)
val settingsViaApply = TestKitSettings(system)
settingsViaExtension should be theSameInstanceAs settingsViaApply
}
}
}Override Default Values:
// Custom configuration in application.conf or test configuration
val customConfig = ConfigFactory.parseString("""
akka.test {
timefactor = 2.0
single-expect-default = 5s
multi-expect-default = 10s
expect-no-message-default = 200ms
filter-leeway = 5s
default-timeout = 30s
}
""")
val system = ActorSystem("CustomTestSystem", customConfig)
class CustomConfigTest extends TestKit(system) {
"Custom configuration" should {
"use overridden values" in {
val settings = TestKitExtension(system)
settings.TestTimeFactor should equal(2.0)
settings.SingleExpectDefaultTimeout should equal(5.seconds)
settings.MultiExpectDefaultTimeout should equal(10.seconds)
settings.ExpectNoMessageDefaultTimeout should equal(200.millis)
settings.TestEventFilterLeeway should equal(5.seconds)
settings.DefaultTimeout.duration should equal(30.seconds)
}
}
}Environment-Specific Configuration:
// Different configurations for different environments
object TestConfigurations {
val ciConfig = ConfigFactory.parseString("""
akka.test {
timefactor = 3.0 # Slower CI environment
single-expect-default = 10s
filter-leeway = 10s
}
""")
val localConfig = ConfigFactory.parseString("""
akka.test {
timefactor = 1.0 # Fast local development
single-expect-default = 3s
filter-leeway = 3s
}
""")
val loadTestConfig = ConfigFactory.parseString("""
akka.test {
timefactor = 5.0 # Very slow load testing environment
single-expect-default = 30s
multi-expect-default = 60s
filter-leeway = 15s
}
""")
def getConfig(environment: String): Config = environment match {
case "ci" => ciConfig
case "local" => localConfig
case "load" => loadTestConfig
case _ => ConfigFactory.empty()
}
}
// Usage in tests
class EnvironmentConfigTest {
val environment = sys.env.getOrElse("TEST_ENV", "local")
val config = TestConfigurations.getConfig(environment)
val system = ActorSystem("EnvTestSystem", config)
// Tests will use environment-appropriate timeouts
}Automatic Timeout Usage:
class TimeoutIntegrationTest extends TestKit(ActorSystem("TestSystem")) with DefaultTimeout {
"TestKit with configuration" should {
"use configured timeouts automatically" in {
val actor = system.actorOf(Props(new Actor {
def receive = {
case msg =>
Thread.sleep(100) // Simulate work
sender() ! s"processed: $msg"
}
}))
// Uses SingleExpectDefaultTimeout from configuration
actor ! "test"
expectMsg("processed: test") // No explicit timeout needed
// Uses ExpectNoMessageDefaultTimeout from configuration
expectNoMessage() // Uses configured default
}
"respect time dilation" in {
import akka.testkit._
// All dilated timeouts use TestTimeFactor
within(2.seconds.dilated) {
// Code that might run slowly in some environments
Thread.sleep(100)
expectNoMessage(100.millis.dilated)
}
}
}
}Event Filter Configuration Integration:
class EventFilterConfigTest extends TestKit(ActorSystem("TestSystem")) {
"Event filters with configuration" should {
"use configured filter leeway" in {
val settings = TestKitExtension(system)
// EventFilter internally uses TestEventFilterLeeway for timeout
EventFilter.info(message = "test message").intercept {
// Simulate slow logging
Future {
Thread.sleep(settings.TestEventFilterLeeway.toMillis / 2)
log.info("test message")
}
// Filter waits up to filter-leeway time for the event
}
}
}
}Dynamic Configuration:
class DynamicConfigTest {
def createSystemWithTimeoutFactor(factor: Double): ActorSystem = {
val config = ConfigFactory.parseString(s"""
akka.test.timefactor = $factor
""")
ActorSystem("DynamicTestSystem", config)
}
"Dynamic configuration" should {
"allow runtime configuration changes" in {
val fastSystem = createSystemWithTimeoutFactor(1.0)
val slowSystem = createSystemWithTimeoutFactor(5.0)
val fastKit = new TestKit(fastSystem)
val slowKit = new TestKit(slowSystem)
import fastKit._
val fastSettings = TestKitExtension(fastSystem)
fastSettings.TestTimeFactor should equal(1.0)
import slowKit._
val slowSettings = TestKitExtension(slowSystem)
slowSettings.TestTimeFactor should equal(5.0)
fastSystem.terminate()
slowSystem.terminate()
}
}
}Configuration Validation:
class ConfigValidationTest extends TestKit(ActorSystem("TestSystem")) {
"Configuration validation" should {
"ensure reasonable timeout values" in {
val settings = TestKitExtension(system)
// Validate timeout values are reasonable
settings.SingleExpectDefaultTimeout should be >= 100.millis
settings.MultiExpectDefaultTimeout should be >= settings.SingleExpectDefaultTimeout
settings.TestEventFilterLeeway should be >= 1.second
settings.TestTimeFactor should be >= 1.0
// Validate dilated timeouts don't exceed reasonable bounds
val dilated = settings.dilated(1.minute)
dilated should be <= 10.minutes // Reasonable upper bound
}
"provide consistent configuration access" in {
// Multiple ways to access should return same instance
val settings1 = TestKitExtension(system)
val settings2 = TestKitSettings(system)
settings1 should be theSameInstanceAs settings2
settings1.TestTimeFactor should equal(settings2.TestTimeFactor)
}
}
}// Good: Environment-aware configuration
val timeFactor = if (sys.env.contains("CI")) 3.0 else 1.0
val config = ConfigFactory.parseString(s"akka.test.timefactor = $timeFactor")
// Good: Consistent settings access
val settings = TestKitExtension(system)
val timeout = settings.SingleExpectDefaultTimeout
// Good: Use dilated timeouts everywhere
expectMsg(5.seconds.dilated, expectedMessage)
within(10.seconds.dilated) { /* test code */ }
// Good: Validate configuration in tests
assert(settings.TestTimeFactor >= 1.0, "Time factor should be at least 1.0")
assert(settings.DefaultTimeout.duration > Duration.Zero, "Default timeout must be positive")Performance Issues:
// Problem: Time factor too high for local development
akka.test.timefactor = 10.0 // Makes tests unnecessarily slow locally
// Solution: Environment-specific factors
akka.test.timefactor = ${?TEST_TIME_FACTOR} // Override via environment variableTimeout Issues:
// Problem: Timeouts too short for slow operations
akka.test.single-expect-default = 100ms // Too short for most operations
// Solution: Reasonable defaults with dilation
akka.test.single-expect-default = 3s
// Then use .dilated in tests for environment adaptationFilter Issues:
// Problem: Filter leeway too short for async logging
akka.test.filter-leeway = 100ms // May miss async log events
// Solution: Adequate leeway for async operations
akka.test.filter-leeway = 3sInstall with Tessl CLI
npx tessl i tessl/maven-com-typesafe-akka--akka-testkit-2-13