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

CreatePollViewModel unit tests [PSF-1122] #6320

Merged
merged 16 commits into from
Jun 17, 2022
Merged

Conversation

onurays
Copy link
Contributor

@onurays onurays commented Jun 15, 2022

Type of change

  • Feature
  • Bugfix
  • Technical
  • Other :

Content

Motivation and context

Increase CreatePollViewModel test coverage to 100% (83/83)

Screenshots / GIFs

Tests

Tested devices

  • Physical
  • Emulator
  • OS version(s):

Checklist

@onurays onurays requested review from a team, Claire1817 and mnaturel and removed request for a team June 15, 2022 18:26
}
}

@Before
Copy link
Contributor

Choose a reason for hiding this comment

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

We should unmock mocked objects after each test in general. We can add:

@After
fun tearDown() {
    unmockkAll()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

private val fakeSendService = mockk<SendService>()
private val fakeCancellable = mockk<Cancelable>()

private fun createPollViewModel(pollMode: PollMode): CreatePollViewModel {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we also should add tests for PollMode.EDIT?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@get:Rule
val mvrxTestRule = MvRxTestRule()

private val fakeSession = mockk<Session>()
Copy link
Contributor

Choose a reason for hiding this comment

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

We should use/create FakeObject classes to be reusable for other unit tests. Here you can use FakeSession and FakeRoom. You can add givenXXX methods in them to mock some calls (see existing examples). For the SendService, a FakeSendService can be created and instantiated inside the FakeRoom class. We may only use mockk<> inside tests class when it is very specific to the tested class or simple object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, done.

createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion))

// We need to wait for createPollViewModel.onChange is triggered
delay(10)
Copy link
Contributor

@mnaturel mnaturel Jun 16, 2022

Choose a reason for hiding this comment

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

For state transition, you can use assertStatesChanges instead of assertState to avoid having to add a delay in the tests which may lead to result inconsistencies.

Copy link
Contributor

Choose a reason for hiding this comment

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

different cpu/computers will be able to execute faster/slower, we should avoid relying on delays whenever possible 🤞

// We need to wait for createPollViewModel.onChange is triggered in the test enviroment we should control all the dispatching and block asynchronous calls (which is the purpose of runTest and MvRxTestRule)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried assertStatesChanges and verify with a timeout but no luck.

Copy link
Contributor

Choose a reason for hiding this comment

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

the issue here looks like your handle is not blocking correctly 🤔

the ViewModel.test()extension is how we're capturing emissions from the ViewModel, if we interact with the ViewModel before test() then we'll miss state events

val test = createPollViewModel.test() // register state and event listeners

viewModel.handle()  // blocking - causes a state update

test.assert() // asserts against the captured states

however here I can see the handle emissions happening after .test()

println("start")
val createPollViewModel = createPollViewModel(PollMode.CREATE)
createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion))
println("handle")

// We need to wait for createPollViewModel.onChange is triggered
delay(10)
createPollViewModel
        .also { println("test") }
        .test() // prints captures emissions
        .assertState(pollViewStateWithOnlyQuestion)
        .finish()
start
handle
test
CreatePollViewState(roomId=fakeRoomId, editedEventId=null, mode=CREATE, question=What is your favourite coffee?, options=[, ], canCreatePoll=false, canAddMoreOptions=true, pollType=DISCLOSED_UNSTABLE)

notice how the emission happens after test(), which shows the logic and updates inside handle are non blocking (a test's worst enemy!)

Copy link
Contributor

@ouchadam ouchadam Jun 16, 2022

Choose a reason for hiding this comment

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

separating the test() resolves the issue for me

println("start")
val createPollViewModel = createPollViewModel(PollMode.CREATE)
val test = createPollViewModel.test()

createPollViewModel.handle(CreatePollAction.OnQuestionChanged(fakeQuestion))
println("handled")

test
        .assertStates(initialCreatePollViewState, pollViewStateWithOnlyQuestion)
        .finish()
start
CreatePollViewState(roomId=fakeRoomId, editedEventId=null, mode=CREATE, question=, options=[, ], canCreatePoll=false, canAddMoreOptions=true, pollType=DISCLOSED_UNSTABLE)
CreatePollViewState(roomId=fakeRoomId, editedEventId=null, mode=CREATE, question=What is your favourite coffee?, options=[, ], canCreatePoll=false, canAddMoreOptions=true, pollType=DISCLOSED_UNSTABLE)
handled

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I see; thanks for clarifying. Using the same Dispatcher seems to prevent us from having to pay attention to the call ordering. What do you think of having a TestDispatcher in the test extension which will be the same as the one used in the mvrx rule?

Copy link
Contributor

Choose a reason for hiding this comment

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

the ordering is important because we're subscribing to the ViewModel flows in order to capture their emissions

if all the helper logic was inlined it would look like...

// .test()
val captures = mutableListOf()
viewModel.stateFlow
  .onEach { captures.add(it}
  .launchIn(nonBlockingContext)

// handle
viewModel.setState {
    copy("foo")
}

// assert
captures shouldBeEqualTo expectedEmissions

Copy link
Contributor

Choose a reason for hiding this comment

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

turns out there's a bug/coroutine-test incompatibility airbnb/mavericks#599

@get:Rule
val mvrxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())

@Test
fun `can create poll`() = runTest {
    val createPollViewModel = createPollViewModel(PollMode.CREATE)
    val test = createPollViewModel.test()

    createPollViewModel.handle(CreatePollAction.OnQuestionChanged(A_FAKE_QUESTION))
    repeat(CreatePollViewModel.MIN_OPTIONS_COUNT) {
        createPollViewModel.handle(CreatePollAction.OnOptionChanged(it, A_FAKE_OPTIONS[it]))
    }

    test.assertStatesChanges(
            initialCreatePollViewState,
            { copy(question = A_FAKE_QUESTION) },
            { copy(options = listOf(A_FAKE_OPTIONS[0], "")) },
            { copy(options = listOf(A_FAKE_OPTIONS[0], A_FAKE_OPTIONS[1])) },
            { copy(canCreatePoll = true) },
    ).finish()
}

Copy link
Contributor

@mnaturel mnaturel Jun 16, 2022

Choose a reason for hiding this comment

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

Ahah just found the same solution on my side 😄 I knew there was something strange with TestDispatcher.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you both for the great help! Please see my last commit (and approve :).

import im.vector.app.features.poll.PollMode
import kotlin.random.Random

object CreatePollViewStates {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should move this class to the test package as it is only used as testing purpose. We could also rename it into FakeCreatePollViewStates or we could also declare all the fake data at the top of the related test file: CreatePollViewModelTest before the class declaration. I have a preference for the latter as we can see tha fake data and the tests at the same time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly, done.


object CreatePollViewStates {

const val fakeRoomId = "fakeRoomId"
Copy link
Contributor

Choose a reason for hiding this comment

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

For all const val, it is preferable to use upper case format like:

const val A_FAKE_ROOM_ID = "fakeRoomId"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, done.

Copy link
Contributor

@mnaturel mnaturel left a comment

Choose a reason for hiding this comment

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

Thanks for the tests! I have added some comments to improve reusability and readability.

Copy link
Contributor

@ouchadam ouchadam left a comment

Choose a reason for hiding this comment

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

great job! LGTM 💯

import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import kotlin.random.Random

object FakeCreatePollViewStates {
Copy link
Contributor

@ouchadam ouchadam Jun 16, 2022

Choose a reason for hiding this comment

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

minor naming comment for fakes vs fixtures

Fakes are objects where behaviour has been changed/overridden or they're extended with test helpers for capturing information, whereas a fixture is a reusable set of defaults,

I would argue the content of the file fits more into the description of a fixture

for example a FakeRoom could return a FakeTimeline which returns a list of PollEvent fixtures, usually it's a case of class = fake, data class = fixture

@sonarcloud
Copy link

sonarcloud bot commented Jun 16, 2022

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 0 Code Smells

No Coverage information No Coverage information
0.0% 0.0% Duplication

@onurays onurays merged commit 242cc28 into develop Jun 17, 2022
@onurays onurays deleted the feature/ons/poll_unit_tests branch June 17, 2022 09:14
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.

3 participants