Writing Tests

Now that your initial setUp() function is written, you are ready to write your tests. A test is a function in your test class annotated with @Test.

Start by writing a test that asserts existing behavior in SoundViewModel: The title property is connected to the Sound’s name property. Write a function that tests this (Listing 20.8).

Listing 20.8  Testing the title property (SoundViewModelTest.kt)

class SoundViewModelTest {
    ...
    @Before
    fun setUp() {
        ...
    }

    @Test
    fun exposesSoundNameAsTitle() {
        assertThat(subject.title, `is`(sound.name))
    }
}

Two functions will show up red: the assertThat(…) function and the is(…) function. Key in Option-Return (Alt-Enter) on assertThat(…) and select Assert.assertThat(…) from org.junit. Do the same for the is(…) function, selecting Is.is from org.hamcrest.

This test uses the is(…) Hamcrest matcher with JUnit’s assertThat(…) function. The code reads almost like a sentence: Assert that subject’s title property is the same value as sound’s name property. If those two functions return different values, the test fails.

To run your unit tests, right-click on the SoundViewModelTest class name and select Run 'SoundViewModelTest'. Android Studio’s bottom pane shows the results (Figure 20.4).

Figure 20.4  Passing tests

Passing tests

By default, the test display only shows details for failing tests, since those are the only tests that are interesting. So this output means that everything is A-OK – your tests ran, and they passed.

Testing object interactions

Now for the real work: building out the interactions between SoundViewModel and your new BeatBox.play(Sound) function. A common way to go about this is to write a test that shows what you expect a new function to do before you have written the function. You are going to write a new function on SoundViewModel called onButtonClicked() that calls BeatBox.play(Sound). Write a test function that calls onButtonClicked() (Listing 20.9).

Listing 20.9  Writing a test for onButtonClicked() (SoundViewModelTest.kt)

class SoundViewModelTest {
    ...
    @Test
    fun exposesSoundNameAsTitle() {
        assertThat(subject.title, `is`(sound.name))
    }

    @Test
    fun callsBeatBoxPlayOnButtonClicked() {
        subject.onButtonClicked()
    }
}

That function does not exist yet, so it shows up in red. Put your cursor over it and key in Option-Return (Alt-Enter). Then select Create member function 'SoundViewModel.onButtonClicked', and the function will be created for you.

Listing 20.10  Creating onButtonClicked() (SoundViewModel.kt)

class SoundViewModel : BaseObservable() {
    fun onButtonClicked() {
        TODO("not implemented") //To change ...
    }
    ...
}

For now, leave it empty (or delete the TODO, if Android Studio adds one) and key in Command-Shift-T (Ctrl-Shift-T) to return to SoundViewModelTest.

Your test calls the function, but it should also verify that the function does what you say it does: calls BeatBox.play(Sound). The first step to implementing this is to provide SoundViewModel with a BeatBox object.

You could create an instance of BeatBox in your test and pass it to the view model constructor. But if you do that in a unit test, you create a problem: If BeatBox is broken, then tests you write in SoundViewModel that use BeatBox might break, too. That is not what you want. SoundViewModel’s unit tests should only fail when SoundViewModel is broken.

In other words, you want to test the behavior of SoundViewModel and its interactions with other classes in isolation. This is a key tenet of unit testing.

The solution is to use a mocked BeatBox. This mock object will be a subclass of BeatBox that has all the same functions as BeatBox – but none of those functions will do anything. That way, your test of SoundViewModel can verify that SoundViewModel itself is using BeatBox correctly, without depending at all on how BeatBox works.

To create a mock object with Mockito, you call the mock(Class) static function, passing in the class you want to mock. Create a mock of BeatBox and a field to store it in SoundViewModelTest (Listing 20.11).

Listing 20.11  Creating a mock BeatBox (SoundViewModelTest.kt)

class SoundViewModelTest {

    private lateinit var beatBox: BeatBox
    private lateinit var sound: Sound
    private lateinit var subject: SoundViewModel

    @Before
    fun setUp() {
        beatBox = mock(BeatBox::class.java)
        sound = Sound("assetPath")
        subject = SoundViewModel()
        subject.sound = sound
    }
    ...
}

The mock(Class) function will need to be imported, just like a class reference. This function will automatically create a mocked-out version of BeatBox for you. Pretty slick.

With the mock BeatBox ready, you can finish writing your test to verify that the play function is called. Mockito can help you do this odd-sounding job. All Mockito mock objects keep track of which of their functions have been called, as well as what parameters were passed in for each call. Mockito’s verify(Object) function can then check to see whether those functions were called the way you expected them to be called.

Call verify(Object) to ensure that onButtonClicked() calls BeatBox.play(Sound) with the Sound object you hooked up to your SoundViewModel (Listing 20.12).

Listing 20.12  Verifying that BeatBox.play(Sound) is called (SoundViewModelTest.kt)

class SoundViewModelTest {
    ...
    @Test
    fun callsBeatBoxPlayOnButtonClicked() {
        subject.onButtonClicked()

        verify(beatBox).play(sound)
    }
}

verify(Object) uses a fluent interface. It is an abbreviation for the following code:

    verify(beatBox)
    beatBox.play(sound)

Calling verify(beatBox) says, I am about to verify that a function was called on beatBox. The next function call is then interpreted as, Verify that this function was called like this. So your call to verify(…) here means, Verify that the play(…) function was called on beatBox with this specific sound as a parameter.

No such thing has happened, of course. SoundViewModel.onButtonClicked() is empty, so beatBox.play(Sound) has not been called. Also, SoundViewModel does not even hold a reference to beatBox, so it cannot call any functions on it. This means that your test should fail. Because you are writing the test first, that is a good thing – if your test does not fail at first, it must not be testing anything.

Run your test to see it fail. You can follow the same steps from earlier, or key in Control-R (Shift-F10) to repeat the last run command you performed. The result is shown in Figure 20.5.

Figure 20.5  Failing test output

Failing test output

The output says that your test expected a call to beatBox.play(Sound) but did not receive it:

    Wanted but not invoked:
    beatBox.play(
        com.bignerdranch.android.beatbox.Sound@3571b748
    );
    -> at ….callsBeatBoxPlayOnButtonClicked(SoundViewModelTest.java:28)
    Actually, there were zero interactions with this mock.

Under the hood, verify(Object) made an assertion, just like assertThat(…) did. When that assertion failed, it caused the test to fail and logged this output describing what went wrong.

Now to fix your test. First, create a constructor property for SoundViewModel that accepts a BeatBox instance.

Listing 20.13  Providing a BeatBox to SoundViewModel (SoundViewModel.kt)

class SoundViewModel(private val beatBox: BeatBox) : BaseObservable() {
    ...
}

This change will cause two errors in your code: one in your tests and one in your production code. Fix the production error first. Open MainActivity.kt and provide the beatBox object to your view models when they are created in SoundHolder (Listing 20.14).

Listing 20.14  Fixing the error in SoundHolder (MainActivity.kt)

private inner class SoundHolder(private val binding: ListItemSoundBinding) :
    RecyclerView.ViewHolder(binding.root) {

    init {
        binding.viewModel = SoundViewModel(beatBox)
    }

    fun bind(sound: Sound) {
        ...
    }
}

Next, provide the mock version of BeatBox to your view model in your test class (Listing 20.15).

Listing 20.15  Providing the mock BeatBox in test (SoundViewModelTest.kt)

class SoundViewModelTest {
    ...
    @Before
    fun setUp() {
        beatBox = mock(BeatBox::class.java)
        sound = Sound("assetPath")
        subject = SoundViewModel(beatBox)
        subject.sound = sound
    }
    ...
}

This gets you halfway to a passing test. Next, implement onButtonClicked() to do what the test expects (Listing 20.16).

Listing 20.16  Implementing onButtonClicked() (SoundViewModel.kt)

class SoundViewModel(private val beatBox: BeatBox) : BaseObservable() {
    ...
    fun onButtonClicked() {
        sound?.let {
            beatBox.play(it)
        }
    }
}

Rerun your test. This time you should see green, indicating that all your tests passed (Figure 20.6).

Figure 20.6  All green, all good

All green, all good
..................Content has been hidden....................

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