https://kotlinlang.org logo
#feed
Title
# feed
a

Adam Hurwitz

01/02/2020, 6:08 PM
Any feedback on the way I've implemented parameterized unit tests in JUnit 5 would be much appreciated! 🙏 Embarrassingly when I started building the Unidirectional Data Flow (UDF) with LiveData pattern five months ago, I did not have testing experience. This made for a great opportunity to develop local unit testing fundamentals in Coinverse same code below. https://proandroiddev.com/android-unidirectional-data-flow-local-unit-testing-487a6e6f5c9
👍 3
d

Daniel

01/02/2020, 6:35 PM
Hi and first of all a huge thanks for your post! My opinion is this: You summed it up at the end very precisely: 80% of work consists of configuration (Coroutines, LiveData, component injection, test cases) and mocking. Your tests are telling you something here: The implementation is probably too complicated and too coupled. Unit testing should be easy and require very few setup. This is normally achieved when the logic is simple and nicely decupled. Also be careful when using Mockk. Because you can mock static stuff now does not mean you should use static stuff. On top of this parametrized tests have disadvantages most of the time. A test name is a requirement and can be written very precisely. You loose this if you bunch up all requirements into a single parametrized test. To give an example for a (in my opinion simple) unit test for a view model which still has a lot of resposibilities:
Copy code
@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.
👍 1
c

codeslubber

01/02/2020, 6:37 PM
In the intro you apologize for not having done much testing, but then say management never supplied the resources, I would maybe nix that. Old argument, might inflame or reopen a long ago debated issue.. on the whole the article spends like 80% of its time on scaffolding. Good job though, it is nice when something takes you all the way from start to finish.
👍 1
(made my comment before reading @Daniel’s 🙂 we both pareto’ed the same thing…)
👏 1
🤣 1
a

Adam Hurwitz

01/02/2020, 8:15 PM
Thank you for the in-depth feedback @Daniel!
A 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 setup
My 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?
d

Daniel

01/02/2020, 8:27 PM
No problem and don't worry. Everyone runs into this kind of problems at first when unit testing. Shortly to the static stuff: If you can't avoid it best put your own wrapper class around it. Or pass the object class as an dependency
ViewModel(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:
Copy code
// 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!)
a

Adam Hurwitz

01/02/2020, 8:31 PM
Thank you for pointing this out @codeslubber!
but then say management never supplied the resources
My 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 though
Thank 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.
c

codeslubber

01/02/2020, 8:44 PM
I am in no way saying its an invalid argument, of course it is, just saying you don’t want to open that gash, you have other fish to fry.. 🙂
👍 1
on your other point, I COMPLETELY agree, but let me make a suggestion: do the post and whlie you are in that honeymoon window, show what we are going to get, then link out to a complete setup explanation (which frankly, belongs in a readme)..
a

Adam Hurwitz

01/02/2020, 8:49 PM
@Daniel Static Methods In Coinverse, the
static
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.
@codeslubber, Great to know for future concepts. Ideally, you are suggesting a short Medium post outlining the value proposition, then linking to the in-depth ReadMe focused on technical implementation?
show what we are going to get, then link out to a complete setup explanation
c

codeslubber

01/02/2020, 8:56 PM
yes, I love it when people say ‘I did this thing, here it is on GitHub’ then continue at the conceptual level and when I am done I click through and see a complete readme that gives me confidence that I could install it pretty easily
d

Daniel

01/02/2020, 8:57 PM
Thats the nice thing about testing, it points you to a good architecture! 🙂 Its all about decoupling here: The ViewModel gets the repo through the constructor (see ViewModelFactory) which then can be easily mocked. The repository gets the
CoinverseDatabase
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)
The last part is also why I am personally against using Mockk in meaningful Projects. If you suddenly can mock static stuff whats keeping you from doing it? Standard JUnit + MockitoKotlin force you to think about proper class separation
a

Adam Hurwitz

01/02/2020, 9:37 PM
@Daniel, does this StackOverflow properly explain why mocking
static
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-testing
d

Daniel

01/02/2020, 11:39 PM
Its more this things: • Adding a library which provides static stuff is terrible when your code base is well decoupled. (Because you have to wrap it to use it as a dependency). • You can never assert that the right static methods are called because you don't have a mock (with standard tools) • And most of all: Such libraries come for sure with static state. And this is where the fun begins. Someone will inadvertantly forget to reset that state (if its even possible) in a test case and then you may have randomly failing tests because of (random) test ordering. And this is for tests only, static state is terribly to gebug when something goes wrong
👍🏻 2
a

Adam Hurwitz

01/03/2020, 3:28 AM
@Daniel, Well summarized! To conclude, would implementing Dagger2 to create the Repository and Database instances be one proposed refactor to avoid the issues with using Singletons above?
d

Daniel

01/03/2020, 10:00 AM
Depends, I would say in the long run there are benefits from having a dedicated dependency injection (if its sparsely used, I am not a fan of too much dagger magic). If the application is very small and should stay small it might be better to implement your own solution
👍 1
3 Views