-
Notifications
You must be signed in to change notification settings - Fork 2
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
test: add instrumented tests for active session #144
base: main
Are you sure you want to change the base?
Conversation
This NotificationManager is dependency injected by dagger hilt which at the same time creates the notification channels. Thereby the whole mechanism works out of the box when used in tests.
… for active session use cases and pass timestamps through inputs instead This means that we are now actually using the timestamp emitted by the "clock" flow and basing calculations on it.
Some of the checks were moved around the use cases in order to ensure the user can not finish an empty session.
… querying it from the repository
Removed snackbar host from active session scaffold and used the main ui event "ShowSnackbar" instead.
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.
leaving comments so far
composeRule.onNodeWithText("TestItem3").performClick() | ||
|
||
// Item is selected | ||
composeRule.onNodeWithText("TestItem3").assertIsDisplayed() |
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.
This should be the different node, of the current section, shouldn't it? So maybe select the node more explicitly, e.g. by contentDescription.
(1) this will make it clear that it's a different node and
(2) prevents the test from passing if the modalBottomSheet does somehow not close and you are asserting the same Item as before.
assertThat(sessions).containsExactly( | ||
SessionWithSectionsWithLibraryItems( | ||
session = Session( | ||
id = UUIDConverter.fromInt(6), |
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.
can you add a comment why the UUID has to be 6 exactly here (and the other ones as well)?
Story
This PR adds tests for the active session. As you can imagine, this is kinda complicated because the active session has many dependencies like multiple services, use cases and repositories.
Modifications to the main code
NotificationManager
One thing I stumbled over at first was the fact that even the simplest test would fail with the error
Bad notification for startForeground()
. This lead me down the rabbit hole of trying to "fix" the service or somehow inject a test service. On the way I wrote a simple test for the session service, which doesn't really do much, because the service only shows a notification and has no public methods which could be tested.Back to the error in the active session test. The
Bad notification for startForeground()
didn't have anything to do with the service per se, but rather with the non-existence of the notification channels which are normally created by theMusikus : Application()
class. However, the application appears not to get instantiated for instrumented tests so I had to find a solution.The answer, as so many times with testing, is "Dependency injection". Creating a dedicated
NotificationModule
which provides theNotificationManager
while also creating the notification channels.Works like a charm 👌
TimeProvider
A potentially controversial change in this PR is the discretization of the clock in
TimeProvider
. The interface now looks like this:The interesting thing is the difference in implementation between
TimeProviderImpl
andFakeTimeProvider
:This implementation has two main advantages:
advanceTimeBy
orsetCurrentDateTime
is called.get**Duration
use cases are now more easily testable after being converted tocompute**Duration
use casesActiveSessionViewModel
Problems
A problem which I ran into, is the fact that our active session UI is updated by a clock. So when trying to advance the time usingDone 👌fakeTimeProvider.advanceTimeBy(90.seconds)
the UI didn't actually update adelay(100)
had passed. This, of course, is not a nice thing to add to a test. I think we should look into modifying theTimeProvider
to provide a clock-like flow of some kind, which we can then use in all our screens which need to update automatically. TheFakeTimeProvider
could then send the updates immediately.The "Pause" button, or more specific theDone 👌PauseActiveSessionUseCase
does not allow the user to pause the session if the session timer is still at zero. In reality this is never a problem, but for testing it was quite tricky to figure out why pressing "Pause" wasn't working. The check could probably go into another use case...Learnings
Use Cases
Important
To reduce undefined behavior and improve testability, use cases should not fetch data from the repository and do too much processing at the same time, especially if the data gets combined with state from the view model.
Here is an example which led to this conclusion:
This code can lead to a race condition... Can you spot it?
Since both
getPracticeDuration
andgetOngoingPauseDuration
first request the session state from the active session repository, a race condition occurs whenever the user presses "Pause" while the code execution is somewhere in front ofgetOngoingPauseDuration
and the clock has updated (10 Hz). In this case,currentPauseStartTimestamp
in the session state will get updated with the new clock value but the value ofnow
is still original value which leads to a negative pause duration.Adds to: #18