Loading...
Loading...
Manual dependency injection using SystemContext (production) and TestContext (test doubles) patterns for Kotlin. Use when structuring service dependencies, wiring application components, or creating test contexts without DI frameworks.
npx skill4agent add anderssv/the-example kotlin-context-diopen class SystemContext {
interface Repositories {
val customerRepo: CustomerRepository
val orderRepo: OrderRepository
}
open val repositories: Repositories by lazy {
object : Repositories {
override val customerRepo by lazy { CustomerRepositoryImpl(dataSource) }
override val orderRepo by lazy { OrderRepositoryImpl(dataSource) }
}
}
open val customerService by lazy {
CustomerService(repositories.customerRepo)
}
open val orderService by lazy {
OrderService(repositories.orderRepo, customerService)
}
}lazyDataSourceSystemContextdataSourceclass SystemTestContext : SystemContext() {
class TestRepositories : Repositories {
override val customerRepo = CustomerRepositoryFake() // concrete type!
override val orderRepo = OrderRepositoryFake() // concrete type!
}
val testRepositories = TestRepositories()
override val repositories: Repositories get() = testRepositories
}TestRepositories.customerRepoCustomerRepositoryFakeCustomerRepositoryrepositories.customerRepoCustomerRepositorytestRepositories.customerRepoCustomerRepositoryFake@Test
fun testOrderCreation() {
with(SystemTestContext()) {
// Arrange - use service methods to set up state
customerService.registerCustomer(Customer.valid())
// Act
val order = orderService.createOrder(customerId, items)
// Assert — direct access to fake methods, no casting needed
assertThat(testRepositories.orderRepo.getSavedOrders())
.contains(order)
}
}with(SystemTestContext()) { ... }open val customerService = CustomerService(repositories.customerRepo)open val customerService by lazy {
CustomerService(repositories.customerRepo)
}lazyopen class SystemContext(private val config: Config) {
// Data layer
interface Repositories {
val userRepo: UserRepository
val productRepo: ProductRepository
}
// External services
interface Clients {
val paymentClient: PaymentClient
val emailClient: EmailClient
}
// Infrastructure
interface Infrastructure {
val database: Database
val cache: Cache
}
open val infrastructure: Infrastructure by lazy {
object : Infrastructure {
override val database by lazy { DatabaseImpl(config.dbUrl) }
override val cache by lazy { RedisCache(config.redisUrl) }
}
}
open val repositories: Repositories by lazy {
object : Repositories {
override val userRepo by lazy { UserRepositoryImpl(infrastructure.database) }
override val productRepo by lazy { ProductRepositoryImpl(infrastructure.database) }
}
}
open val clients: Clients by lazy {
object : Clients {
override val paymentClient by lazy { PaymentClientImpl(config.paymentApiKey) }
override val emailClient by lazy { EmailClientImpl(config.smtpConfig) }
}
}
// Business logic
open val userService by lazy { UserService(repositories.userRepo) }
open val orderService by lazy {
OrderService(
repositories.productRepo,
clients.paymentClient,
clients.emailClient
)
}
}class SystemTestContext : SystemContext(Config.test()) {
class TestRepositories : Repositories {
override val userRepo = UserRepositoryFake() // concrete type
override val productRepo = ProductRepositoryFake() // concrete type
}
class TestClients : Clients {
override val paymentClient = PaymentClientFake() // concrete type
override val emailClient = EmailClientFake() // concrete type
}
val testRepositories = TestRepositories()
override val repositories: Repositories get() = testRepositories
val testClients = TestClients()
override val clients: Clients get() = testClients
}@Test
fun testEmailSent() {
with(SystemTestContext()) {
orderService.completeOrder(orderId)
// Direct access to fake methods via testClients — no casting
assertThat(testClients.emailClient.sentEmails).hasSize(1)
assertThat(testClients.emailClient.sentEmails[0].subject)
.contains("Order Confirmed")
}
}
@Test
fun testPaymentFailure() {
with(SystemTestContext()) {
// Configure fake behavior — no casting
testClients.paymentClient.failOnNextCharge()
val result = orderService.createOrder(request)
assertThat(result.status).isEqualTo(OrderStatus.PAYMENT_FAILED)
}
}@Test
fun `should save order`() {
with(SystemTestContext()) {
// Fresh fakes with no accumulated state
orderService.createOrder(request)
assertThat(testRepositories.orderRepo.getSavedOrders()).hasSize(1)
}
}
@Test
fun `should not save order when payment fails`() {
with(SystemTestContext()) {
// Independent from the test above
testClients.paymentClient.failOnNextCharge()
orderService.createOrder(request)
assertThat(testRepositories.orderRepo.getSavedOrders()).isEmpty()
}
}OrderRepositoryFakeEmailClientFakewith(SystemTestContext()) { ... }// Production interface — nullable because config may not exist
interface Clients {
val authClient: AuthClient?
val notificationClient: NotificationClient?
}
// Test implementation — non-nullable
class TestClients : Clients {
override val authClient = AuthClientStub() // non-nullable!
override val notificationClient = NotificationClientStub() // non-nullable!
}class CustomerRepositoryFake : CustomerRepository {
private val db = mutableMapOf<String, Customer>()
override fun save(customer: Customer) {
db[customer.id] = customer
}
override fun findById(id: String): Customer? {
return db[id]
}
// Test-specific methods (not in interface)
fun getSavedCustomers(): List<Customer> = db.values.toList()
fun failOnNextSave() { /* ... */ }
}class SystemTestContext : SystemContext() {
class TestRepositories : Repositories {
override val customerRepo = CustomerRepositoryFake() // concrete type
}
val testRepositories = TestRepositories()
override val repositories: Repositories get() = testRepositories
}customerServiceCustomerRepositoryFakerepositories.customerRepotestRepositories.customerRepofun main() {
val context = SystemContext(Config.fromEnvironment())
// Start application with context
val app = Application(
context.orderService,
context.userService
)
app.start()
}fun Application.module() {
val context = SystemContext(Config.fromEnvironment())
routing {
get("/orders/{id}") {
val orderId = call.parameters["id"]!!
val order = context.orderService.getOrder(orderId)
call.respond(order)
}
post("/orders") {
val request = call.receive<CreateOrderRequest>()
val order = context.orderService.createOrder(request)
call.respond(order)
}
}
}// Don't do this — forces test subclasses to satisfy constructor parameters
open class Repositories(private val dataSource: DataSource) {
open val customerRepo: CustomerRepository = CustomerRepositoryImpl(dataSource)
}// Don't do this
val emailClient = clients.emailClient as EmailClientFake
assertThat(emailClient.sentEmails).hasSize(1)testClients.emailClient// Don't do this unless needed
open val customerService by lazy { CustomerService(...) }
open val orderService by lazy { OrderService(...) }
open val productService by lazy { ProductService(...) }// Too complex
open class DatabaseContext : InfrastructureContext()
open class RepositoryContext : DatabaseContext()
open class ServiceContext : RepositoryContext()
open class SystemContext : ServiceContext()// Don't mix patterns
@Inject lateinit var customerService: CustomerService // Framework DI
val orderService = OrderService(repositories.orderRepo) // Manual DI