Although mobile applications have been around for several years, developing on Android is still challenging. In this talk, we will see how to write an Android application using clean architecture and TDD methodology, in order to write (almost) fully tested and easy to maintain applications.
The talk is meant to show how to write tests for the common task of loading and displaying an API to users. We’ll also discuss how to structure the code in order to avoid any dependency chaos and test every single component in isolation.
The talk will also make use of Kotlin: the first real alternative to Java within the Android world.
7. What’s Clean Architecture
“The ratio of time spent reading
(code) versus writing is well over
10 to 1 … (therefore) making it
easy to read makes it easier to
write.”
“Write code for your
mates not for the
machine.”
8. But which (real) architecture?
Model-View-Presenter
Model-View-ViewModel Viper
Model-View-Adapter
12. Real MVP Architecture
UI
Activity/
view
Presenter Interactor Repository
API
DB
Android Plain Java/Kotlin
13. Real MVP Architecture
UI
Activity/
view
Presenter Interactor Repository
API
DB
Android Plain Java/Kotlin
Background Thread
14. Real MVP Architecture
UI
Activity/
view
Presenter Interactor Repository
API
DB
Plain Java/Kotlin
Background Thread
Espresso Unit Test
Android
15. Try to not re-invent the wheel
Retrofit
Dagger2
Android-priority-queue v2
EventBus
16. Try to not re-invent the wheel
Retrofit
Dagger2
Android-priority-queue v2
EventBus
http request
dependency injection
job scheduler
communication
17. Why Kotlin
Can be used with existing Java frameworks and libraries
Costs nothing to adoptCompiles to JVM bytecode
No runtime overhead
Streams & lambdas
Because Lucio is in the room and can’t stand
Java for too long
18. The actual goal
Write an Android application to display first
generation of Pokemon in a Grid.
19. The actual goal
Write an Android application to display first
generation of Pokemon in a Grid.
22. interface PokemonService {
@GET("api/v2/pokemon")
fun listPokemon(@Query("offset") offset: String = "0"): Call<ListPokemonResponse>
}
class ListPokemonResponse(val count : Int, val results: List<PokemonResponse>)
data class PokemonResponse(val url: String = "", val name: String)
NO TESTS HERE
23. class PokemonRestClientTest {
lateinit var underTest: PokemonRestClient
@Before
fun setUp() {
underTest = PokemonRestClientImpl()
}
@Test
fun `shouldCallRetrofitClient_andReturnResponse`() {
}
}
24. class PokemonRestClientTest {
lateinit var underTest: PokemonRestClient
@Mock
lateinit var pokemons: ListPokemonResponse
@Before
fun setUp() {
underTest = PokemonRestClientImpl()
}
@Test
fun `shouldCallRetrofitClient_andReturnResponse`() {
val result = underTest.listPokemon()
assertEquals(pokemons, result)
}
}
25.
class PokemonRestClientTest {
lateinit var underTest: PokemonRestClient
@Mock
lateinit var service: PokemonService
@Mock
lateinit var call: Call<ListPokemonResponse>
@Mock
lateinit var response: Response<ListPokemonResponse>
@Mock
lateinit var pokemons: ListPokemonResponse
@Before
fun setUp() {
Mockito.`when`(service.listPokemon()).thenReturn(call)
Mockito.`when`(call.execute()).thenReturn(response)
Mockito.`when`(response.body()).thenReturn(pokemons)
underTest = PokemonRestClientImpl(service)
}
@Test
fun `shouldCallRetrofitClient_andReturnResponse`() {
val result = underTest.listPokemon()
verify(service).listPokemon()
assertEquals(pokemons, result)
verifyNoMoreInteractions(service)
}
}
29. class PokemonRepositoryTest {
lateinit var underTest: PokemonRepository
@Before
fun setUp() {
underTest = PokemonRepositoryImpl()
}
@Test
fun `shouldCallServiceListPokemon`() {
}
}
30. class PokemonRepositoryTest {
lateinit var underTest: PokemonRepository
@Mock
lateinit var client: PokemonRestClient
@Before
fun setUp() {
underTest = PokemonRepositoryImpl(client)
}
@Test
fun `shouldCallServiceListPokemon`() {
underTest.getPokemon()
verify(client).listPokemon()
verifyNoMoreInteractions(client)
}
}
31. interface PokemonRepository {
fun getPokemon()
}
class PokemonRepositoryImpl(val client: PokemonClient) : PokemonRepository{
override fun getPokemon(){
client.listPokemon()
}
}
32. class PokemonRepositoryTest {
val SQUIRTLE = "Squirtle"
val CHARMENDER = "Charmender"
val NUMBER_OF_POKEMONS = 2
lateinit var underTest: PokemonRepository
@Mock
lateinit var client: PokemonClient
@Before
fun setUp() {
underTest = PokemonRepositoryImpl(client)
}
@Test
fun `shouldReturnPokemonsFromNetwork`() {
val squirtle = PokemonResponse(name = SQUIRTLE)
val charmender = PokemonResponse(name = CHARMENDER)
val pokemonsResponse = ListPokemonResponse(NUMBER_OF_POKEMONS, listOf(squirtle, charmender))
Mockito.`when`(client.listPokemon()).thenReturn(pokemonsResponse)
val result = underTest.getPokemon()
assertNotNull(result)
assertEquals(NUMBER_OF_POKEMONS, result.size)
assertEquals(SQUIRTLE, result[0].name)
assertEquals(CHARMENDER, result[1].name)
}
}
33. class PokemonRepositoryTest {
val SQUIRTLE = "Squirtle"
val CHARMENDER = "Charmender"
val NUMBER_OF_POKEMONS = 2
@Rule @JvmField var mockitoRule = MockitoJUnit.rule()
lateinit var underTest: PokemonRepository
@Mock
lateinit var client: PokemonRestClient
@Before
fun setUp() {
val squirtle = PokemonResponse(name = SQUIRTLE)
val charmender = PokemonResponse(name = CHARMENDER)
val pokemonsResponse = ListPokemonResponse(NUMBER_OF_POKEMONS, listOf(squirtle, charmender))
Mockito.`when`(client.listPokemon()).thenReturn(pokemonsResponse)
underTest = PokemonRepositoryImpl(client)
}
@Test
fun `shouldCallPokemonRestClient`() {
underTest.getPokemon()
verify(client).listPokemon()
verifyNoMoreInteractions(client)
}
@Test
fun `shouldReturnPokemonsFromNetwork`() {
val result = underTest.getPokemon()
assertNotNull(result)
assertEquals(NUMBER_OF_POKEMONS, result.size)
assertEquals(SQUIRTLE, result[0].name)
assertEquals(CHARMENDER, result[1].name)
}
}
Those are Pokemon objects not
PokemonResponse objects