Your first test

You will first work on displaying the notes to the user. The notes presenter will provide the logic showing a progress indicator, which displays the notes and other note-related views.

Since the Presenter coordinates between the Model and View, you will have to mock them so you can focus on the class under test.

In this test, you will verify that asking the NotesPresenter to Add a new note will trigger a call to the View to show the add-note screen. Let's implement the `should display note when button is clicked`() test method.

You will first add a call to the presenter's addNewNote() method. Then, you will verify that the View's showAddNote() is called. Therefore, you call on one method and verify that it, in turn, calls another method (recall how the MVP pattern works; the presenter coordinates with the views).

For now, we will not worry about what the second call method does; this is unit testing, and you test one small thing (unit) at a time. So, you will have to mock out the View, and you don't need to implement it now. A few interfaces can achieve this; that is, an API or contract without necessarily implementing them. See the following final pieces of code:

import com.packtpub.eunice.notesapp.notes.NotesContract
import com.packtpub.eunice.notesapp.notes.NotesPresenter
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@Mock
private lateinit var notesView: NotesContract.View
private lateinit var notesPresenter: NotesPresenter

@Before
fun setUp() {
MockitoAnnotations.initMocks(this)

// The class under test
notesPresenter = NotesPresenter()
}

@Test
fun `should display note view when button is clicked`() {
// When adding a new note
notesPresenter.addNewNote()

// Then show add note UI
verify(notesView)?.showAddNote()
}

Now, create NotesContract, which is the View part of the MVP architecture. It will be an interface where only the methods are required to make the test pass:

interface NotesContract {
interface View {
fun showAddNote()
}

interface UserActionsListener {

fun loadNotes(forceUpdate: Boolean)

fun addNewNote()

fun openNoteDetails(requestedNote: Note)
}
}

Next, create the Note class. It represents the Model in the MVP architecture. It defines the structure of the notes for the notes-app you're building:

import java.util.UUID

data class
Note(val title: String?,
val description: String?,
val imageUrl: String? = null) {

val id: String = UUID.randomUUID().toString()
}

Create the NotesPresenter, which represents the Presenter in the MVP architecture. Let it implement the UserActionsListener in the NotesContract class:

class NotesPresenter: NotesContract.UserActionsListener {
override fun loadNotes(forceUpdate: Boolean) {
}

override fun addNewNote() {
}

override fun openNoteDetails(requestedNote: Note) {
}
}

That's enough for the first test. Are you ready? Okay, now click the right arrowhead beside the number on which the test method is defined. Or, you could equally right-click in or on the NotesPresenterTest file and select Run:

Your test should fail:

It failed because we expected the showAddNote() method of the NotesView class to be called, but it wasn't. This happened because you only implemented the interface in the Presenter class, but you never called the expected method in the NotesView class. 

Let's go ahead and fix that now.

First, update NotesPresenter to accept a NotesContract.View object in its primary constructor. Then, call the expected method, showAddNote(), within the addNewNote() method.

You should always prefer constructor injection to field injection. It is much easier to handle, and easier to read and maintain too.

Your NotesPresenter class should now look like this:

class NotesPresenter(notesView: NotesContract.View): NotesContract.UserActionsListener {
private var notesView: NotesContract.View = checkNotNull(notesView) {
"notesView cannot be null"
}

override fun
loadNotes(forceUpdate: Boolean) {
}

override fun addNewNote() = notesView.showAddNote()

override fun openNoteDetails(requestedNote: Note) {
}
}
checkNotNull is a built-in Kotlin utility function for verifying whether an object is null or not. Its second parameter takes a lambda function which should return a default message if the object is null.

Since the NotesPresenter now requires a NotesContract.View in its primary constructor, you'll have to update the test to cater for that:

@Before
fun setUp() {
MockitoAnnotations.initMocks(this)

// Get a reference to the class under test
notesPresenter = NotesPresenter(notesView)
}

The code has been refactored. Now, rerun the test:

Hooray! The test passes now; that's awesome. Excellent work. 

That's one complete cycle using TDD. Now, you need to keep going, and there are a few more tests to complete before the feature will be fully implemented.

Your next test is to validate that the presenter displays the notes as expected. In this process, the notes will have to be retrieved from the repository first before you update the view.

You will use similar test APIs from the previous test. There is a new one you'll learn here, however, which is called ArgumentCaptor. As you may have guessed, it captures the arguments passed to a method. You will use these to call another method and pass them in as parameters. Let's have a look:

@Mock
private lateinit var notesRepository: NotesRepository

@Captor
private var loadNotesCallbackCaptor: ArgumentCaptor<NotesRepository.LoadNotesCallback>? = null

private val
NOTES = arrayListOf(Note("Title A", "Description A"),
Note("Title A", "Description B"))
...

@Test
fun `should load notes from repository into view`() {
// When loading of Notes is requested
notesPresenter.loadNotes(true)

// Then capture callback and invoked with stubbed notes
verify(notesRepository)?.getNotes(loadNotesCallbackCaptor?.capture())
loadNotesCallbackCaptor!!.value.onNotesLoaded(NOTES)

// Then hide progress indicator and display notes
verify(notesView).setProgressIndicator(false)
verify(notesView).showNotes(NOTES)
}

Let's go over this again very briefly.

You first called the method you are testing, which is loadNotes(). Then, you verified that that action, in turn, gets the notes (getNotes()) using the NotesRepository instance, just like the previous test. You then verified that the instance passed to the getNotes() method, which is again used to load the notes (onNotesLoaded()). Afterwards, you verify that notesView hides the progress indicator (setProgressIndicator(false)) and displays the notes (showNotes()).

Leverage the Null Safety feature in Kotlin as much as possible. Instead of having nullable types for the mocks, use Kotlin's lateinit modifier instead.

This results in much cleaner code because then you don't have to have nullability checks everywhere, nor do you have to use the elvis operator, either.

Now, create the NotesRepository as follows:

interface NotesRepository {

interface LoadNotesCallback {

fun onNotesLoaded(notes: List<Note>)
}

fun getNotes(callback: LoadNotesCallback?)
fun refreshData()
}

Next, update the NotesContract:

interface NotesContract {
interface View {
fun setProgressIndicator(active: Boolean)

fun showNotes(notes: List<Note>)

...
}

...
}

You are all set to test your second test case now. Go ahead and run it:

Okay, it fails. And again, with TDD, that's perfect! You realize that this tells us exactly what is missing, and thus what needs to be done. You only have the contract (interface) implemented, but no further action goes on there.

Open up your NotesPresenter and refactor the code to make this test pass. You will first add the NotesRepository as part of the constructor parameters, and then you will make the call within the appropriate method. See the following code for the full implementation:

import com.packtpub.eunice.notesapp.data.NotesRepository
import com.packtpub.eunice.notesapp.util.EspressoIdlingResource

class
NotesPresenter(notesView: NotesContract.View, notesRepository: NotesRepository) :
NotesContract.UserActionsListener {

private var notesRepository: NotesRepository = checkNotNull(notesRepository) {
"notesRepository cannot be null"
}

override fun loadNotes(forceUpdate: Boolean) {
notesView.setProgressIndicator(true)
if (forceUpdate) {
notesRepository.refreshData()
}

EspressoIdlingResource.increment()

notesRepository.getNotes(object : NotesRepository.LoadNotesCallback {
override fun onNotesLoaded(notes: List<Note>) {
EspressoIdlingResource.decrement()
notesView.setProgressIndicator(false)
notesView.showNotes(notes)
}
})
}
...
}

You used constructor injection to inject a NotesRepository instance into NotesPresenter. You checked it for nullability just like you did for NotesContract.View.

In the loadNotes() method, you displayed the progress indicator and refreshed the data depending on the forceUpdate field.

You then used a utility class, EspressoIdlingResource, basically to alert Espresso of a possible asynchronous request. On getting the notes, you hide the progress indicator and showed the notes.

Create a util package to contain EspressoIdlingResource and SimpleCountingIdlingResource:

import android.support.test.espresso.IdlingResource

object EspressoIdlingResource {

private const val RESOURCE = "GLOBAL"

private val countingIdlingResource = SimpleCountingIdlingResource(RESOURCE)

val idlingResource = countingIdlingResource

    fun increment() = countingIdlingResource.increment()

fun decrement() = countingIdlingResource.decrement()
}

And for SimpleCountingIdlingResource :

package com.packtpub.eunice.notesapp.util

import android.support.test.espresso.IdlingResource
import java.util.concurrent.atomic.AtomicInteger

class SimpleCountingIdlingResource

(resourceName: String) : IdlingResource {

private val mResourceName: String = checkNotNull(resourceName)

private val counter = AtomicInteger(0)

@Volatile
private var resourceCallback: IdlingResource.ResourceCallback? =
null

override fun getName() = mResourceName

override fun isIdleNow() = counter.get() == 0

override fun registerIdleTransitionCallback(resourceCallback:
IdlingResource.ResourceCallback) {
this.resourceCallback = resourceCallback
}

fun increment() = counter.getAndIncrement()

fun decrement() {
val counterVal = counter.decrementAndGet()
if (counterVal == 0) {
// we've gone from non-zero to zero. That means we're idle
now! Tell espresso.
resourceCallback?.onTransitionToIdle()
}

if (counterVal < 0) {
throw IllegalArgumentException("Counter has been
corrupted!"
)
        }
}
}

Make sure to update your app's build dependencies with the EspressoIdlingResource library:

dependencies {
...
implementation "com.android.support.test.espresso:espresso-idling-resource:3.0.1"
...
}

Next, update the setUp method to initialize correctly the NotesPresenter class:

@Before
fun setUp() {
MockitoAnnotations.initMocks(this)

// Get a reference to the class under test
notesPresenter = NotesPresenter(notesView)
}

Now that everything's set, run the test:

Great! Really awesome stuff. You have successfully written the business logic for the NotesApp using a TDD approach. 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset