Skip to content

Add Kotlin unit test infrastructure and CircleCI step#156

Closed
pavelzeman wants to merge 2 commits intomattermost:masterfrom
pavelzeman:chore/add-kotlin-test-infrastructure
Closed

Add Kotlin unit test infrastructure and CircleCI step#156
pavelzeman wants to merge 2 commits intomattermost:masterfrom
pavelzeman:chore/add-kotlin-test-infrastructure

Conversation

@pavelzeman
Copy link
Copy Markdown
Contributor

@pavelzeman pavelzeman commented Mar 20, 2026

Summary

Add infrastructure for running Kotlin unit tests in CI. Currently the repo has one Kotlin test file (ExponentialRetryInterceptorTest.kt) but no test dependencies in build.gradle and no CI step to run them.

The Android side of this library has 36 Kotlin files totaling ~3,250 lines of code — including interceptors for auth, retry, compression, timeouts, and metrics. That's a decent amount of logic with no test coverage in CI. This PR fixes that.

Ticket Link

N/A — infrastructure improvement to support ongoing Sentry issue fixes.

Release Note

NONE

Changes

  • android/build.gradle: Add testImplementation dependencies for JUnit 4, Mockito, and OkHttp MockWebServer
  • test-runner/: Standalone Gradle project with wrapper for running Kotlin unit tests independently of the React Native build. Includes a smoke test to verify the infrastructure works.
  • .circleci/config.yml: Add kotlin-tests job using cimg/android executor, wired as a gate for build-package

Why a standalone test-runner?

The Android module (android/build.gradle) is a library that depends on React Native at build time, making it difficult to run ./gradlew test standalone. The test-runner project provides a lightweight Kotlin+JVM environment with OkHttp dependencies — enough to test interceptor logic without the full React Native build chain.

What this enables

  • TDD for Android interceptor code (e.g. header validation, retry logic)
  • CI enforcement of Kotlin test correctness
  • Foundation for upcoming interceptor bug fixes (#155)

Summary by CodeRabbit

  • Tests
    • Established automated Kotlin testing infrastructure with CircleCI integration.
    • Added test runner module with comprehensive testing dependencies including JUnit and mocking tools.
    • Implemented smoke test to validate testing environment configuration.
    • Test results are now automatically published as artifacts in the CI/CD pipeline.

pavelzeman and others added 2 commits March 20, 2026 00:36
Add JUnit 4, Mockito, and OkHttp MockWebServer as test dependencies.
These are needed to run the existing ExponentialRetryInterceptorTest and
future interceptor tests.

Co-authored-by: Claude <claude@anthropic.com>
- Add JUnit 4, Mockito, and OkHttp MockWebServer to android/build.gradle
  test dependencies
- Add standalone test-runner project with Gradle wrapper for running
  Kotlin unit tests independently of the React Native build
- Add kotlin-tests job to CircleCI using cimg/android executor
- Add smoke test to verify the test infrastructure works
- Wire kotlin-tests into the build workflow as a gate for build-package

This enables TDD for Android interceptor code without requiring a full
React Native app build.

Co-authored-by: Claude <claude@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This pull request adds a new Kotlin test infrastructure for the project. It introduces a test-runner module with Gradle/Kotlin build configuration, includes test dependencies in the Android module, adds a smoke test validating OkHttp MockWebServer integration, and updates CircleCI to run these tests before packaging.

Changes

Cohort / File(s) Summary
CircleCI Configuration
.circleci/config.yml
Added android executor using cimg/android:2024.01, created kotlin-tests job to run ./gradlew test in test-runner and publish JUnit results and HTML test reports as artifacts. Updated build workflow to run kotlin-tests before build-package.
Test Runner Module Setup
test-runner/build.gradle.kts, test-runner/settings.gradle.kts, test-runner/gradle/wrapper/gradle-wrapper.properties
Configured new test-runner module with Kotlin/JVM plugin (v1.9.22), set project name to interceptor-tests, added dependencies for OkHttp, JUnit, and MockWebServer, and configured Gradle 8.5 distribution.
Gradle Wrapper Scripts
test-runner/gradlew, test-runner/gradlew.bat
Added POSIX shell and Windows batch Gradle wrapper scripts to bootstrap builds with OS detection, Java resolution, and proper classpath configuration.
Test Dependencies
android/build.gradle
Added testImplementation dependencies: junit:junit:4.13.2, org.mockito:mockito-core:5.11.0, and com.squareup.okhttp3:mockwebserver:4.12.0.
Smoke Test Implementation
test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt
Added smoke test validating Kotlin test framework and OkHttp MockWebServer integration by verifying HTTP 200 responses and body content.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main purpose of the changeset: adding Kotlin unit test infrastructure and integrating it into CircleCI workflows.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can customize the tone of the review comments and chat replies.

Configure the tone_instructions setting to customize the tone of the review comments and chat replies. For example, you can set the tone to Act like a strict teacher, Act like a pirate and more.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
.circleci/config.yml (1)

69-83: Consider caching Gradle artifacts in kotlin-tests to speed CI.

This job will re-download Gradle deps each run. Adding cache can significantly reduce pipeline time.

⚙️ Optional CI optimisation
   kotlin-tests:
     executor: android
     steps:
       - checkout
+      - restore_cache:
+          keys:
+            - gradle-test-runner-{{ checksum "test-runner/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "test-runner/build.gradle.kts" }}
+            - gradle-test-runner-
       - run:
           name: Run Kotlin unit tests
           command: |
             cd test-runner
             ./gradlew test
+      - save_cache:
+          key: gradle-test-runner-{{ checksum "test-runner/gradle/wrapper/gradle-wrapper.properties" }}-{{ checksum "test-runner/build.gradle.kts" }}
+          paths:
+            - ~/.gradle/caches
+            - ~/.gradle/wrapper
       - store_test_results:
           path: test-runner/build/test-results
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.circleci/config.yml around lines 69 - 83, The kotlin-tests job currently
runs ./gradlew test every time and lacks dependency caching; update the
kotlin-tests job to add CircleCI cache steps (restore_cache before running
Gradle and save_cache after) targeting Gradle caches like ~/.gradle and project
.gradle (e.g., ~/.gradle/caches and test-runner/.gradle), use stable cache keys
that include a checksum of the Gradle wrapper or gradle.properties to bust when
dependencies change, and ensure the new restore_cache/save_cache steps are
placed around the existing checkout and ./gradlew test steps so the ./gradlew
test run benefits from the cached artifacts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test-runner/build.gradle.kts`:
- Around line 15-21: The test runner currently limits sourceSets.test to only
Kotlin sources, excluding Java tests under android/src/test/java (e.g.,
ExponentialRetryInterceptorTest); update the test source set configuration in
build.gradle.kts so test sourceSet includes both Kotlin and Java test
directories (add the Java test srcDir or configure srcDirs to include
"src/test/java" alongside "src/test/kotlin") so that tests for interceptor
implementations in android/src/main/java/... are executed by the CI; modify the
block that references sourceSets { test { kotlin { srcDir("src/test/kotlin") } }
} to include the Java source directory for tests.

In `@test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt`:
- Around line 17-30: The MockWebServer instance `server` is shut down only after
assertions, which can leak the server if an assertion fails; wrap the test body
that starts and uses `server` (the `MockWebServer` variable `server`, the
request/response flow using `client.newCall(request).execute()` and `response`)
in a try/finally so `server.shutdown()` is always called in finally; also ensure
the `Response`/`response.body` is closed in the try (or via use/close) before
leaving so resources are released.

---

Nitpick comments:
In @.circleci/config.yml:
- Around line 69-83: The kotlin-tests job currently runs ./gradlew test every
time and lacks dependency caching; update the kotlin-tests job to add CircleCI
cache steps (restore_cache before running Gradle and save_cache after) targeting
Gradle caches like ~/.gradle and project .gradle (e.g., ~/.gradle/caches and
test-runner/.gradle), use stable cache keys that include a checksum of the
Gradle wrapper or gradle.properties to bust when dependencies change, and ensure
the new restore_cache/save_cache steps are placed around the existing checkout
and ./gradlew test steps so the ./gradlew test run benefits from the cached
artifacts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c3475ae3-2982-498b-b50f-259bf0411ce6

📥 Commits

Reviewing files that changed from the base of the PR and between dd42247 and 3203b9e.

⛔ Files ignored due to path filters (1)
  • test-runner/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
📒 Files selected for processing (8)
  • .circleci/config.yml
  • android/build.gradle
  • test-runner/build.gradle.kts
  • test-runner/gradle/wrapper/gradle-wrapper.properties
  • test-runner/gradlew
  • test-runner/gradlew.bat
  • test-runner/settings.gradle.kts
  • test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt

Comment on lines +15 to +21
sourceSets {
test {
kotlin {
srcDir("src/test/kotlin")
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify which test/source dirs are wired into test-runner
rg -n "sourceSets|srcDir|srcDirs" test-runner/build.gradle.kts -C2

# Confirm interceptor tests currently live outside test-runner
rg -n "class ExponentialRetryInterceptorTest" android/src/test/java/com/mattermost/networkclient/ExponentialRetryInterceptorTest.kt -C2

Repository: mattermost/react-native-network-client

Length of output: 368


🏁 Script executed:

#!/bin/bash
# Find all test files in android/src/test/java
find android/src/test/java -name "*.java" -o -name "*.kt" | head -20

# Check if android/src/main/java exists and what it contains
ls -la android/src/main/ 2>/dev/null || echo "No android/src/main/"

# Verify current test-runner structure
cat test-runner/build.gradle.kts | head -30

Repository: mattermost/react-native-network-client

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Check what's in android/src/main/java
find android/src/main/java -type f -name "*.java" -o -name "*.kt" 2>/dev/null

# Check specifically for interceptor implementations
rg -n "class.*Interceptor" android/src/main/java --type java --type kotlin

# Verify if there are any imports or references between test-runner and android
rg -n "android" test-runner/build.gradle.kts

Repository: mattermost/react-native-network-client

Length of output: 3778


🏁 Script executed:

#!/bin/bash
# Check what's in test-runner/src/test/kotlin
find test-runner/src/test/kotlin -type f 2>/dev/null | head -20

# Check test-runner directory structure
tree test-runner -L 2 2>/dev/null || find test-runner -type f -name "*.kt" -o -name "*.gradle.kts"

# Check if there's any documentation or README about test-runner
cat test-runner/README.md 2>/dev/null || cat test-runner/readme.md 2>/dev/null || echo "No README found"

Repository: mattermost/react-native-network-client

Length of output: 324


🏁 Script executed:

#!/bin/bash
# Check the full build.gradle.kts to understand context better
cat test-runner/build.gradle.kts

# Check if settings.gradle.kts references android module
cat test-runner/settings.gradle.kts

# Verify the actual test file in test-runner to understand what it imports
cat test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt

Repository: mattermost/react-native-network-client

Length of output: 1394


CI gate currently misses existing Android interceptor tests.

With sourceSets.test limited to src/test/kotlin, tests under android/src/test/java/... (e.g., ExponentialRetryInterceptorTest) are not included by this runner. The test-runner only executes SmokeTest.kt, which validates the test infrastructure itself rather than the actual interceptor implementations available in android/src/main/java/com/mattermost/networkclient/interceptors/. This gap weakens the stated interceptor-coverage goal.

💡 Suggested direction
 sourceSets {
+    main {
+        kotlin {
+            srcDir("../android/src/main/java")
+        }
+    }
     test {
         kotlin {
-            srcDir("src/test/kotlin")
+            srcDirs(
+                "src/test/kotlin",
+                "../android/src/test/java"
+            )
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
sourceSets {
test {
kotlin {
srcDir("src/test/kotlin")
}
}
}
sourceSets {
main {
kotlin {
srcDir("../android/src/main/java")
}
}
test {
kotlin {
srcDirs(
"src/test/kotlin",
"../android/src/test/java"
)
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test-runner/build.gradle.kts` around lines 15 - 21, The test runner currently
limits sourceSets.test to only Kotlin sources, excluding Java tests under
android/src/test/java (e.g., ExponentialRetryInterceptorTest); update the test
source set configuration in build.gradle.kts so test sourceSet includes both
Kotlin and Java test directories (add the Java test srcDir or configure srcDirs
to include "src/test/java" alongside "src/test/kotlin") so that tests for
interceptor implementations in android/src/main/java/... are executed by the CI;
modify the block that references sourceSets { test { kotlin {
srcDir("src/test/kotlin") } } } to include the Java source directory for tests.

Comment on lines +17 to +30
val server = MockWebServer()
server.enqueue(MockResponse().setBody("ok").setResponseCode(200))
server.start()

val client = OkHttpClient()
val request = Request.Builder()
.url(server.url("/test"))
.build()

val response = client.newCall(request).execute()
Assert.assertEquals(200, response.code)
Assert.assertEquals("ok", response.body?.string())

server.shutdown()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether cleanup is protected by finally/use in the smoke test
rg -n "server.shutdown\\(|execute\\(\\)\\.use|try|finally" test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt -C2

Repository: mattermost/react-native-network-client

Length of output: 190


Wrap test cleanup in finally block to prevent resource leaks on assertion failures.

The server.shutdown() call runs only if all assertions pass. If any assertion fails, the MockWebServer remains open, potentially causing test flakiness in subsequent tests.

Suggested fix
     fun mockWebServer_basicRequestResponse() {
         val server = MockWebServer()
         server.enqueue(MockResponse().setBody("ok").setResponseCode(200))
         server.start()
-
-        val client = OkHttpClient()
-        val request = Request.Builder()
-            .url(server.url("/test"))
-            .build()
-
-        val response = client.newCall(request).execute()
-        Assert.assertEquals(200, response.code)
-        Assert.assertEquals("ok", response.body?.string())
-
-        server.shutdown()
+        try {
+            val client = OkHttpClient()
+            val request = Request.Builder()
+                .url(server.url("/test"))
+                .build()
+
+            client.newCall(request).execute().use { response ->
+                Assert.assertEquals(200, response.code)
+                Assert.assertEquals("ok", response.body?.string())
+            }
+        } finally {
+            server.shutdown()
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test-runner/src/test/kotlin/com/mattermost/networkclient/SmokeTest.kt` around
lines 17 - 30, The MockWebServer instance `server` is shut down only after
assertions, which can leak the server if an assertion fails; wrap the test body
that starts and uses `server` (the `MockWebServer` variable `server`, the
request/response flow using `client.newCall(request).execute()` and `response`)
in a try/finally so `server.shutdown()` is always called in finally; also ensure
the `Response`/`response.body` is closed in the try (or via use/close) before
leaving so resources are released.

@pavelzeman
Copy link
Copy Markdown
Contributor Author

Superseded by org-branch PR (155→#160, 156→#159, 157→#158). Closing fork PR.

@pavelzeman pavelzeman closed this Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant