Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add setup for functional tests. Test sending events over the local variable limit. #85

Merged
merged 14 commits into from
Oct 30, 2023

Conversation

wzieba
Copy link
Collaborator

@wzieba wzieba commented Oct 29, 2023

Description

This PR adds a setup for writing and running functional tests on CI. Contrary to unit tests, they are meant to be written via black-box technique. The only mocked/faked element of the setup is API, which is intentionally mocked via MockWebServer.

Additionally, it adds a test case for tracking number of events exceeding maximum number of events that are stored in memory. This asserts correctness of saving/querying data from the local file as well as shows the benefits of functional tests.

Note

Important pivot how to approach the proposed test was made in 8d4a0d3 so please don't pay a lot of attention to implementation details introduced in commit before, which is e97a7a5.

How to test

Green light on CI should be enough. You can also download the build artifact and see results of instrumentation tests.

- add dependencies
- add test scaffold
- introduce an empty, test-only activity
With address of mocked server as `ROOT_URL`, applied via reflection. `ROOT_URL` *must not* be a plain declaration, because otherwise the compiler will *inline* the value of the constant, and we won't be able to change it, even via reflection, for the functional test.
By using AndroidX test orchestrator with `clearPackageData` flag. Each test should run in homogeneous environment. This change asserts, that the file used to store events locally won't be shared between tests.
This commit adds test that checks if sending events over the limit of 50 events, works as intended. To not use flaky `Thread#sleep` approach, this test checks for:
- local storage file changes via `java.nio.file.WatchService` API
- finish of HTTP requests via `okhttp3.mockwebserver.takeRequest` API

The `waitForFileEvents` method works in a way as `app.cash.turbine`, without a timeout though.
This commit changes the approach for functional tests to more blackbox style, which seems to be more suitable here. As we don't care for implementation details (e.g. how or if events are stored locally), we no longer observe file changes.
of triggered HTTP request.
On a separate job, as it has to run on macOS.
of unit and instrumentation tests
@wzieba wzieba requested a review from ParaskP7 October 29, 2023 17:45
Copy link
Collaborator

@ParaskP7 ParaskP7 left a comment

Choose a reason for hiding this comment

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

👋 @wzieba !

I have reviewed and tested this PR as per the instructions, everything works as expected, another great job, more testing, more loving! 🌟 🧪 ❤️


I have left few questions (❓), a couple of suggestions (💡) and some minor (🔍) comments for you to consider. I am going to approve this PR anyway, since none is blocking. I am NOT going to merge this PR yet to give you some time to apply any of my suggestions. However, feel free to ignore them and merge the PR yourself.

parsely/build.gradle Show resolved Hide resolved
parsely/build.gradle Outdated Show resolved Hide resolved
// Waits for the SDK to send events (flush interval passes)
val requestPayload = server.takeRequest().toMap()
assertThat(requestPayload["events"]).hasSize(51)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Question (❓): Although I understand how this test is working and what it tried to verify, I feel that something is missing here, an assertion of some kind, something that will showcase what was documented on the test case above:

  • The SDK will save the events to disk...
  • ...and send them in the next flush interval.
  • At the end, when all events are sent...
  • ...the SDK will delete the content of local storage file.

I mean, can we assert that the SDK is saving the events first? For example, will it save all 51 of them, or only the 1 event that exceed the threshold, something else? Then, what happens during the first flush interval, what happens during the next flush interval, these kind of question. Apologies for trying to push things, I am just trying to review this as a very naive test reader that would like to understand this SDK through testing, assuming I don't have any prior knowledge or the SDK. Wdyt, too much? 🤔

PS: I also naively tried to add something basic, the assertThat(locallyStoredEvents).isNotEmpty before the waitFor { locallyStoredEvents.isEmpty() }, to see if that will pass, but it failed. 🤷

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I mean, can we assert that the SDK is saving the events first?

This is something I pivoted from in 8d4a0d3. IMO, the functional tests shouldn't check for such implementation details. The things we're interested in/testing here are:

  • that SDK has sent the same number of events consumer app tracked (asserted in line 65)
  • that SDK doesn't store additional data (line 69). Actually, we shouldn't even check this probably in a functional test, but this is a safe-check as I know we have some issues with events duplications.

Then, what happens during the first flush interval, what happens during the next flush interval, these kind of question.

There is only one flush, and it's tested 👍 I'll be extending this tests suite to cover more cases, like multiple flushes, but here we test "what would happen if user recorded more than 50 events quickly".

PS: I also naively tried to add something basic, the assertThat(locallyStoredEvents).isNotEmpty before the waitFor { locallyStoredEvents.isEmpty() }, to see if that will pass, but it failed. 🤷

Good point, but it's only timing thing. It seems that assertions above take long enough to give SDK time to clean the file.

Try this:

Subject: [PATCH] refactor: move the class to Kotlin

This class is relatively simple and difficult to test because of `java.utils.Timer`. That's why I decided to make migration to Kotlin right away, without unit tests coverage first.
---
Index: parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt
--- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt	(revision 5900d93eff388bc5b2d98c44072cee07d91b1a96)
+++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt	(date 1698666962291)
@@ -62,11 +62,11 @@
 
             // Waits for the SDK to send events (flush interval passes)
             val requestPayload = server.takeRequest().toMap()
+            assertThat(locallyStoredEvents).isNotEmpty
             assertThat(requestPayload["events"]).hasSize(51)
 
             // Wait a moment to give SDK time to delete the content of local storage file
             waitFor { locallyStoredEvents.isEmpty() }
-            assertThat(locallyStoredEvents).isEmpty()
         }
     }

and the test should fail!

WDYT?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the reply @wzieba ! 👍

This is something I pivoted from in 8d4a0d3. IMO, the functional tests shouldn't check for such implementation details. The things we're interested in/testing here are:

  • that SDK has sent the same number of events consumer app tracked (asserted in line 65)
  • that SDK doesn't store additional data (line 69). Actually, we shouldn't even check this probably in a functional test, but this is a safe-check as I know we have some issues with events duplications.

👍 if you are also 👍 with that, I just wanted to question it a bit... 😊

There is only one flush, and it's tested 👍 I'll be extending this tests suite to cover more cases, like multiple flushes, but here we test "what would happen if user recorded more than 50 events quickly".

Thanks for the clarification on that. 👍

Good point, but it's only timing thing. It seems that assertions above take long enough to give SDK time to clean the file.

Try this:

  • assertThat(locallyStoredEvents).isNotEmpty

and the test should fail!

WDYT?

Yea, it is hard to assert those things... 😭

FYI: The test should fail, I don't understand that, the test should success with this change, is it not? It failing was the actual problem I had as well, as I would have expected it to succeed before waiting... 🤔

PS: Apologies, I am a bit confused it seems... 🤷

  • assertThat(locallyStoredEvents).isEmpty()

Why did you remove this assertion from the patch you shared? 🤔

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, sorry I made some confusion with the diff! Please ignore the diff in previous comment. But it's still case of timing - check this:

Subject: [PATCH] fix: add `intern` to `ROOT_URL` of emulator localhost
---
Index: parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt
--- a/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt	(revision d2a25dbc864396d67dc517943a5a079cffb86304)
+++ b/parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt	(date 1698668794333)
@@ -71,6 +71,7 @@
     }
 
     private fun RecordedRequest.toMap(): Map<String, List<Event>> {
+        assertThat(locallyStoredEvents).isEmpty()
         val listType: TypeReference<Map<String, List<Event>>> =
             object : TypeReference<Map<String, List<Event>>>() {}
 

In this case the test will fail although, in theory, it shouldn't because after making the request, SDK will clean the local file. But, as the things happen on different threads, we don't have certainty what will happen first - assertion, or file deletion.

In case of this test - the assertions and JSON mapping take enough time, that, at least in theory, we shouldn't need to wait. Yet, relying on "JSON serialization being slow" as a determiner of test success is super flaky. Hence, the waitFor method.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But it's still case of timing...

Thanks for sharing a new diff, and I see, thanks for explaining again! ✅

I don't want to block this PR, feel free to merge it when you think it is ready, I just wanted to question it a bit just to get a better idea on the actual value of this functional test and how you would that be of use later on. 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the questions! We can always iterate over how those tests look now. For now, I think this test brings value - we're sure that saving and getting data from the local file is working fine and doesn't produce duplicates.

Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 💯 🥇

@codecov
Copy link

codecov bot commented Oct 30, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (2bec4fe) 44.23% compared to head (e6727a0) 44.38%.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #85      +/-   ##
==========================================
+ Coverage   44.23%   44.38%   +0.15%     
==========================================
  Files           8        8              
  Lines         364      365       +1     
  Branches       56       56              
==========================================
+ Hits          161      162       +1     
  Misses        189      189              
  Partials       14       14              
Files Coverage Δ
...ava/com/parsely/parselyandroid/ParselyTracker.java 11.89% <100.00%> (+0.47%) ⬆️

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Because we only now run instrumentation tests in the `debug` build type by default, we no longer need to configure `debug` as the `testBuildType`.
@wzieba wzieba merged commit fcb6d7c into main Oct 30, 2023
4 checks passed
@wzieba wzieba deleted the functional_tests branch October 30, 2023 14:41
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.

None yet

2 participants