• No DEX • No APK Installation Limited - No access to Android SDK / components • Views, Context, SharedPreferences, res... • *Can be simulated (Robolectrics & others) or mocked.
fun create(@Body fields: CreateRecordBody): Response<AirtableRecord> companion object { fun build(baseUrl: String, apiKey: String): AirtableAPI { ... } } } data class RecordsApiResponse(val records: List<AirtableRecord>) data class AirtableRecord(val id: String, val fields: Map<String, String>) data class CreateRecordBody(val fields: Map<String, Any>)
fun create(@Body fields: CreateRecordBody): Response<AirtableRecord> companion object { fun build(baseUrl: String, apiKey: String): AirtableAPI { ... } } } data class RecordsApiResponse(val records: List<AirtableRecord>) data class AirtableRecord(val id: String, val fields: Map<String, String>) data class CreateRecordBody(val fields: Map<String, Any>)
val request = mockWebServer.takeRequest() assertThat(request.path).isEqualTo("/") assertThat(request.headers["Authorization"]) .isEqualTo("Bearer $API_KEY") assertThat(clientResponse.isSuccessful).isTrue() val records = clientResponse.body()?.records ?: listOf() assertThat(records).hasSize(3) val record = records[0] assertThat(record.fields).hasSize(6) assertThat(record.fields["Weight"]).isEqualTo("80") assertThat(record.fields["Date"]).isEqualTo("2019-06-03T21:03:00.000Z }
: RoomDatabase() { abstract fun weightItemsDao(): WeightItemsDao } @Entity(tableName = "WeightItems") data class WeightItem( @PrimaryKey val id: String, val date: OffsetDateTime, val weight: Double, val notes: String? ) @Dao interface WeightItemsDao { @Insert(onConflict = REPLACE) suspend fun save(item: WeightItem) @Query("SELECT * from WeightItems") suspend fun all(): List<WeightItem> }
: RoomDatabase() { abstract fun weightItemsDao(): WeightItemsDao } @Entity(tableName = "WeightItems") data class WeightItem( @PrimaryKey val id: String, val date: OffsetDateTime, val weight: Double, val notes: String? ) @Dao interface WeightItemsDao { @Insert(onConflict = REPLACE) suspend fun save(item: WeightItem) @Query("SELECT * from WeightItems") suspend fun all(): List<WeightItem> }
: RoomDatabase() { abstract fun weightItemsDao(): WeightItemsDao } @Entity(tableName = "WeightItems") data class WeightItem( @PrimaryKey val id: String, val date: OffsetDateTime, val weight: Double, val notes: String? ) @Dao interface WeightItemsDao { @Insert(onConflict = REPLACE) suspend fun save(item: WeightItem) @Query("SELECT * from WeightItems") suspend fun all(): List<WeightItem> }
var weightsDatabase = WeightsDatabaseRule() @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun givenCleanStateAllShouldCallNetworkAndSaveInDB() { } }
var weightsDatabase = WeightsDatabaseRule() @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun givenCleanStateAllShouldCallNetworkAndSaveInDB() { } }
var weightsDatabase = WeightsDatabaseRule() @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun givenCleanStateAllShouldCallNetworkAndSaveInDB() { } }
var weightsDatabase = WeightsDatabaseRule() @get:Rule var instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun givenCleanStateAllShouldCallNetworkAndSaveInDB() { } @Test fun givenSavedItemsAllShouldUpdateDB() { } }
abstract fun refresh() sealed class ViewState { object Loading : ViewState() data class Error(val error: Throwable? = null) : ViewState() data class Success(val list: List<WeightItem>) : ViewState() } }
components together • TestObserver & InstantTaskExecutorRule • Modern test pyramid Tips • Integration tests increase confidence • Unit tests are disposable
MutableLiveData() override val state: LiveData<ViewState> = internalState override fun refresh() = Unit fun postViewState(viewState: ViewState) { internalState.postValue(viewState) } }