Skill v1.0.1
currentAutomated scan100/100+3 new
version: "1.0.1" 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:
- Unit tests — mock the
MutexContendServiceFactory, test your business logic in isolation - TCK (Technology Compatibility Kit) — extend
MutexContendServiceSpecto verify a backend implementation - 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
MutexContendServiceSpecand 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:
import io.mockk.everyimport io.mockk.mockkimport io.mockk.verifyimport me.ahoo.simba.core.MutexContendServiceimport me.ahoo.simba.core.MutexContendServiceFactoryimport me.ahoo.simba.core.MutexContenderimport me.ahoo.simba.core.MutexOwnerimport me.ahoo.simba.core.MutexStateclass MyServiceTest {private val mockFactory = mockk<MutexContendServiceFactory>()private val mockService = mockk<MutexContendService>(relaxed = true)@BeforeEachfun setup() {every { mockFactory.createMutexContendService(any()) } returns mockService}@Testfun `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:
@Testfun `should react to leadership change`() {val contenderSlot = slot<MutexContender>()every { mockFactory.createMutexContendService(capture(contenderSlot)) } returns mockService// Create your service/component that uses Simbaval myComponent = MyComponent(mockFactory)myComponent.start()// Simulate acquiring leadershipval mutexState = MutexState(MutexOwner.NONE, MutexOwner("test-contender"))contenderSlot.captured.onAcquired(mutexState)// Assert your component's behaviormyComponent.isLeader.assert().isTrue()// Simulate losing leadershipval 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:
import me.ahoo.simba.locker.SimbaLocker@Testfun `locker should acquire and release`() {val mockService = mockk<MutexContendService>(relaxed = true)every { mockFactory.createMutexContendService(any()) } returns mockServiceval 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 lockerlocker.close()verify { mockService.stop() }}
TCK — Extending MutexContendServiceSpec
When implementing a new Simba backend, extend the TCK to verify correctness:
import me.ahoo.simba.test.MutexContendServiceSpecclass MyBackendMutexContendServiceTest : MutexContendServiceSpec() {override val mutexContendServiceFactory: MutexContendServiceFactory =MyBackendMutexContendServiceFactory(/* dependencies */)}
This gives you five standard tests:
- `start()` — acquire, verify owner, stop, verify released
- `restart()` — stop and restart, verify full lifecycle repeats
- `guard()` — acquire, wait 3s, verify owner hasn't changed (TTL renewal works)
- `multiContend()` — 10 contenders compete, exactly one owner at any time
- `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:
import me.ahoo.simba.schedule.AbstractSchedulerimport me.ahoo.simba.schedule.ScheduleConfig@Testfun `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:
import me.ahoo.test.asserts.assertvalue.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
- Timing-dependent tests: Distributed lock tests are inherently timing-sensitive. Use generous timeouts (5-30s) and prefer
CountDownLatch/CompletableFutureover newThread.sleepcalls. - Shared mutex names: Each test should use a unique mutex name to avoid cross-test interference. Use
"test-mutex-${UUID.randomUUID()}". - Resource cleanup: Always stop/close services in
@AfterEachto avoid leaked threads and held locks. - Mocking `MutexContendService` vs `MutexContendServiceFactory`: Mock the factory (the DI seam), not the service directly. The factory is what application code injects.
- 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.