Loading...
Home
  • Tech Blogs
  • Videos
  • Conferences
    • Droidcon News
    • Upcoming Conferences
    • Become a Partner
    • Past Events
    • Keep Me Informed
    • Diversity Scholarships
  • Community
    • droidcon Team
    • How to Hold a Droidcon
  • Android Careers
Sign In

Global CSS

 

Android Model-View-Intent with Unit Tests

Local unit testing using JUnit 5 and MockK

 

 
Adam Hurwitz
Creator of Coinverse - The 1st Crypto News Audiocast App @ bit.ly/play-coin
Published: October 16, 2020
Tweet
Share
 

Photo by Zach Reiner on Unsplash

 

You’ve spent too much time testing if you have had to reconfigure a feature’s code to make it testable or have debugged the ordering of actions/events and the resulting test output. Using the Model-View-Intent pattern (MVI) provides the ability to test built-in to the pattern following the best practice of Test Driven Development (TDD).

If you’re not familiar with MVI, the pattern structures how information is shared and how views are created and updated. MVI defines a clear contract between the view and business logic with an interface that is a one-way, immutable, unidirectional data flow (UDF) pattern. The structure of MVI grants the ability to implement with any reactive framework, such as Kotlin Coroutines, RxJava, and LiveData.

The last post, Android Model-View-Intent with Kotlin Flow, provides a comprehensive overview of the MVI pattern used in the Coinverse cryptocurrency news app. It compares MVI to a forest transferring information via its’ highly structured fungi network. If MVI 🌲 ️is like an intelligently designed forest’s ecosystem, then the unit tests 🐝 are the insects that feed off the fungi network and know whether it is poisonous, or in our case, contains errors.

Coinverse code

 

Coinverse Open App

Coinverse beta app is the first audiocast app for cryptocurrency news. 🚀github.com

 

 

Also available on the Play Store.

 

Overview

Advantages

Implement

Summary

 

Advantages

 

Image for post

Photo by Wolfgang Hasselmann on Unsplash

 

🙋🏻 Why Local Unit Tests?

Local unit tests with JUnit 5 can be run quickly prior to writing a feature and regularly to ensure code quality and mitigate regressions of features’ business logic. These tests do not require dependencies on the Android system. This covers the majority of code for most apps making it a great first step for testing. These local unit tests can be expanded into integration tests for more thorough testing of the data layer, such as network requests and local databases, and for instrumented tests including the UI components.

 

🥼 Built-in testing

Once modularized properly, the business logic model, in this case Android’s ViewModel component, and the view interface contract used in production are also used for local unit tests without modification to test the given intents that the model uses to generate the expected view states.

MVI defines the intents driven by the Android system and user actions that the model processes to create the view states. Because the ViewModel business logic component observes the intent actions and creates the final persisted data of each view state, a local unit test makes use of the existing ViewModel and view interface contract.

 

🏎️ More efficient code

Creating intents and view states with MVI reduced the test code by more than 50% compared to the previous implementation using the UDF and emitting many streams of data simultaneously.

100% of FeedViewModel.kt is covered in local unit tests in Coinverse with MVI comprising 306 lines of code in the FeedViewTest.kt class, compared to 753 lines of code across four test classes covering mostly the same code prior to the MVI refactor. In the pre-MVI pattern, UDF was implemented to separate the view, business logic, and data layer. However, many sources of data were emitted from the ViewModel to the View making the tests harder to read, write, and created issues with the order of data being emitted. By knowing exactly what view state should be emitted based on each intent with MVI, it makes the tests more reliable, quicker to build, and more concise.

 

Image for post

FeedViewModel.kt Test Coverage

 

Implement

 

Image for post

Photo by Fredrik Ivansson on Unsplash

 

📚 Libraries — Step 1 of 5

  • JUnit 5 with parameterized tests — Reuse the tests with a stream of many test cases.
  • MockK — Creates components involving the Android system or classes handling data requests like a repository or database, returning data that would be expected to be returned in the loading, success, and error states.
  • Kotlin coroutines — Initiate intent actions and observe view state changes with StateFlow.
  • AssertJ — Evaluates the actual vs. expected states.

build.gradle (Module)

 

 

build.gradle (App)

 

 

Releases/Versions

  • JUnit 5
  • MockK
  • Coroutines
  • AssertJ

 

💼 ViewModel — Step 2 of 5

A. Define how components are injected

In order to use the same business logic in production and testing, all components that rely on the Android system or data requests are injected into the ViewModel so that test instances can be used in the local unit tests.

See: Android Model-View-Intent with Kotlin Flow > Model — ViewModel > ‘CoroutineScope and CoroutineDispatcher’ and ‘Dagger 2 Dependency Injection for ViewModels with AssistedInject’

FeedViewModel.kt

  1. CoroutineScope — With the custom extension function,getViewModelScope, for production no scope is injected and the Android’s viewModelScope is used by default. In testing the TestCoroutineScope is explicitly injected.
  2. FeedRepository — Data requests made to the network and local database will be mocked requiring a test instance of the repository to be injected.

B. Create the test components to inject

FeedViewTestExtension.kt

  1. TestCoroutineDispatcher and TestCoroutineScope are created in a JUnit 5 test extension. The dispatcher is necessary to ensure coroutines run on a test thread and the scope defines the lifecycle that the coroutines runs within.
  2. The ArchTaskExecutor is required for LiveData to return the PagedList.

See: Android Unidirectional Data Flow — Local Unit Testing > 1a. Configuration — Extensions

 

 

FeedViewTest.kt

  1. The mock repository is initialized and passed into the ViewModel along with the TestCoroutineScope created in FeedViewTestExtension.kt above.
  2. The repository method getMainFeedNetwork's response is mocked to return test data. In the full sample code this emits a Flow of PagedListdata. With the Paging 3 library this can be simplified removing the dependency to mock LiveData asPagedList and return a PagingDataresponse with Kotlin coroutines. See StackOverflow for Paging 2: Convert List Into PagedList With Mock DataSource.Factory
  3. The FeedViewTestCaseStream is a Stream.of(...) FeedViewTestCase defined in a data class and represents different scenarios such as loading, success, and error states. The stream is passed into the test making the test reusable for all of the states defined.

 

 

🌉 Interface — Step 3 of 5

Bind the view interface contract to the ViewModel

  1. The majority of logic tested is defined by implementing the view interface and binding it to the ViewModel. The interface represents the intent/actions sent from the view to the ViewModel, and the view states rendered by the ViewModel back to the view. See: Android Model-View-Intent with Kotlin Flow > Intent — View Interface
  2. The view’s StateFlow intents are defined in FeedViewIntent for organizational purposes. See: Android Model-View-Intent with Kotlin Flow > View — Activity/Fragment
  3. The test view interface observes emissions from the intent Flows and the test view interface renders a view state sealed class as it would in production.

FeedViewTest.kt

 

 

🖼️ Intents and View States — Step 4 of 5

  1. Call the intents/actions that initiate the business logic to generate a view state, in this case, loadFromNetwork intent is emitting a true value which initiates the FeedViewModel to create the Feed state. Then, observe that the actual view state of type Feed matches the expected state.
  2. isEqualToComparingFieldByField is used to compare the properties of the actual and observed Feed view state emitted. Feed is a class rather than a data class so that each instance of Feed has a unique hash code and can be properly emitted by the model.

FeedViewTest.kt

 

 

🔬 Verify and Confirm Method Calls — Step 5 of 5

  1. The MockK verification coVerify confirms that the method getMainFeedNetwork is called within the test.
  2. confirmVerified checks that all method calls in the test are verified/expected to be called.

See: Android Unidirectional Data Flow — Local Unit Testing > 5. Verify Tests

FeedViewTest.kt

 

 

Summary

Image for post

Photo by Michael D on Unsplash

 

⚠️ UDF Without MVI

From Step 3 of 5 — Interface section above when a view state is rendered, we can see how much more complicated this is compared to UDF emitting many data types simultaneously. In this case, loading the feed data is done by observing two emissions of data, the view state, and a one-time view effect to update ads. This also creates ugly code nesting, and is harder to read vs. creating one view state with all of the view’s final persisted data contained.

See: Android Unidirectional Data Flow — Local Unit Testing > 4. Make Assertions

FeedLoadContentTests.kt

 

 

🌊 Alternatives to Kotlin Flow

The Kotlin Flow implementation is powerful and concise in handling threading, lifecycle scope, and allowing for mutable and immutable flows.

MVI also works well for testing with other reactive frameworks as well.

  • RxJava’s Subject or Relay are also solid options for MVI in the data, view state, and view layers.
  • LiveData can be used to achieve the tests above for the view state layer using MutableLiveData and LiveData. It is not recommended for the data request repository layer as LiveData is tied to a view’s lifecycle scope and some data requests may need to be decoupled from the UI.
  • Callbacks could work in theory, but run the risk of becoming overly complicated with both the view and the ViewModel implementing listeners to send actions from the view to the ViewModel and rendering view states from the ViewModel to the view.

Whichever tool is chosen, the MVI pattern separates the view, business logic, and data requests as well as modularizes the various features into view states make initiating the user and system actions and testing the generated view states from the model accessible.

 

🌱 Next Steps

Now that local unit tests are created for the business logic using the MVI pattern, there is a foundation to expand upon for integration and instrumented tests.

  • Integration — Components the app is dependent upon such as network requests and local databases. Retrofit requests can be tested using MockWebServer and a Room database can be tested by creating a database with a test app context using Robolectric.
  • Instrumented — End-to-end behavior of a real device or emulator. UI tests can be incorporated with Espresso.

Explore Coinverse on the Google Play Store or the open-source code. Pleaseclap if you like the above. The longer the clap button is pressed the more claps. 👏🏻

📝 Notes

  • MockK
  • JUnit Tests
  • Model-View-Intent

 

I’m Adam Hurwitz, writer of code and more. Follow me on Twitter.

 

Image for post

 

 

Tags: Android App Development, AndroidDev, Kotlin, Android, Software Development

 

View original article at: 


 

Originally published: October 05, 2020

Android News
Evolution of Android Update SystemEvolution of Android Update System
Evolution of Android Update SystemEvolution of Android Update System

By Ivan Kuten

So, how can you update Android on mobile devices? While developing software for Smart TVs and Android-based set-top boxes, we’ve narrowed it down to four ways, discarding some very exotic options:

By ProAndroidDev -
Android News
Happy Railway
Happy Railway

By Hadi Lashkari Ghouchani

This post is on the tail of Railway Oriented Programming in Kotlin by Antony Harfield. So you need to read it first and continue here. As it’s obvious I really liked it and tried it out. It needs every process have a result like

By ProAndroidDev -
Android News
Unit Tests and Concurrency
Unit Tests and Concurrency

By Stojan Anastasov

Once Retrofit added RxJava support, RxJava became my go-to concurrency framework for writing Android apps. One of the great things about RxJava is the excellent testing support. It includes TestObserver, TestScheduler, RxJavaPlugins so you can switch your schedulers in tests.

By ProAndroidDev -
Android News
When Compat libraries will not save you
When Compat libraries will not save you

By Danny Preussler

And why you should avoid using the “NewApi” suppression! The idea of “Compat” libraries was probably one of the key aspects of Android dominating the mobile space. Other than with iOS, Android users often could not update their operating system after a new version launch, simply as their phones won’t allow them to, the Android problem of fragmentation.

 

By ProAndroidDev -
droidcon News

Tech Showcases,

Developer Resources &

Partners

/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/home-details/EmployerBrandingHeader
EmployerBrandingHeader
https://jobs.droidcon.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/jobs-droidcon/jobs.droidcon.com
jobs.droidcon.com

Latest Android Jobs

http://www.kotlinweekly.net/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/kotlin-weekly/Kotlin Weekly
Kotlin Weekly

Your weekly dose of Kotlin

https://proandroiddev.com/
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/pad/ProAndroidDev
ProAndroidDev

Android Tech Blogs, Case Studies and Step-by-Step Coding

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Zalando/Zalando
Zalando

Meet one of Berlin's top employers

/detail?content-id=/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
/portal/rest/jcr/repository/collaboration/Groups/spaces/droidcon_hq/Documents/public/employerbranding/Academy for App Success/Academy for App Success
Academy for App Success

Google Play resources tailored for the global droidcon community

Follow us

Team droidcon

Get in touch with us

Write us an Email

 

 

Quicklinks

> Code of Conduct

> Terms and Conditions

> How to hold a conference

> FAQs

> Imprint

Droidcon is a registered trademark of Mobile Seasons GmbH Copyright © 2020. All rights reserved.

powered by Breakpoint One