---
name: simba-testing
description: Guide for testing Simba distributed lock and leader-election code. Use when writing or reviewing tests for MutexContender, SimbaLocker, AbstractScheduler, backend TCK conformance via MutexContendServiceSpec, Redis/JDBC/Zookeeper integration tests, timing-sensitive lock behavior, or new Kotlin assertions in Simba-based code.
---

# Testing Simba-Based Code

## Test Strategy Overview

Simba testing has three layers:
1. **Unit tests** — mock the `MutexContendServiceFactory`, test your business logic in isolation
2. **TCK (Technology Compatibility Kit)** — extend `MutexContendServiceSpec` to verify a backend implementation
3. **Integration tests** — run against a real backend (Redis, MySQL, Zookeeper)

Choose the simplest layer that gives confidence. Most application code only needs unit tests with mocks. Backend implementors need TCK + integration tests.

Before writing a test, decide:
- **Application behavior**: mock `MutexContendServiceFactory`, capture the contender, and trigger callbacks directly.
- **Backend implementation**: extend `MutexContendServiceSpec` and run against the real backend.
- **Scheduler behavior**: verify leadership gating separately from the business logic in `work()`.

## Unit Tests with MockK

For application code that injects `MutexContendServiceFactory`, mock it:

```kotlin
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import me.ahoo.simba.core.MutexContendService
import me.ahoo.simba.core.MutexContendServiceFactory
import me.ahoo.simba.core.MutexContender
import me.ahoo.simba.core.MutexOwner
import me.ahoo.simba.core.MutexState

class MyServiceTest {
    private val mockFactory = mockk<MutexContendServiceFactory>()
    private val mockService = mockk<MutexContendService>(relaxed = true)

    @BeforeEach
    fun setup() {
        every { mockFactory.createMutexContendService(any()) } returns mockService
    }

    @Test
    fun `should start contend service`() {
        val contender = MyContender()
        val service = mockFactory.createMutexContendService(contender)
        service.start()

        verify { service.start() }
    }
}
```

### Simulating Leadership Changes

To test code that reacts to `onAcquired`/`onReleased`, capture the contender and invoke callbacks directly:

```kotlin
@Test
fun `should react to leadership change`() {
    val contenderSlot = slot<MutexContender>()
    every { mockFactory.createMutexContendService(capture(contenderSlot)) } returns mockService

    // Create your service/component that uses Simba
    val myComponent = MyComponent(mockFactory)
    myComponent.start()

    // Simulate acquiring leadership
    val mutexState = MutexState(MutexOwner.NONE, MutexOwner("test-contender"))
    contenderSlot.captured.onAcquired(mutexState)

    // Assert your component's behavior
    myComponent.isLeader.assert().isTrue()

    // Simulate losing leadership
    val releasedState = MutexState(MutexOwner("test-contender"), MutexOwner.NONE)
    contenderSlot.captured.onReleased(releasedState)

    myComponent.isLeader.assert().isFalse()
}
```

### SimbaLocker Unit Tests

Mock the factory and verify the locker lifecycle:

```kotlin
import me.ahoo.simba.locker.SimbaLocker

@Test
fun `locker should acquire and release`() {
    val mockService = mockk<MutexContendService>(relaxed = true)
    every { mockFactory.createMutexContendService(any()) } returns mockService

    val locker = SimbaLocker("test-lock", mockFactory)
    // acquire() will block, so in unit tests we typically don't call it directly
    // Instead test the code that uses the locker
    locker.close()

    verify { mockService.stop() }
}
```

## TCK — Extending MutexContendServiceSpec

When implementing a new Simba backend, extend the TCK to verify correctness:

```kotlin
import me.ahoo.simba.test.MutexContendServiceSpec

class MyBackendMutexContendServiceTest : MutexContendServiceSpec() {
    override val mutexContendServiceFactory: MutexContendServiceFactory =
        MyBackendMutexContendServiceFactory(/* dependencies */)
}
```

This gives you five standard tests:
1. **`start()`** — acquire, verify owner, stop, verify released
2. **`restart()`** — stop and restart, verify full lifecycle repeats
3. **`guard()`** — acquire, wait 3s, verify owner hasn't changed (TTL renewal works)
4. **`multiContend()`** — 10 contenders compete, exactly one owner at any time
5. **`schedule()`** — AbstractScheduler lifecycle, work executes on leader

### Backend-Specific Test Requirements

| Backend | External dependency | Notes |
|---------|-------------------|-------|
| Redis | Running Redis instance | Current repository tests use `RedisStandaloneConfiguration` defaults |
| JDBC | Running MySQL instance | Current repository tests use `jdbc:mysql://localhost:3306/simba_db`, `root`/`root`; init script: `simba-jdbc/src/init-script/init-simba-mysql.sql` |
| Zookeeper | None | Uses Curator's `TestingServer` (embedded) |

Do not silently add Testcontainers to this repository's tests. If CI isolation is required, add the dependency and Gradle wiring intentionally, then update the backend setup code and this skill together.

## AbstractScheduler Tests

Test that scheduled work runs only on the leader:

```kotlin
import me.ahoo.simba.schedule.AbstractScheduler
import me.ahoo.simba.schedule.ScheduleConfig

@Test
fun `scheduler should run work only when leader`() {
    val workLatch = CountDownLatch(1)
    val scheduler = object : AbstractScheduler("test-scheduler", mockFactory) {
        override val config = ScheduleConfig.delay(Duration.ZERO, Duration.ofMillis(100))
        override val worker = "test"
        override fun work() {
            workLatch.countDown()
        }
    }

    scheduler.start()
    // Simulate acquiring leadership by triggering the contender
    // ...

    workLatch.await(5, TimeUnit.SECONDS).assert().isTrue()
    scheduler.stop()
}
```

## Assertion Style

Use `fluent-assert` for new Kotlin assertions:
```kotlin
import me.ahoo.test.asserts.assert

value.assert().isEqualTo(expected)
collection.assert().hasSize(3)
bool.assert().isTrue()
```

Do not churn existing Hamcrest/AssertJ assertions solely for style. When adding or touching assertions, prefer `.assert()` and keep the local test readable.

## Common Test Pitfalls

1. **Timing-dependent tests**: Distributed lock tests are inherently timing-sensitive. Use generous timeouts (5-30s) and prefer `CountDownLatch` / `CompletableFuture` over new `Thread.sleep` calls.
2. **Shared mutex names**: Each test should use a unique mutex name to avoid cross-test interference. Use `"test-mutex-${UUID.randomUUID()}"`.
3. **Resource cleanup**: Always stop/close services in `@AfterEach` to avoid leaked threads and held locks.
4. **Mocking `MutexContendService` vs `MutexContendServiceFactory`**: Mock the factory (the DI seam), not the service directly. The factory is what application code injects.
5. **Testing `AbstractScheduler` without Simba**: If you only need to test the `work()` method, call it directly. The scheduler pattern is about leadership gating, not the work itself.
