Share
Embed
Android Testing Strategy
Testing an Android application was always hard. In the past couple of years we got access to better tools, but we still rely a lot on architecture and design decisions in order to properly test an Android application. This talk will discuss the layers of automated testing an Android application should have, from End-to-end, Component, Integration to Unit tests. Also we will discuss BDD, TDD and Clean Architecture as tools to implement those layers.
Marcelo Benites
November 07, 2018
Transcript
114 slides
-
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) } -
class FakeCallGateway(private val calls: List<Call>) : CallGateway { override fun
getCalls(): List<Call> { return calls } } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls) ) } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) } -
data class State<T>(val name: Name, val value: T? = null)
{ enum class Name { IDLE, LOADING, LOADED, ERROR } } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() val callsCaptor = argumentCaptor<State<List<Call>>>() verify(listenerMock, times(3)).onCallUpdate(callsCaptor.capture()) } -
@Test fun shouldLoadCalls() { val calls = listOf( Call("John Lennon"),
Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val callManager = TalkdeskCallManager( FakeCallGateway(calls), Schedulers.trampoline(), Schedulers.trampoline() ) val listenerMock = mock<CallListener>() callManager.registerListener(listenerMock) callManager.loadCalls() val callsCaptor = argumentCaptor<State<List<Call>>>() verify(listenerMock, times(3)).onCallUpdate(callsCaptor.capture()) assertEquals(State(State.Name.LOADED, calls), callsCaptor.lastValue) } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler ) { } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) { } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var callsState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) .subscribe ( { calls -> updateCurrentState(currentState.copy(name = State.Name.LOADED, value = calls))}, { updateCurrentState(currentState.copy(name = State.Name.ERROR, value = null))} ) } private fun updateCurrentState(calls: State<List<Call>>) { this.currentState = calls this.listener?.onCallUpdate(currentState) } } -
class TalkdeskCallManager( private val callGateway: CallGateway, private val scheduler: Scheduler,
private val publishScheduler: Scheduler, private var currentState: State<List<Call>> = State(State.Name.IDLE) ) : CallManager { private var listener: CallListener? = null override fun loadCalls() { updateCurrentState(currentState.copy(name = State.Name.LOADING, value = null)) Single .fromCallable { callGateway.getCalls() } .subscribeOn(scheduler) .observeOn(publishScheduler) .subscribe ( { calls -> updateCurrentState(currentState.copy(name = State.Name.LOADED, value = calls))}, { updateCurrentState(currentState.copy(name = State.Name.ERROR, value = null))} ) } override fun registerListener(listener: CallListener) { this.listener = listener this.listener?.onCallUpdate(currentState) } private fun updateCallsState(calls: State<List<Call>>) { this.callsState = calls this.listener?.onCallUpdate(callsState) } } -
class FakeJsonConverter(private val calls: List<Call>) : JsonConverter { override fun
parse(json: String): List<Call> { return calls } } -
@Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val
baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val baseUrl = server.url("/").toString() val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) } -
@Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val
baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) gateway.getCalls() } -
@Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val
baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) } -
@Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val
baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) val request = server.takeRequest() assertEquals( "GET", request.method) assertEquals("/Calls", request.path) } -
@Test fun shouldReturnCallsFromHttpServerOn200StatusCode() { val server = MockWebServer() server.start() val
baseUrl = server.url("/").toString() server.enqueue(MockResponse().setResponseCode(200)) val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway(baseUrl, FakeJsonConverter(calls), OkHttpClient()) assertEquals(calls, gateway.getCalls()) val request = server.takeRequest() assertEquals( "GET", request.method) assertEquals("/Calls", request.path) server.shutdown() } -
class FakeCallManager( private val calls: State<List<Call>> ) : CallManager {
private var listener: CallListener? = null override fun loadCalls() { listener?.onCallUpdate(calls) } override fun registerListener(listener: CallListener) { this.listener = listener this.listener?.onCallUpdate(calls) } } -
class TestApplication: Application(), DependencyManager { var testCallManager: CallManager? = null
override fun getCallManager() = testCallManager!! } -
class CallActivity: AppCompatActivity(), CallFragmentContainer { private lateinit var dependencyManager: DependencyManager
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_call) dependencyManager = application as DependencyManager } override fun getCallManager() = dependencyManager.getCallManager() } -
class CallFragment : Fragment() { private lateinit var container: CallFragmentContainer
override fun onAttach(context: Context?) { super.onAttach(context) if (context is CallFragmentContainer) { container = context } else { throw IllegalStateException("Context must implement CallFragmentContainer") } } ….. } -
class CallFragment : Fragment() { private lateinit var container: CallFragmentContainer
private lateinit var callManager: CallManager override fun onAttach(context: Context?) { super.onAttach(context) if (context is CallFragmentContainer) { container = context } else { throw IllegalStateException("Context must implement CallFragmentContainer") } } override fun onActivityCreated(savedInstanceState: Bundle?) { super.onActivityCreated(savedInstanceState) callManager = container.getCallManager() } ….. } -
@Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @Test fun shouldShowLoadedCalls()
{ val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) } } -
@Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @Test fun shouldShowLoadedCalls()
{ val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) } } -
@Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @get:Rule val rule:
ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java, true, false) @Test fun shouldShowLoadedCalls() { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) rule.launchActivity(Intent()) } } -
@Config(application = TestApplication::class) @RunWith(AndroidJUnit4::class) class CallFragmentTest { @get:Rule val rule:
ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java, true, false) @Test fun shouldShowLoadedCalls() { val calls = listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) ApplicationProvider.getApplicationContext<TestApplication>().testCallManager = FakeCallManager(State(State.Name.LOADED, calls)) rule.launchActivity(Intent()) onView(withText("John Lennon")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } } -
class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =
listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) } } -
class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =
listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) gateway.getCalls() } } -
class HttpCallGatewayIntegrationTest { @Test fun shouldReturnCallsFromHttpServer() { val calls =
listOf( Call("John Lennon"), Call("Ringo Starr"), Call("George Harrison"), Call("Paul Mccartney") ) val gateway = HttpCallGateway("http://www.dev.talkdesk.com/", JsonObjectConverter(), OkHttpClient()) assertEquals(calls, gateway.getCalls()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { val server
= MockWebServer() server.start() val json = """ [ { "contact": "John Lennon" }, { "contact": "Ringo Starr" }, { "contact": "George Harrison" }, { "contact": "Paul Mccartney" } ] """.trimIndent() server.enqueue(MockResponse().setResponseCode(200).setBody(json)) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @Test fun shouldShowCalls() { // MockWebServer
setup - omitted for brevity val baseUrl = server.url("/").toString() val callGateway = HttpCallGateway(baseUrl, JsonObjectConverter(), OkHttpClient()) val callManager = TalkdeskCallManager(callGateway, Schedulers.trampoline(), AndroidSchedulers.mainThread()) val callApplication = ApplicationProvider.getApplicationContext<CallApplication>() val field = callApplication.javaClass.getDeclaredField("callManager") field.isAccessible = true field.set(callApplication, callManager) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,
true, false) @Test fun shouldShowCalls() { // Arrange - omitted for brevity callManager.loadCalls() rule.launchActivity(Intent()) } } -
@RunWith(AndroidJUnit4::class) class CallComponentTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,
true, false) @Test fun shouldShowCalls() { // Arrange - omitted for brevity callManager.loadCalls() rule.launchActivity(Intent()) onView(withText("John Lennon")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) server.shutdown() } } -
@RunWith(AndroidJUnit4::class) class CallEndToEndTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,
true, false) @Test fun shouldShowCalls() { rule.launchActivity(Intent()) } } -
@RunWith(AndroidJUnit4::class) class CallEndToEndTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,
true, false) @Test fun shouldShowCalls() { rule.launchActivity(Intent()) Thread.sleep(2000) } } -
@RunWith(AndroidJUnit4::class) class CallEndToEndTest { @get:Rule val rule: ActivityTestRule<CallActivity> = ActivityTestRule(CallActivity::class.java,
true, false) @Test fun shouldShowCalls() { rule.launchActivity(Intent()) Thread.sleep(2000) onView(withText("John Lennon")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Ringo Starr")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("George Harrison")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Paul Mccartney")).check(ViewAssertions.matches(withEffectiveVisibility(Visibility.VISIBLE))) } }