Adam Hurwitz
01/02/2020, 6:08 PMDaniel
01/02/2020, 6:35 PM@RunWith(MockitoJUnitRunner::class)
class GamePageViewModelTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
private lateinit var gameCommander: GameCommander
private lateinit var viewModel: GamePageViewModel
private lateinit var gameCommands: Observable<GameCommand>
@Before
fun setUp() {
gameCommands = Observable()
whenever(gameCommander.commands) doReturn gameCommands
viewModel = createViewModel()
}
@Test
fun `Should hide the source mark when showing a valid info`() {
// GIVEN
val observer: () -> Unit = mock()
viewModel.hideSourceMark.observeForever(
LiveDataCommandObserver(observer))
// WHEN
gameCommands.emit(ShowInfo(validInfo()))
// THEN
verify(observer).invoke()
}
Here the view model subscribes to a repository (GameCommander) and recieves events through the subscription (gameCommands are the observable).
You can now push events to the view model and assert its behaviour.
The view model doesn't even know that there are somewhere coroutines used.codeslubber
01/02/2020, 6:37 PMAdam Hurwitz
01/02/2020, 8:15 PMA huge thanks for your post!Absolutely, it would not have been possible without the collaboration from the Kotlin community and devs like yourself.
Unit testing should be easy and require very few setupMy statement was moreso a comment on the initial one-time setup researching how to configure. Moving forward testing will be easy as the
LiveData
and Coroutine
configuration can be copy & pasted into the test extension.
Because you can mock static stuff now does not mean you should use static stuff.Noted. I have not written static methods within Coinverse. The static methods are from dependent libraries implemented.
The view model doesn't even know that there are somewhere coroutines used.Interesting, does this mean the
Coroutines
are contained within the mocked Repository, Therefore, the ViewModel only handles observing the Observable?
Daniel
01/02/2020, 8:27 PMViewModel(private val referenceToTheObject: SomeObject)
The goal should always be to have the dependency injected through the constructor. This makes mocking / the setup for tests simple.
For the second part: Yes. But its only one possibility. You see lots of stuff in the android world. For example that the repository exposes LiveData that can be subscribed to by the view model (search MediatorLiveData for an example how nice this can be).
You can also make it purely procedural and call suspending methods exposed by the repository:
// in viewModel
init {
launch(Main) {
val result = repository.doStuff()
// Handle result
}
}
Then you only have to inject Main
in the constructor ViewModel(mainContext: CoroutineContext = Main
and swap it out with Unconfined
in the test or use the setMain()
stuff from the corutine test framework.
In either case the view model should know as little as possible from threading. Thats the purpose of the repository or service classes.
The ViewModel is busy enough with mapping the data to the ui (most of the time!)Adam Hurwitz
01/02/2020, 8:31 PMbut then say management never supplied the resourcesMy intention was to take personal ownership of not learning testing, as well as call out the difficulties of allocating time to it on a fast moving environment. I've edited the intro to better reflect this sentiment. When I started building the Unidirectional Data Flow (UDF) with LiveData pattern, I did not have testing experience. Working on a small/fast moving startup team, Close5, owned by eBay, and building Coinverse from scratch, I had not been required, nor did I allocate the time to develop tests.
the article spends like 80% of its time on scaffolding. Good job thoughThank you. I find it frustrating when I consume introductory samples/tutorials that don't include the full setup, so it was important for me to cover/link to these items.
codeslubber
01/02/2020, 8:44 PMAdam Hurwitz
01/02/2020, 8:49 PMstatic
dependency is located in the Repository. The Repository is then injected as an Application
level Singleton
into the ViewModel. Then, the static
library is being called as a result of another method in the Repo.
My action item: I will look at how I can better mock the Repo to avoid the static method from being called to begin with.
ViewModel/Repo
I used MediatorLiveData
in my first version of the UDF pattern. It works well. After experimenting with Flow
in the Repo layer, I think I like that for more advanced transformations. The linear nature of the transformations is good for readability.
My strategy moving forward for more advanced transformations is to use Flow
in the Repo and return the formatted data to the ViewModel to be saved as LiveData
for the view state(s) that can be observed by the view.show what we are going to get, then link out to a complete setup explanation
codeslubber
01/02/2020, 8:56 PMDaniel
01/02/2020, 8:57 PMCoinverseDatabase
as a constructor dependency which then can be easily mocked (the tests tell you that the repo should not be singleton) and so forth. You push the static stuff as much down as possible if it really cant be avoided.
Object lifecycle is in this case better handled by a dedicated framework (dagger2) or by passing references around. (Build your own "dependency injection" by keeping a reference to the database where it stays the whole application lifecycle)
At the end you have lots of easily testable small peaces that can be swapped out easily (which also helps when you have to swap one library with another)Adam Hurwitz
01/02/2020, 9:37 PMstatic
methods and Singleton
objects is inherently bad?
TLDR: "makes your test code unnecessarily complex"
i.e.: In the test I need to build the Singleton
before the tests run, and unmock afterwards to ensure the remembered state does not affect future tests. This seems to work without issue, but adds the additional code in the lifecycle methods.
https://stackoverflow.com/questions/30544357/why-is-using-static-methods-not-good-for-unit-testingDaniel
01/02/2020, 11:39 PMAdam Hurwitz
01/03/2020, 3:28 AMDaniel
01/03/2020, 10:00 AM