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.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add Ahoo-Wang/skills --skill simba-testing --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Simba Testing?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/ahoo-wang-simba-testing)More formats (shields.io, HTML) on the badges page.
---
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 pass ownership changes to `notifyOwner`.
- **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`, call the component under test and mock only the factory seam:
```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 MyComponentTest {
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 myComponent = MyComponent(mockFactory)
myComponent.start()
verify(exactly = 1) { mockService.start() }
}
}
```
### Simulating Leadership Changes
To test code that reacts to `onAcquired`/`onReleased`, capture the contender and pass states through its real `notifyOwner` dispatch:
```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 contender = contenderSlot.captured
val owner = MutexOwner(contender.contenderId)
contender.notifyOwner(MutexState(MutexOwner.NONE, owner))
// Assert your component's behavior
myComponent.isLeader.assert().isTrue()
// Simulate losing leadership
contender.notifyOwner(MutexState(owner, MutexOwner.NONE))
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 stop running service on close`() {
val mockService = mockk<MutexContendService>(relaxed = true)
every { mockFactory.createMutexContendService(any()) } returns mockService
every { mockService.running } returns true
val locker = SimbaLocker("test-lock", mockFactory)
locker.close()
verify { mockService.stop() }
}
```
Stub `running` as true because `close()` calls `stop()` only for an active contend service.
## 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 io.mockk.capture
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
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
import me.ahoo.simba.schedule.AbstractScheduler
import me.ahoo.simba.schedule.ScheduleConfig
import me.ahoo.test.asserts.assert
import org.junit.jupiter.api.Test
import java.time.Duration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@Test
fun `scheduler should run work only when leader`() {
val contenderSlot = slot<MutexContender>()
val mockService = mockk<MutexContendService>(relaxed = true)
val mockFactory = mockk<MutexContendServiceFactory>()
every { mockFactory.createMutexContendService(capture(contenderSlot)) } returns mockService
val leader = AtomicBoolean(false)
val leaderWork = CountDownLatch(1)
val outsideLeadershipWork = 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() {
if (leader.get()) leaderWork.countDown() else outsideLeadershipWork.countDown()
}
}
scheduler.start()
val contender = contenderSlot.captured
val owner = MutexOwner(contender.contenderId)
try {
outsideLeadershipWork.await(200, TimeUnit.MILLISECONDS).assert().isFalse()
leader.set(true)
contender.notifyOwner(MutexState(MutexOwner.NONE, owner))
leaderWork.await(5, TimeUnit.SECONDS).assert().isTrue()
contender.notifyOwner(MutexState(owner, MutexOwner.NONE))
leader.set(false)
outsideLeadershipWork.await(200, TimeUnit.MILLISECONDS).assert().isFalse()
} finally {
if (leader.getAndSet(false)) {
contender.notifyOwner(MutexState(owner, MutexOwner.NONE))
}
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!