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

Poll view state unit tests [PSF-1130] #6366

Merged
merged 24 commits into from
Jun 27, 2022

Conversation

onurays
Copy link
Contributor

@onurays onurays commented Jun 22, 2022

Type of change

  • Feature
  • Bugfix
  • Technical
  • Other :

Content

Determining PollState and PollOptionViewState require business login. This PR aims to clean up MessageItemFactory, refactor this by creating a factory class and increase the test coverage.

Motivation and context

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 22, 2022 13:46
@Johennes Johennes requested review from ouchadam and removed request for Claire1817 June 22, 2022 14:09
@onurays onurays changed the title Poll view state unit tests Poll view state unit tests [PSF-1130] Jun 22, 2022
}

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun List<PollAnswer>.mapToOptions(
Copy link
Contributor

@ouchadam ouchadam Jun 22, 2022

Choose a reason for hiding this comment

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

rather than exposing the internals for testing I would take a step back and consider what we can and can't easily unit test, in this case it's the AndroidViews/Epoxy Items

which means instead of returning an epoxy PollItem, we could create a dedicated proxy ViewState data class for the PolItem

// MessageItemFactory

createPollItem() {
  val viewState = pollItemViewStateFactory.create(params)
  return PollItem_()
    .callback()
    .highlight()
    .state(viewState)
}

If Views have no/minimal business logic, then it means our ViewStates could be considered UI proxies (which means less need for slow/flaky instrumentation tests)

whichhhh would turn our class api into...

val viewState = PollItemViewStateFactory().create(
  pollContent: MessagePollContent,
  informationData: MessageInformationData,
)

as the factory returns immutable data we can make simple asserts against the result

@Test
fun `given a certain input, when creating state, then has specific result`() {
  val specific: MessagePollContent

  val result = factory.create(specificInput) 

  result shouldBeEqualTo expectedOutput
} 

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very nice idea, done!


@Test
fun `given a sent poll then all option view states is PollReady`() = runTest {
with(pollItemFactory) {
Copy link
Contributor

@ouchadam ouchadam Jun 22, 2022

Choose a reason for hiding this comment

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

For the test bodies if we follow Gherkin conventions (with blank lines between the sections) we can make the tests more consistent and quicker to understand

// given - the parameters or setup required to create the test result
val payload = Payload(argumentThatCreatesResult)

// when - the single action under test 
val result = myClassUnderTest.theFunctionUnderTest(payload)

// then - assert or verify side effects
result shouldBeEqualTo ExpectedResult()
verify(mySideEffect).wasCalled()

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 (hopefully).

fun setup() {
// We are not going to test any UI related code
pollItemFactory = PollItemFactory(
stringProvider = mockk(),
Copy link
Contributor

Choose a reason for hiding this comment

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

these should be test fakes rather than direct mocks and as they're unused by the tests it also suggests we aren't asserting any of the data they provide

Copy link
Contributor

Choose a reason for hiding this comment

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

for example we have a FakeStringProvider which allows us to assert against the correct resource ids

val fake = FakeStringProvider()

val result = fake.getString(R.string.ok)

result shouldBeEqualTo R.string.ok.toTestString()

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.


@After
fun tearDown() {
unmockkAll()
Copy link
Contributor

Choose a reason for hiding this comment

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

unmocking is only needed if we're statically mocking with @Before/AfterClass or at the object/singleton level

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, removed.


@Test
fun `given an ended poll then all option view states is Ended`() = runTest {
with(pollItemFactory) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would argue that the optionViewStates can exist on a parent PollItemViewState, if we want to avoid too many conditions under a single class, we can extract out another factory which the PollItemViewStateFactory could depend on

// Our PollItemViewState test doesn't need to know how the option view state is generated
// that class can have its own tests 
fakeOptionViewStateFactory.given(A_OPTION_VIEW_STATE)

val result = factory.create()

result shouldBeEqualTo Result(
  optionViewStates = A_OPTION_VIEW_STATE
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored.

import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData

object MessageItemFactoryHelper {
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 in general it is better to create classic injectable classes and avoid object. Objects are not scoped to only where they are needed and are not suitable for unit tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No need to a helper class after refactoring.

private val testDispatcher = UnconfinedTestDispatcher()

@get:Rule
val mvRxTestRule = MvRxTestRule(
Copy link
Contributor

Choose a reason for hiding this comment

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

Here this rule is not needed since the PollItemFactory does not rely on coroutines. And it is only useful in ViewModel and when switching the context Dispatcher.

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.

}

@Test
fun `given a sending poll state then PollState is Sending`() = runTest {
Copy link
Contributor

Choose a reason for hiding this comment

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

runTest can be removed from all tests. It is only useful when testing suspend functions.

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.

@onurays onurays requested review from ouchadam and mnaturel June 27, 2022 12:44
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.

LGTM!

@Before
fun setup() {
// We are not going to test any UI related code
pollItemViewStateFactory = PollItemViewStateFactory(
Copy link
Contributor

Choose a reason for hiding this comment

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

we can inline the creation as the junit test runner will create a new instance of the test class for each test

private val stringProvider = FakeStringProvider()
private val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)

@onurays onurays requested a review from ouchadam June 27, 2022 15:37
@sonarqubecloud
Copy link

SonarCloud Quality Gate failed.    Quality Gate failed

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

73.7% 73.7% Coverage
0.0% 0.0% Duplication


pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast),
Copy link
Contributor

Choose a reason for hiding this comment

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

because we're using the fakeStringProvider it means our expected values can be R.string.poll_no_votes_cast.toTestString() (rather than using the provider directly)

Copy link
Member

Choose a reason for hiding this comment

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

So the test is not passing?

Copy link
Member

Choose a reason for hiding this comment

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

(oh yes, please ignore me)

Copy link
Contributor

Choose a reason for hiding this comment

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

ideally our expected data creation would avoid calling any real functions or mocks as it makes them more susceptible to unrelated changes

eg if the StringProvider.getString function parameters change, this unrelated individual test will also need to be updated

In this case we know the expected value is the computed value of R.string.poll_no_votes_cast which we can create with the .toTestString() extension, fetching the value from the fake we've already setup is unnecessary

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.

tiny comment about the test string resource, otherwise great PR! the tests look nice and simple 💯 (which should make them easier to maintain 🤞 )

@@ -27,6 +27,10 @@ class FakeStringProvider {
every { instance.getString(any()) } answers {
"test-${args[0]}"
}

every { instance.getQuantityString(any(), any(), any()) } answers {
"test-${args[0]}-${args[1]}"
Copy link
Member

Choose a reason for hiding this comment

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

Quick question, why don't we use args[2] here?

Copy link
Member

@bmarty bmarty left a comment

Choose a reason for hiding this comment

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

Only one small question, will force push

@bmarty bmarty merged commit a398391 into develop Jun 27, 2022
@bmarty bmarty deleted the feature/ons/poll_view_state_unit_tests branch June 27, 2022 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants