-
Notifications
You must be signed in to change notification settings - Fork 6
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
Extract EngagementManager
and cover it with unit tests
#82
Conversation
So this method can be accessed by extracted `EngagementManager`
The logic itself is complex and should be unit tested. Also, the production implementation will return long intervals (like 10s). Because `EngagementManager` uses TimerTask we have to use `Thread#sleep` in unit tests. All in all - with default implementation of `updateLatestInterval` the unit tests would last very long, hence this extraction, which will allow to inject custom intervals.
To improve readability in tests.
To validate that the tracked event is correct.
The test will contain many unchecked casts, but they're safe - they'll always be <String, Any>.
As per comment: `inc` will always be 0 because the interval between events is lower than 1s
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #82 +/- ##
==========================================
+ Coverage 32.95% 42.06% +9.10%
==========================================
Files 5 7 +2
Lines 355 359 +4
Branches 56 56
==========================================
+ Hits 117 151 +34
+ Misses 225 194 -31
- Partials 13 14 +1
☔ View full report in Codecov by Sentry. |
4c8c002
to
b9f8797
Compare
private static final long OFFSET_MATCHING_BASE_INTERVAL = 35; | ||
private static final double BACKOFF_PROPORTION = 0.3; | ||
|
||
long updateLatestInterval(@NonNull final Calendar startTime) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unit tests for this class are planned to be added in a separate PR.
There was a problem hiding this 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 testing job well done! 🌟 🧪 🌟
But wow, that is a very convoluted test, I am very sorry you had to go through all this... 🫂
I have left a warning (
EXTRAS
- Suggestion (💡): I have noticed that during you refactor commits some of them wouldn't be able to build, just because it would require this other subsequent commit that you add later on. I would personally merge those 2 commits together and make it easier for the reviewer to understand that in order to (for example) extract a class, one would also need to make some methods more public (package private). 🤔
- Suggestion (💡): In this PR, I am also seeing some additional commits that are not part of this PR. Consider rebasing you branch with the
main
one before open a PR for review. This will make it easier for reviewers to know exactly what need to be reviewed and avoid having them to figure out the out-of-scope commits.
public Map<String, Object> baseEvent; | ||
private boolean started; | ||
private final Timer parentTimer; | ||
private TimerTask waitingTimerTask; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion (💡): I suggest making this @Nullable
and then checking that during stop()
:
public void stop() {
if (waitingTimerTask != null) {
waitingTimerTask.cancel();
}
started = false;
}
PS: Actually, I would even recommend for such refactor/test PRs to add any missing nullability annotation wherever possible going forward. 🤷
parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.java
Outdated
Show resolved
Hide resolved
started = false; | ||
} | ||
|
||
public boolean isSameVideo(String url, String urlRef, ParselyVideoMetadata metadata) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion (💡): Consider resolving/suppressing all the warnings related to this method:
- Suppress:
Unchecked cast: 'java.lang.Object' to 'java.util.Map<java.lang.String,java.lang.Object>'
- Resolve:
Method invocation 'equals' may produce 'NullPointerException'
- Resolve:
Unboxing of 'baseMetadata.get("duration")' may produce 'NullPointerException'
FYI: For no.2 there is an easy fix, just flit the equals(...)
, for example:
BEFORE: baseEvent.get("url").equals(url)
AFTER: url.equals(baseEvent.get("url")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave this to a different PR - I wanted this PR to be focused on extracting EngagementManager
and adding minimal set of changes to make it testable.
parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.java
Outdated
Show resolved
Hide resolved
parsely/src/main/java/com/parsely/parselyandroid/UpdateEngagementIntervalCalculator.java
Outdated
Show resolved
Hide resolved
parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt
Show resolved
Hide resolved
parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Test | ||
fun `when starting manager, then schedule task each interval period`() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question (❓): Please help me understand how start()
works on EngagementManager
for multiple events. 🙏
FYI: I am asking that because, to my understanding, a new instance of EngagementManager
is created with just one event, the baseEvent
, and that is the only event needed to be enqueued. I am struggling a bit to understand the logic here and when we can have multiple of the same events queuing. 🤔
PS: This is related to 51a4674 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we call start
on EngagementManager
, the java.utils.TimerTask
is scheduled inside java.utils.Timer
. The next execution of the TimerTask
will schedule a subsequent task.
So this test checks if subsequent tasks are executed and if they have correct parameters like e.g. updated total timestamp. Does this answer your questions @ParaskP7 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, yes, I understand how the TimerTask
will schedule a subsequent task. I just don't understand when multiple events, mostly in production, could be scheduled, can you help me with that? 🙏
engagementManager = new EngagementManager(this, timer, DEFAULT_ENGAGEMENT_INTERVAL_MILLIS, event, intervalCalculator);
engagementManager.start();
I see the above happening on production, which on (for example) startEngagement(...)
creates a new instance of EngagementManager
with a specific single event
. How is this manager is able to handle multiple events, when does this happens, this is what I don't understand @wzieba . 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this manager is able to handle multiple events, when does this happens, this is what I don't understand @wzieba . 🤔
When you say "multiple events" do you mean "multiple engagement sessions"? If yes, then the SDK cannot handle it.
Circling back to the original question:
I am asking that because, to my understanding, a new instance of EngagementManager is created with just one event, the baseEvent, and that is the only event needed to be enqueued.
Yes, the EngegementManager
is created with just one event, the baseEvent
. But no - it's not the only event needed to be enqueued. After the initial baseEvent
is enqueued, enqueuing of the next event is scheduled.
When interval time passes, the SDK, based on baseEvent
, enqueues a new event with modifications done in doEnqueue
. It modifies, e.g. ts
, inc
or tt
parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you say "multiple events" do you mean "multiple engagement sessions"? If yes, then the SDK cannot handle it.
Good to know! 💯
Yes, the EngegementManager is created with just one event, the baseEvent. But no - it's not the only event needed to be enqueued. After the initial baseEvent is enqueued, enqueuing of the next event is scheduled.
👍
When interval time passes, the SDK, based on baseEvent, enqueues a new event with modifications done in doEnqueue. It modifies, e.g. ts, inc or tt parameters.
That explains it, and apologies for only knowing the basics of the SDK, trying to figure out the rest during reviews... 😊
So, when startEngagement(...)
gets triggered, it is always triggered with a baseEvent
, and then, until stopEngagement(...)
is called, the same baseEvent
will be enqueued multiple time, based on the interval, with only some of its parameters being modified. If so, me repeating correctly what you already said, I then think I got the complete picture now, thanks so much for explaining. 🙇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same baseEvent will be enqueued multiple time, based on the interval, with only some of its parameters being modified
Yes, exactly this 🙌 !
withTotalTime: AbstractLongAssert<*>.() -> AbstractLongAssert<*>, | ||
withTimestamp: AbstractLongAssert<*>.() -> AbstractLongAssert<*>, | ||
): MapAssert<String, Any> { | ||
return containsEntry("action", "heartbeat") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor (🔍): Consider moving the .hasEntrySatisfying("data")
check above the .containsEntry("inc", 0L)
check to make it easier to follow the same logical order found within the EngagementManager.doEnqueue(...)
method, that is, without having to jump up and down to verify test correctness.
return containsEntry("action", "heartbeat")
// Incremental will be always 0 because the interval is lower than 1s
.hasEntrySatisfying("data") { data ->
data as Map<String, Any>
assertThat(data).hasEntrySatisfying("ts") { timestamp ->
timestamp as Long
assertThat(timestamp).withTimestamp()
}.containsAllEntriesOf(testData.minus("ts"))
}.containsEntry("inc", 0L)
.hasEntrySatisfying("tt") { totalTime ->
totalTime as Long
assertThat(totalTime).withTotalTime()
}
parsely/src/test/java/com/parsely/parselyandroid/EngagementManagerTest.kt
Outdated
Show resolved
Hide resolved
import java.util.Calendar; | ||
import java.util.TimeZone; | ||
|
||
class UpdateEngagementIntervalCalculator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion (💡): Ah, I forgot to mention this in my review, what about making this a Utils
class instead of instantiating it as an object. Maybe even a Kotlin utils class, wdyt? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In next iterations, I'll inject some Clock
to this class to fake the current time. Otherwise, test for this class will be time-sensitive. So I'll have to leave this as a class, not a static one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice to know, thanks for the heads-up on that. 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took it for another spin, everything LGTM now @wzieba , thanks again! 🚀
Thank you @ParaskP7 ! 🎉 |
@ParaskP7 one question on the initial review:
Which additional commits did you have in mind? |
👋 @wzieba and thanks for the reply! 🙇 Those two below got me a bit confused: 🤷 I remembered reviewing those two on this #81 PR, see bottom of this commit list: 🤷 Does that help? 🙏 |
I see, thanks! It's interesting though that those commits have different hashes. Normally, we shouldn't see those commits after merging |
Yeaaa, not sure what happened there, that's why, by default, I always recommend rebasing branches with latest |
Description
This PR extracts
EngagementManager
to a separate unit and adds unit tests for it.The proposed unit test is based on manual
Thread#sleep
calls and is time-sensitive. It's due to usage ofjava.utils.Timer
which runs a new thread internally - a thread we cannot control. Still, the unit test is written in a way to not be flaky (usages oforg.assertj.core.api.isCloseTo
assertions) and brings value. E.g., the current test suite covers fix added in #62 .How to test
Review and green light on CI should be just fine.