How do I mock dependencies in EK9 tests without Mockito?

← Testing · Ref: Q808

EK9 does not need Mockito, unittest.mock, or any mocking library. Test doubles are ordinary implementations of a contract, chosen either by the caller or by dependency injection.

THE PATTERN

1. Define the behaviour as an abstract contract
2. Production code depends on the contract, not the concrete implementation
3. In tests, provide a test implementation of the contract
4. For injected dependencies, an application block selects which implementation is registered

WHY NO MOCKING FRAMEWORK

Mocking frameworks exist because languages allow tight coupling to concrete classes. In EK9:
- Code depends on traits or abstract components, not concrete types
- The DI system wires implementations at compile time
- Test implementations are just another implementation of the contract
- No reflection, no bytecode manipulation, no proxy objects

TRAIT-BASED TEST DOUBLES (passed directly)
A class implements a trait with 'with trait of':

  defines trait
    Formatter
      format() as pure abstract
        -> number as Integer
        <- rtn as String?
  defines class
    SimpleFormatter with trait of Formatter
      override format() as pure
        -> number as Integer
        <- rtn as String: $number

Any code that takes a Formatter can be handed the test double.

INJECTED TEST DOUBLES (components)
Only components can be registered and injected - registering a class, or registering anything 'as' a trait, is E50020. So the injected contract is an abstract component, the test double is wired in an application, and the program runs with that application:

  defines component
    DataStore as abstract
      fetch() as abstract
        -> key as String
        <- rtn as String?
      default operator ?
    FakeDataStore extends DataStore
      override fetch()
        -> key as String
        <- rtn as String: "test-value"
      default operator ?
  defines application
    TestApp
      register FakeDataStore() as DataStore
  defines program
    MockingDemo() with application of TestApp
      stdout <- Stdout()
      store as DataStore!
      stdout.println(store.fetch("user"))

The program prints test-value. A ProductionApp registers RealDataStore() as DataStore instead; the program code does not change.

ADVANTAGES

- Compile-time verified: missing method implementations caught immediately
- No magic: test doubles are normal classes and components
- Reusable: same test double works across all tests
- Type-safe: cannot accidentally mock the wrong method signature

See Q227 for DI basics. See Q324 for DI deep dive. See Q155 for basic testing.

Example

defines module qa.testdeep.mocking

  defines trait

    <?-
      The trait defines the contract.
      Production and test code both implement this.
    -?>
    Formatter
      format() as pure abstract
        -> number as Integer
        <- rtn as String?

  defines class

    // === PRODUCTION IMPLEMENTATION ===

    FancyFormatter with trait of Formatter
      override format() as pure
        -> number as Integer
        <- rtn as String: `[${number}]`

    // === TEST DOUBLE ===

    SimpleFormatter with trait of Formatter
      override format() as pure
        -> number as Integer
        <- rtn as String: $number

  defines component

    // === INJECTED CONTRACT: an abstract component, not a trait ===

    DataStore as abstract
      fetch() as abstract
        -> key as String
        <- rtn as String?

      default operator ?

    RealDataStore extends DataStore
      override fetch()
        -> key as String
        <- rtn as String: `db-value-for-${key}`

      default operator ?

    FakeDataStore extends DataStore
      override fetch()
        -> key as String
        <- rtn as String: "test-value"

      default operator ?

  defines application

    // Production wiring
    ProductionApp
      register RealDataStore() as DataStore

    // Test wiring: the fake replaces the real store
    TestApp
      register FakeDataStore() as DataStore

  defines program

    MockingDemo() with application of TestApp
      stdout <- Stdout()

      // Trait doubles: pass whichever implementation you want
      fancy <- FancyFormatter()
      stdout.println(fancy.format(42))

      simple <- SimpleFormatter()
      stdout.println(simple.format(42))

      // DI doubles: TestApp injects FakeDataStore
      store as DataStore!
      stdout.println(store.fetch("user"))

      stdout.println("No Mockito needed")
      stdout.println("Traits and components replace mocking frameworks")

Common mistakes

E50020 — A class implements a trait with 'with trait of', not 'extends' — using 'extends' on a trait is an incompatible genus (class vs trait). See ek9 -h E50020 for details.

Incorrect:

FancyFormatter extends Formatter

Correct:

FancyFormatter with trait of Formatter
Other ways to ask this
  • What is the EK9 equivalent of Mockito?
  • How do I use traits for test doubles in EK9?
  • How do I test components with injected dependencies in EK9?

Coming from another language?

Java: Mockito mock()/when()/verify(), Spring @MockBean. Python: unittest.mock.patch(), MagicMock. Rust: mockall crate with #[automock]. Go: interfaces + hand-written fakes (standard pattern). Kotlin: MockK or Mockito-kotlin. EK9: trait implementations as test doubles passed directly, or abstract components with an application that registers the fake - no mocking library needed.

Keywords: double, fake, injection, application, test, component, stub, trait, interface, mock, dependency, mockito