-
Notifications
You must be signed in to change notification settings - Fork 767
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
Conversation
} | ||
|
||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) | ||
internal fun List<PollAnswer>.mapToOptions( |
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.
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
}
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.
Very nice idea, done!
|
||
@Test | ||
fun `given a sent poll then all option view states is PollReady`() = runTest { | ||
with(pollItemFactory) { |
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.
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()
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.
Done (hopefully).
fun setup() { | ||
// We are not going to test any UI related code | ||
pollItemFactory = PollItemFactory( | ||
stringProvider = mockk(), |
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.
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
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.
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()
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.
Done.
|
||
@After | ||
fun tearDown() { | ||
unmockkAll() |
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.
unmocking is only needed if we're statically mocking with @Before/AfterClass
or at the object/singleton level
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.
Right, removed.
|
||
@Test | ||
fun `given an ended poll then all option view states is Ended`() = runTest { | ||
with(pollItemFactory) { |
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 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
)
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.
Refactored.
import im.vector.app.features.home.room.detail.timeline.TimelineEventController | ||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData | ||
|
||
object MessageItemFactoryHelper { |
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 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.
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.
No need to a helper class after refactoring.
private val testDispatcher = UnconfinedTestDispatcher() | ||
|
||
@get:Rule | ||
val mvRxTestRule = MvRxTestRule( |
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.
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
.
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.
Done.
} | ||
|
||
@Test | ||
fun `given a sending poll state then PollState is Sending`() = runTest { |
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.
runTest
can be removed from all tests. It is only useful when testing suspend
functions.
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.
Done.
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.
LGTM!
@Before | ||
fun setup() { | ||
// We are not going to test any UI related code | ||
pollItemViewStateFactory = PollItemViewStateFactory( |
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.
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)
SonarCloud Quality Gate failed. |
|
||
pollViewState shouldBeEqualTo PollViewState( | ||
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "", | ||
totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast), |
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.
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)
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.
So the test is not passing?
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.
(oh yes, please ignore me)
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.
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
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.
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]}" |
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.
Quick question, why don't we use args[2]
here?
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.
Only one small question, will force push
Type of change
Content
Determining
PollState
andPollOptionViewState
require business login. This PR aims to clean upMessageItemFactory
, refactor this by creating a factory class and increase the test coverage.Motivation and context
Screenshots / GIFs
Tests
Tested devices
Checklist