Skip to content

fix: resolve @InjectMockKs dependency order for interface#1526

Merged
Raibaz merged 3 commits into
mockk:masterfrom
neungs-2:fix/1523-injectmockks-interface-dependency-order
Apr 22, 2026
Merged

fix: resolve @InjectMockKs dependency order for interface#1526
Raibaz merged 3 commits into
mockk:masterfrom
neungs-2:fix/1523-injectmockks-interface-dependency-order

Conversation

@neungs-2

Copy link
Copy Markdown
Contributor

Summary

  • Fix @InjectMockKs failing to resolve dependency order when a constructor parameter is an interface (or superclass) type
  • Keep the existing typeToProperty lookup as the fast path and fall back to isSubclassOf only when no exact provider is found, preserving existing performance
  • Add a test for interface-based dependencies

Problem

When @InjectMockKs properties depend on an interface (or any supertype) rather than the concrete provider type, dependency-order resolution introduced in #1500 could not match them. The exact-type lookup never found the implementation, so the graph treated the dependency as absent and the consumer was initialized before its dependency.

interface InterfaceDependency
class InterfaceDependencyImpl : InterfaceDependency
class InterfaceConsumer(val dependency: InterfaceDependency)

class MyTest {
    @InjectMockKs lateinit var consumer: InterfaceConsumer        // depends on InterfaceDependency
    @InjectMockKs lateinit var implementation: InterfaceDependencyImpl
}

Here consumer's InterfaceDependency parameter never matched the InterfaceDependencyImpl provider, so the dependency edge was missing and consumer could be created without the implementation being available.

Solution

Match providers against a constructor parameter type using a subtype check instead of strict equality. To avoid a hot-path regression (subtype reflection is much more expensive than a hash lookup), probe the existing typeToProperty map first and fall back to a subtype scan over its entries only on miss.

typeToProperty[paramType]
    ?: typeToProperty.entries
        .firstOrNull { (providerType, _) -> providerType.isSubclassOf(paramType) }
        ?.value

This covers both interface implementations and class inheritance while keeping the common concrete-type case at O(1) per parameter.

Changes

  • modules/mockk/src/jvmMain/kotlin/io/mockk/impl/annotations/JvmMockInitializer.kt
    • buildDependencyGraph(...):
      • Keep the existing exact-type map lookup as the fast path
      • Fall back to a subtype scan over typeToProperty.entries using KClass.isSubclassOf when no exact provider exists
  • modules/mockk/src/commonTest/kotlin/io/mockk/it/InjectMocksTest.kt
    • Add interfaceDependency() — consumer depends on an interface provided by a sibling @InjectMockKs

Test

  • Interface dependency resolved via implementation provider
  • Existing dependency-order tests (two-level, three-level chain, diamond, circular) continue to pass

Performance impact of DependencyOrder in Mock initialization

I ran JMH benchmarks comparing initWithDependencyOrder vs initWithoutDependencyOrder across different dependency graph shapes and sizes. The nested interface test covers interface nesting up to 5 levels deep.

Focusing on the more stable and realistic case (size = 20, thrpt mode):

• independent: initWithoutDependencyOrder is ~0.8% faster than initWithDependencyOrder
• wide: initWithoutDependencyOrder is ~3.1% faster than initWithDependencyOrder
• linear: initWithoutDependencyOrder is ~5.5% faster than initWithDependencyOrder
• diamond: initWithoutDependencyOrder is ~2.5% faster than initWithDependencyOrder
• interface: initWithoutDependencyOrder is ~9.9% faster than initWithDependencyOrder
• nested interface: initWithoutDependencyOrder is ~14.6% faster than initWithDependencyOrder

Overall, initWithoutDependencyOrder again does not show overhead compared to initWithDependencyOrder. For size = 20, it is on average about 6.1% lower cost per operation. In throughput terms, applying DependencyOrder introduces about 6.7% throughput regression on average for size = 20. Existing cases show 3-5% overhead and show comparable results to previous tests. However, tests with interfaces show performance degradation of up to 14.6%. I think the figure is acceptable because we applied opt-in.

main summary

Benchmark Variant Shape Size Score (ops/s) Units
initWithDependencyOrder independent 5 13,222.457 ops/s
initWithDependencyOrder independent 20 3,330.638 ops/s
initWithDependencyOrder wide 5 12,338.413 ops/s
initWithDependencyOrder wide 20 2,739.984 ops/s
initWithDependencyOrder linear 5 11,800.172 ops/s
initWithDependencyOrder linear 20 2,566.878 ops/s
initWithDependencyOrder diamond 5 11,527.926 ops/s
initWithDependencyOrder diamond 20 2,811.644 ops/s
initWithDependencyOrder interface 5 5,811.512 ops/s
initWithDependencyOrder interface 20 1,243.041 ops/s
initWithDependencyOrder nested interface 5 5,346.561 ops/s
initWithDependencyOrder nested interface 20 953.357 ops/s
initWithoutDependencyOrder independent 5 13,496.209 ops/s
initWithoutDependencyOrder independent 20 3,355.972 ops/s
initWithoutDependencyOrder wide 5 12,679.643 ops/s
initWithoutDependencyOrder wide 20 2,827.488 ops/s
initWithoutDependencyOrder linear 5 12,275.398 ops/s
initWithoutDependencyOrder linear 20 2,715.395 ops/s
initWithoutDependencyOrder diamond 5 12,214.425 ops/s
initWithoutDependencyOrder diamond 20 2,883.256 ops/s
initWithoutDependencyOrder interface 5 6,246.023 ops/s
initWithoutDependencyOrder interface 20 1,380.172 ops/s
initWithoutDependencyOrder nested interface 5 5,759.008 ops/s
initWithoutDependencyOrder nested interface 20 1,116.969 ops/s

overhead summary

Shape Size With DependencyOrder Without DependencyOrder Without vs With
independent 5 13,222.457 13,496.209 -2.0% overhead
independent 20 3,330.638 3,355.972 -0.8% overhead
wide 5 12,338.413 12,679.643 -2.7% overhead
wide 20 2,739.984 2,827.488 -3.1% overhead
linear 5 11,800.172 12,275.398 -3.9% overhead
linear 20 2,566.878 2,715.395 -5.5% overhead
diamond 5 11,527.926 12,214.425 -5.6% overhead
diamond 20 2,811.644 2,883.256 -2.5% overhead
interface 5 5,811.512 6,246.023 -7.0% overhead
interface 20 1,243.041 1,380.172 -9.9% overhead
nested interface 5 5,346.561 5,759.008 -7.2% overhead
nested interface 20 953.357 1,116.969 -14.6% overhead

…interface types

Signed-off-by: logan <gnlee95@gmail.com>
…isSubclassOf only on null

Signed-off-by: logan <gnlee95@gmail.com>
Signed-off-by: logan <gnlee95@gmail.com>
@Raibaz Raibaz merged commit a5c70ba into mockk:master Apr 22, 2026
24 checks passed
@Raibaz

Raibaz commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks a lot for looking into this and for the detailed analysis!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants