7.0 KiB
/*
- Copyright (c) 2026 Meshtastic LLC
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with this program. If not, see https://www.gnu.org/licenses/. */
:core:testing — Shared Test Doubles and Utilities
Purpose
The :core:testing module provides lightweight, reusable test doubles (fakes, builders, factories) and testing utilities for all KMP modules. This module consolidates testing dependencies into a single, well-controlled location to:
- Reduce duplication: Shared fakes (e.g.,
FakeNodeRepository,FakeRadioController) used across multiple modules. - Keep dependency graph clean: All test doubles and libraries are defined once; modules depend on
:core:testinginstead of scattered test deps. - Enable KMP-wide test patterns: Every module (
commonTest,androidUnitTest, JVM tests) can reuse the same fakes. - Maintain purity: Core business logic modules (e.g.,
core:domain,core:data) depend on:core:testingviacommonTest, avoiding test-code leakage into production.
Dependency Strategy
┌─────────────────────────────────────┐
│ core:testing │
│ (only deps: core:model, │
│ core:repository, test libs) │
└──────────────┬──────────────────────┘
↑
│ (commonTest dependency)
┌──────┴─────────────┬────────────────────┐
│ │ │
core:domain feature:messaging feature:node
core:data feature:settings feature:firmware
(etc.) (etc.)
Target Compatibility Warning (March 2026 Audit)
- MockK Removal: MockK has been removed from
commonMainbecause it does not natively support Kotlin/Native (iOS). - Future-Proofing: The project is migrating to
dev.mokkeryfor KMP-compatible mocking or favoring manual fakes. - Recommendation: Favor manual fakes (like
FakeNodeRepository) incommonMainto maintain pure KMP portability.
Key Design Rules
-
:core:testinghas NO dependencies on heavy modules: It only depends on:core:model— Domain types (Node, User, etc.)core:repository— Interfaces (NodeRepository, etc.)- Test libraries (
kotlin("test"),kotlinx.coroutines.test,turbine,junit)
-
No circular dependencies: Modules that depend on
:core:testing(incommonTest) cannot be dependencies of:core:testingitself. -
:core:testingis NOT part of the app bundle: It's declared incommonTestsourceSet only, so it never appears in release APKs or final JARs.
What's Included
Test Doubles (Fakes)
FakeRadioController
A no-op implementation of RadioController for unit tests. Tracks method calls and state changes.
val radioController = FakeRadioController()
radioController.setConnectionState(ConnectionState.Connected)
assertEquals(1, radioController.sentPackets.size)
FakeNodeRepository
An in-memory implementation of NodeRepository for isolated testing.
val nodeRepo = FakeNodeRepository()
nodeRepo.setNodes(TestDataFactory.createTestNodes(5))
assertEquals(5, nodeRepo.nodeDBbyNum.value.size)
Test Builders & Factories
TestDataFactory
Factory methods for creating domain objects with sensible defaults.
val node = TestDataFactory.createTestNode(num = 42, longName = "Alice")
val nodes = TestDataFactory.createTestNodes(10)
Test Utilities
Flow collection helper
val emissions = flow { emit(1); emit(2) }.toList()
assertEquals(listOf(1, 2), emissions)
Usage Examples
Testing a ViewModel (in feature:messaging/src/commonTest)
class MessageViewModelTest {
private val nodeRepository = FakeNodeRepository()
@Test
fun testLoadsNodesCorrectly() = runTest {
nodeRepository.setNodes(TestDataFactory.createTestNodes(3))
val viewModel = createViewModel(nodeRepository)
assertEquals(3, viewModel.nodeCount.value)
}
}
Testing a UseCase (in core:domain/src/commonTest)
class SendMessageUseCaseTest {
private val radioController = FakeRadioController()
@Test
fun testSendsPacket() = runTest {
val useCase = SendMessageUseCase(radioController)
useCase.sendMessage(testPacket)
assertEquals(1, radioController.sentPackets.size)
}
}
Adding New Test Doubles
When adding a new fake to :core:testing:
- Implement the interface from
core:modelorcore:repository. - Track side effects (e.g.,
sentPackets,calledMethods) for test assertions. - Provide test helpers (e.g.,
setNodes(),clear()) to manipulate state. - Document with examples in the class KDoc.
Example:
/**
* A test double for [SomeRepository].
*/
class FakeSomeRepository : SomeRepository {
val callHistory = mutableListOf<String>()
override suspend fun doSomething(value: String) {
callHistory.add(value)
}
// Test helpers
fun getCallCount() = callHistory.size
fun clear() = callHistory.clear()
}
Dependency Maintenance
When adding a new module:
- If it has
commonTesttests, addimplementation(projects.core.testing)to itscommonTest.dependencies. - Do NOT add heavy modules (e.g.,
core:database) to:core:testing's dependencies.
When a test needs a mock:
- Check
:core:testingfirst for an existing fake. - If none exists, consider adding it there (if it's reusable) vs. using
mockk()inline.
When updating interfaces:
- Update corresponding fakes in
:core:testingto match new method signatures. - Keep fakes no-op; don't replicate business logic.
Files
core/testing/
├── build.gradle.kts # Lightweight, minimal dependencies
├── README.md # This file
└── src/commonMain/kotlin/org/meshtastic/core/testing/
├── FakeRadioController.kt # RadioController test double
├── FakeNodeRepository.kt # NodeRepository test double
└── TestDataFactory.kt # Builders and factories
See Also
AGENTS.md§3B: KMP platform purity guidelines (relevant for test code).docs/kmp-status.md: KMP module status and targets..github/copilot-instructions.md: Build and test commands.