| name | mobile-unit-testing |
| description | Expert guidance on writing fast, deterministic unit tests for mobile codebases across Kotlin, Swift, Dart, and TypeScript. Use when asked to structure unit tests, pick a runner, or apply the AAA pattern. |
Mobile Unit Testing
Instructions
Unit tests are the foundation of the mobile test pyramid. They must be hermetic (no network, no filesystem outside temp dirs, no device dependency), deterministic, and fast (< 10 ms each as a rule of thumb). Every other test layer exists to cover gaps unit tests cannot reach — not to compensate for missing unit tests.
1. Scope of a Unit Test
A unit test covers one public behavior of one class or function. It should:
- Run on the host JVM / host Swift / Node / Dart VM — never on an emulator or device.
- Not touch
Context, Application, UIApplication, Activity, WidgetsBinding, or any platform singleton. If it does, it is an integration or instrumentation test.
- Assert on return values or observable state, not on how the code reached that state.
2. Structure: Arrange / Act / Assert
Keep the three phases visually separated. One logical assertion per test; multiple expect lines that assert on the same logical outcome are fine.
Kotlin (JUnit5 + Kotest assertions):
@Test
fun `checkout applies 10 percent loyalty discount`() {
val cart = Cart(items = listOf(item(price = 100)))
val sut = CheckoutCalculator(discount = LoyaltyDiscount(tierPercent = 10))
val total = sut.total(cart)
total shouldBe Money.usd(90)
}
Swift (XCTest):
func test_checkout_appliesTenPercentLoyaltyDiscount() {
let cart = Cart(items: [.item(price: 100)])
let sut = CheckoutCalculator(discount: LoyaltyDiscount(tierPercent: 10))
let total = sut.total(for: cart)
XCTAssertEqual(total, .usd(90))
}
Dart (package:test):
test('checkout applies 10% loyalty discount', () {
final cart = Cart(items: [Item(price: 100)]);
final sut = CheckoutCalculator(discount: LoyaltyDiscount(tierPercent: 10));
expect(sut.total(cart), Money.usd(90));
});
TypeScript (Jest):
test('checkout applies 10% loyalty discount', () => {
const cart = new Cart([{ price: 100 }]);
const sut = new CheckoutCalculator(new LoyaltyDiscount(10));
expect(sut.total(cart)).toEqual(Money.usd(90));
});
3. Naming
Tests are read ten times for every time they are written. Prefer behavioral names:
methodName_condition_expectedResult (Java tradition)
`applies 10 percent loyalty discount when tier is gold` (Kotlin/Swift/Dart)
when X then Y sentences (BDD style)
Avoid testFoo1, testFoo2 — the next reader cannot tell them apart on a failure dashboard.
4. Test Doubles
Prefer fakes over mocks when the collaborator has meaningful state (e.g., a repository, a clock, a feature-flag store). Use mocks only to verify interactions that are the behavior (e.g., "the analytics event was emitted once"). See the mocking-strategies skill for library choices.
5. Parameterized Tests
When the same behavior has many input rows, parameterize instead of copy-pasting:
- Kotlin: JUnit5
@ParameterizedTest + @MethodSource, or Kotest forAll.
- Swift: XCTest has no native parameterization; use
for over a table inside one XCTContext.runActivity.
- Dart: wrap
test calls in a loop over a table of cases.
- TS/Jest:
test.each([...]).
6. What NOT to unit-test
- Framework glue (Activity lifecycle, UIViewController presentation, Navigator stacks) — belongs in UI or integration tests.
- Third-party libraries — trust the library; test your wrapper instead.
- Trivial getters/setters /
data class equality — test the behavior that depends on them.
7. Checklist