Skip to content

[Test Improver] Add unit tests for ConnectOpportunitiesParser#3632

Draft
github-actions[bot] wants to merge 3 commits intomasterfrom
test-assist/connect-opportunities-parser-d3ca5ba4ddf364e7
Draft

[Test Improver] Add unit tests for ConnectOpportunitiesParser#3632
github-actions[bot] wants to merge 3 commits intomasterfrom
test-assist/connect-opportunities-parser-d3ca5ba4ddf364e7

Conversation

@github-actions
Copy link
Copy Markdown
Contributor

🤖 Test Improver — automated AI assistant

Goal and Rationale

ConnectOpportunitiesParser parses the full job opportunity list from the Connect API and had zero test coverage. It contains multiple meaningful branches:

  • Empty-body guard (skips all parsing)
  • Per-entry JSONException handling (corrupt job path via handleCorruptJob)
  • newJobs > 0 gate that controls whether ConnectReleaseTogglesWorker.scheduleOneTimeFetch is called
  • Outer JSONException catch that re-throws as RuntimeException and reports failure to Firebase

Why it matters: This parser controls which opportunities appear in the worker's job list. Silent bugs in any of these branches would either lose valid jobs, silently ignore corrupt entries, or miss triggering the feature-flag refresh.

Approach

New test class: app/unit-tests/src/org/commcare/connect/network/connect/parser/ConnectOpportunitiesParserTest.kt

Tests cover the complete ConnectOpportunitiesParser.parse() method:

Test What it verifies
testEmptyResponseBody_returnsEmptyModel_noStoreJobsCalled Empty body → both lists empty, storeJobs never called
testEmptyJsonArray_returnsEmptyModel_storeJobsCalledOnce [] → both lists empty, storeJobs called once
testSingleValidJob_parsedJobAddedToValidJobsList One valid entry → validJobs has 1 item, corruptJobs empty
testMultipleValidJobs_allAddedToValidJobsList Two valid entries → validJobs has 2 items
testCorruptJobEntry_fromJsonThrowsJSONException_addsToCorruptJobsList fromJson throws JSONException → entry goes to corruptJobs, getName() asserted
testNewJobsGreaterThanZero_schedulesOneTimeFetch storeJobs returns 2 → scheduleOneTimeFetch(context) called exactly once
testNewJobsEqualsZero_doesNotScheduleOneTimeFetch storeJobs returns 0 → scheduleOneTimeFetch never called
testParseSuccess_reportsApiJobsWithCorrectCounts Success path → reportCccApiJobs(true, 1, 3) asserted with specific values
testInvalidJson_throwsRuntimeException Non-array JSON → outer JSONExceptionRuntimeException thrown
testInvalidJson_reportsApiCallWithFailure Non-array JSON → reportCccApiJobs(false, 0, 0) called

Mock strategy:

  • ConnectJobRecord.fromJson / corruptJobFromJsonmockkStatic(ConnectJobRecord::class) (Java static)
  • ConnectJobUtils.storeJobsmockkStatic(ConnectJobUtils::class) (Java static)
  • FirebaseAnalyticsUtil.reportCccApiJobsmockkStatic(FirebaseAnalyticsUtil::class) (Java static)
  • ConnectReleaseTogglesWorker.scheduleOneTimeFetchmockkObject(ConnectReleaseTogglesWorker.Companion) (Kotlin companion object, not @JvmStatic)

This avoids constructing the complex nested JSON required by ConnectJobRecord.fromJson() (which depends on DateUtils.parseDate, ConnectAppRecord.fromJson, ConnectPaymentUnitRecord.fromJson, and ConnectLearnModuleSummaryRecord.fromJson), keeping tests focused on the parser's own branching logic.

Coverage Impact

ConnectOpportunitiesParser.kt previously had 0% coverage. This PR covers all branches of parse():

  • The empty-body guard
  • The per-entry happy path
  • The per-entry JSONException catch → handleCorruptJob
  • The newJobs > 0 gate
  • The outer JSONException catch path

Trade-offs

  • ConnectJobRecord.fromJson is mocked rather than exercised with real JSON — intentional, since fromJson belongs to ConnectJobRecord, not this parser.
  • handleCorruptJob's inner JSONException path (when corruptJobFromJson itself throws) is not covered — it's a secondary error-handling path with no observable side effect beyond a log call.

Test Status

Build not runnable locally (requires ../commcare-core/ sibling, checked out only in CI). ktlintFile unavailable in this environment (Gradle wrapper lock — infrastructure constraint). File follows the same import ordering and annotation style as PushNotificationHelperTest.kt (the existing MockK test in the suite).

To run:

./gradlew testCommcareDebug --tests "org.commcare.connect.network.connect.parser.ConnectOpportunitiesParserTest"

Generated by Daily Test Improver ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/daily-test-improver.md@346204513ecfa08b81566450d7d599556807389f

Tests cover all branches of parse(): empty body guard, valid job
parsing, corrupt job handling (JSONException path), new-jobs gate
for scheduleOneTimeFetch, and the outer JSONException throw path.
Static methods (ConnectJobUtils, FirebaseAnalyticsUtil, ConnectJobRecord)
mocked with MockK mockkStatic; Companion object mocked with mockkObject.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@Test
fun testEmptyResponseBody_returnsEmptyModel_noStoreJobsCalled() {
val inputStream: InputStream = ByteArrayInputStream("".toByteArray())
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's not necessary to add the InputStream type here. Remove the explicit type. The same goes for the rest of the class because this is repeated multiple times.

Comment on lines +67 to +78
@Test
fun testEmptyJsonArray_returnsEmptyModel_storeJobsCalledOnce() {
val inputStream: InputStream = ByteArrayInputStream(JSONArray().toString().toByteArray())
every { ConnectJobUtils.storeJobs(any(), any(), any()) } returns 0
every { FirebaseAnalyticsUtil.reportCccApiJobs(any(), any(), any()) } just Runs

val result = parser.parse(200, inputStream, context)

assertEquals(0, result.validJobs.size)
assertEquals(0, result.corruptJobs.size)
verify(exactly = 1) { ConnectJobUtils.storeJobs(context, any(), true) }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If the json array is empty, we probably should not call storeJobs(). Perhaps we should tweak the original function to not call storeJobs() if there are no jobs to store.

Comment on lines +80 to +93
@Test
fun testSingleValidJob_parsedJobAddedToValidJobsList() {
val mockJob = mockk<ConnectJobRecord>()
val array = JSONArray().apply { put(JSONObject().apply { put("id", 42) }) }
val inputStream: InputStream = ByteArrayInputStream(array.toString().toByteArray())
every { ConnectJobRecord.fromJson(any()) } returns mockJob
every { ConnectJobUtils.storeJobs(any(), any(), any()) } returns 0
every { FirebaseAnalyticsUtil.reportCccApiJobs(any(), any(), any()) } just Runs

val result = parser.parse(200, inputStream, context)

assertEquals(1, result.validJobs.size)
assertEquals(0, result.corruptJobs.size)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need to change this test. Actually, maybe we need a new approach to this entire test class. We expect ConnectJobUtils.storeJobs() to return the number of jobs that have been added. So, ideally, we should be verifying that ConnectJobUtils.storeJobs() returns exactly 1 in this scenario. Also, while we should mock the database transaction itself, we should somehow create some dummy data for the job/opportunity, use that in the tests, and verify that the valid jobs spit out of parser.parse() match the dummy data. In other words, we are mocking too much here.

…icit types

- Remove explicit InputStream type annotations throughout test class
- Guard ConnectJobUtils.storeJobs call: skip when jobs list is empty
- Rework testSingleValidJob to use real JSON and verify parsed job data
- Add validJobJson/appJson helpers for constructing complete job JSON
- Rename testEmptyJsonArray to reflect storeJobs is no longer called

🤖 PR Comment Handler
@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented Apr 8, 2026

Commit pushed: b6cb4fe

Generated by PR Comment Handler

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented Apr 8, 2026

🤖 PR Comment Handler here — I've processed the latest review feedback.

Implemented:

  • ConnectOpportunitiesParserTest.kt (line 58 comment): Removed explicit InputStream type annotations from all val inputStream: InputStream = ByteArrayInputStream(...) declarations throughout the class (addresses @conroy-ricketts's comment)

  • ConnectOpportunitiesParser.kt + ConnectOpportunitiesParserTest.kt (line 78 comment): Added a guard in the parser so ConnectJobUtils.storeJobs() is only called when the jobs list is non-empty (if (jobs.isEmpty()) 0 else ConnectJobUtils.storeJobs(...)). Updated the corresponding test to be named testEmptyJsonArray_returnsEmptyModel_storeJobsNotCalled and verify storeJobs is never called for an empty array (addresses @conroy-ricketts's comment)

  • ConnectOpportunitiesParserTest.kt (line 93 comment): Reworked testSingleValidJob_parsedJobAddedToValidJobsList to use real JSON parsing instead of mocking ConnectJobRecord.fromJson. Added validJobJson(id) and appJson() helpers that construct complete, valid job JSON. The test now: calls unmockkStatic(ConnectJobRecord::class) so real parsing runs; configures storeJobs to return 1; verifies the parsed job has the correct jobId (42) and title ("Test Job 42"); verifies storeJobs was called with the parsed job; and verifies scheduleOneTimeFetch is triggered since one new job was stored (addresses @conroy-ricketts's comment)

Please re-review the latest commit. Happy to make further adjustments.

Generated by PR Comment Handler ·

Move ConnectJobRecord mocking out of setUp/tearDown and into only the
test that requires it (testCorruptJobEntry). All other tests now use
real JSON parsing via the validJobJson helper, removing unnecessary
static mocking throughout the class.

🤖 PR Comment Handler
@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented Apr 9, 2026

Commit pushed: 678c180

Generated by PR Comment Handler

@github-actions
Copy link
Copy Markdown
Contributor Author

github-actions Bot commented Apr 9, 2026

🤖 PR Comment Handler here — I've processed the latest review feedback.

Implemented:

  • ConnectOpportunitiesParserTest.kt: Removed mockkStatic(ConnectJobRecord::class) from setUp() so real JSON parsing is the default for all tests. ConnectJobRecord is now only mocked in testCorruptJobEntry_fromJsonThrowsJSONException_addsToCorruptJobsList, the one test that actually needs to control how fromJson behaves (throwing a JSONException). testMultipleValidJobs, testNewJobsGreaterThanZero, testNewJobsEqualsZero, and testParseSuccess now all use validJobJson for real parsing instead of mocking ConnectJobRecord.fromJson. Addresses @conroy-ricketts's comment about "a new approach to this entire test class" and "we are mocking too much here".

Please re-review the latest commit. Happy to make further adjustments.

Generated by PR Comment Handler ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant