How do I test components with injected dependencies?
← Dependency Injection · Ref: Q331
EK9 components are tested by creating test applications with mock/stub registrations. The abstract/concrete pattern makes substitution natural.
TEST APPLICATION PATTERN
Define a test application that registers test doubles instead of production components:
defines application TestApp register MockLogger() as Logger register ServiceUnderTest() as Service
The ServiceUnderTest receives MockLogger through its injection field.
WHY NO @MOCKBEAN
Spring's @MockBean uses runtime reflection to replace beans in the application context. EK9 does not need this: you simply register a different concrete component against the same abstract type. The compiler validates the test wiring just as it validates production wiring.
TEST DOUBLES AS COMPONENTS
Create concrete components that implement the abstract contract with test behaviour:
StubRepository is Repository override find() -> key as String <- result as String: "stub:" + key
This stub is a real component with the same contract as the production implementation.
ADVANTAGES OVER SPRING TESTING
1. NO CONTEXT LOADING: Spring tests often need @SpringBootTest which loads the entire context. EK9 test applications are lightweight.
2. COMPILE-TIME VALIDATION: Even test wiring is validated at compile time.
3. NO REFLECTION: Test doubles are concrete types, not proxy-based mocks.
4. EXPLICIT SUBSTITUTION: You can see exactly what's replaced in the test application.
See Q155 for writing unit tests. See Q227 for compile-time validation. See Q324 for injection basics. See Q234 for component lifecycle.
Example
defines module qa.di.testing.injected defines component <?- Abstract contract for the dependency. -?> EmailSender as abstract send() as abstract -> message as String <- status as String? default operator ? <?- Production implementation. -?> SmtpEmailSender is EmailSender override send() -> message as String <- status as String: "SMTP sent: " + message default operator ? <?- Test double: captures sent messages for verification. -?> StubEmailSender is EmailSender override send() -> message as String <- status as String: "STUB captured: " + message default operator ? NotificationService as abstract notifyUser() as abstract -> userId as String <- result as String? default operator ? DefaultNotificationService is NotificationService sender as EmailSender! override notifyUser() -> userId as String <- result <- String() result: sender.send("Notification for " + userId) default operator ? defines application <?- Production application: uses real email sender. -?> ProductionApp register SmtpEmailSender() as EmailSender register DefaultNotificationService() as NotificationService <?- Test application: substitutes stub email sender. -?> TestApp register StubEmailSender() as EmailSender register DefaultNotificationService() as NotificationService defines program ProductionDemo() with application of ProductionApp stdout <- Stdout() service as NotificationService! result <- service.notifyUser("user-42") stdout.println(result) TestDemo() with application of TestApp stdout <- Stdout() // === TEST APPLICATION SUBSTITUTES STUB FOR PRODUCTION === service as NotificationService! result <- service.notifyUser("user-42") stdout.println(result) stdout.println("Test double injected via test application")
Common mistakes
E08150 — Injection fields must use abstract types. This is what makes test substitution possible: both ProductionApp and TestApp register different concrete types against the same abstract EmailSender. See ek9 -h E08150 for details.
Incorrect:
sender as SmtpEmailSender!
Correct:
sender as EmailSender!
E50001 — Renaming the variable means later references to 'result' become unresolved, triggering E50001. See ek9 -h E50001 for details.
Incorrect:
resultXYZ <- service.notifyUser("user-42")
Correct:
result <- service.notifyUser("user-42")
Other ways to ask this
- How do I mock injected components in EK9?
- What is the EK9 equivalent of @MockBean?
- How do I unit test EK9 components with DI?
Coming from another language?
Java Spring: @MockBean/@SpyBean for test doubles, @SpringBootTest for integration tests, Mockito for mocking. Guice: override bindings in test modules. .NET: replace services in IServiceCollection for tests. Python: unittest.mock, dependency_injector overrides. Go: interface-based testing, manual mock construction. EK9: test applications with substitute component registrations, compile-time validated test wiring, no reflection-based mocking.
Keywords: substitute, migrate, component, unit, double, stub, test, mock, application, inject, verify