1. Before you begin
In previous codelabs you learned how to write and run both unit and instrumentation tests. This codelab introduces some best practices when writing tests, and how to add specific Gradle dependencies for testing. You will also get to practice writing more unit and instrumentation tests.
Prerequisites
- You have opened an existing project in Android Studio.
- You have written unit and instrumentation tests in Android Studio.
- You have some experience navigating projects in Android Studio.
- You have some experience working with
build.gradle
files in Android Studio.
What you'll learn
- The fundamentals of writing a test.
- How to add testing-specific Gradle dependencies.
- How to test lists with instrumentation tests.
What you need
- A computer with Android Studio installed.
- The solution code for the Affirmations app.
Download the starter code for this codelab
In this codelab you will add instrumentation tests to the solution code for the Affirmations app.
- Navigate to the provided GitHub repository page for the project.
- Verify that the branch name matches the branch name specified in the codelab. For example, in the following screenshot the branch name is main.
- On the GitHub page for the project, click the Code button, which brings up a popup.
- In the popup, click the Download ZIP button to save the project to your computer. Wait for the download to complete.
- Locate the file on your computer (likely in the Downloads folder).
- Double-click the ZIP file to unpack it. This creates a new folder that contains the project files.
Open the project in Android Studio
- Start Android Studio.
- In the Welcome to Android Studio window, click Open.
Note: If Android Studio is already open, instead, select the File > Open menu option.
- In the file browser, navigate to where the unzipped project folder is located (likely in your Downloads folder).
- Double-click on that project folder.
- Wait for Android Studio to open the project.
- Click the Run button
to build and run the app. Make sure it builds as expected.
2. Starter app overview
The Affirmations app consists of one screen that shows the user a list of images paired with words of affirmation.
3. Best practices
By design, test code looks different from the business logic for an application. That's because tests are not supposed to contain logic; they are only supposed to test it. Therefore, tests should not have conditional statements like if
or when
, or control flow statements like for
or while
. They should also not manipulate values or conduct any real computation.
Occasionally, your tests may require some of these things, but in general you should avoid them. Since this is the type of logic that we want to test in our app, if we had that kind of code in a test, it could fail in the same way that our app code could fail.
Our unit tests should only call the piece of code from our app that is necessary for the test and test the values or state of the code that results from calling that code. UI tests should only test for the expected state of the user interface. This concept may take a while to sink in, but that's okay! There are some topics that help to explain this concept that we will cover in future codelabs. In the meantime, as we write more tests, pay careful attention to the approaches we take to writing the tests.
4. Create the tests directories
In a previous codelab, you learned how to create an androidTest
directory for instrumentation tests. Repeat that process for this project for both the androidTest
directory and the test
directory. The process is the same for both, the only difference being that for test
directory, you must select test/java from the New Directory dropdown instead of androidTest/java. Create a new package for each new directory called com.example.affirmations.
5. Create an instrumentation test class
Create a new class in androidTest -> com.example.affirmations called AffirmationsListTests.kt
.
As with the Dice Roller app, Affirmations only has a single activity. In order to test the UI of the activity, we have to specify that we want it to launch. See if you can recall how to do this on your own!
- Add a test runner to the newly created class.
@RunWith(AndroidJUnit4::class)
- Make an activity scenario rule for the main activity.
@get:Rule
val activity = ActivityScenarioRule(MainActivity::class.java)
- Affirmations app displays a list of images and their respective positive affirmations. The UI doesn't allow for any interaction with the items (like clicking or swiping, for example). So for this app, the instrumentation test only tests static data. Create a test method called
scroll_to_item()
. Remember that it must be annotated with@Test
.
This test should scroll to a specific item contained in the list. We haven't covered this approach yet, since it requires a method that our project doesn't have a reference to yet. Before continuing with the test, we need to add some testing dependencies.
6. Adding instrumentation test dependencies
You should already have some familiarity with adding Gradle dependencies for use in your app code. Gradle also lets us add dependencies specifically for unit tests and instrumentation tests. Open up your app level build.gradle
file located at app -> build.gradle. In the dependencies section there are three kinds of implementations for dependencies: implementation
, testImplementation
, and androidTestImplementation
.
implementation
is for dependencies that will be used in the application itself, testImplementation
is for dependencies that are used in unit tests, and androidTestImplementation
is for dependencies that are used in instrumentation tests.
- Add a dependency to allow interaction with
RecyclerView
's in instrumentation tests. Add the following library as anandroidTestImplementation
:
androidx.test.espresso:espresso-contrib:3.4.0
The dependencies look something like this:
dependencies {
...
androidTestImplementation
'androidx.test.espresso:espresso-contrib:3.4.0'
}
- Now sync the project.
7. Test the RecyclerView
- Once the project is synced, go back to the
AffirmationsListTests.kt
file. Provide aViewInteraction
to perform an action on withonView()
. TheonView()
method requires aViewMatcher
to be passed in. UsewithId()
, making sure to pass in the ID of theRecyclerView
that was used for the affirmations. Now callperform()
on theViewInteraction
. This is where the newly added dependency comes into play! TheRecyclerViewActions.scrollToPosition<RecyclerView.Viewholder>(9) ViewAction
can now be passed in.
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollToPosition<RecyclerView.ViewHolder>(9)
)
Understanding the syntax of this line is not critical, but it's worth exploring. The name RecyclerViewActions
is exactly what the name implies: a class that lets your tests take actions on a RecyclerView
. scrollToPosition()
is a static method from the RecyclerViewActions
class that will scroll to a specified position. This method returns what is called a Generic. Generics are outside of the scope of this codelab, but in this case you can think of it as the scrollToPosition()
method returning whatever the items in the RecyclerView
are, which could be anything.
In our app, the items in our RecyclerView
are ViewHolder
s, so we place a pair of angle brackets after the method call and in them we specify RecyclerView.ViewHolder
. Finally, pass in the final position in the list (9
).
- Now that scrolling to the desired position of the
RecyclerView
is enabled, make an assertion to ensure that the UI is displaying the expected information. Make sure that once you have scrolled to the last item, the text associated with the final affirmation is displayed. Start with aViewInteraction
, but this time pass in a newViewMatcher
(in this case,withText()
). To that method, pass the string resource that contains the text of the last affirmation. ThewithText()
method identifies a UI component based on the text it displays. For this component, all that needs to be done is to check that it displays the desired text. This is done by callingcheck()
on theViewInteraction
.check()
requires aViewAssertion
, for which you can use thematches()
method. Finally, make the assertion that the UI component is displayed by passing the methodisDisplayed()
.
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed()))
Going back to the note about hard coding a position to scroll to, there is a way that this can be overcome using RecyclerViewActions
. When you are unsure of the length of your list you can use the scrollTo
action. The scrollTo
function requires a Matcher<View!>!
to find a particular item. This can be a number of things, but to serve the purposes of this test, use withText
. Applying this to the test you just wrote, the code would look like this:
onView(withId(R.id.recycler_view)).perform(
RecyclerViewActions
.scrollTo<RecyclerView.ViewHolder>(
withText(R.string.affirmation10)
)
)
onView(withText(R.string.affirmation10))
.check(matches(isDisplayed())
)
Now everything is ready to run the test. You should see the device or emulator scroll to the bottom of the list, and subsequently that the test passes. If you want to make sure that the test result is accurate, replace the string ID with R.string.affirmation1
. After scrolling, this string resource is not displayed and the test should fail.
There are a number of methods available in the RecyclerViewActions
class, and we encourage you to take a look at the available methods.
8. Create a local test class
Create a new class in test -> com.example.affirmations called AffirmationsAdapterTests.kt
.
9. Adding local test dependencies
- Earlier in this codelab we discussed three different types of dependency implementations and you added a dependency for instrumentation tests. Now add a dependency for local tests. Navigate to app -> build.gradle and add the following as a unit test dependency:
org.mockito:mockito-core:3.12.4
The dependencies should look something like this:
dependencies {
...
testImplementation 'org.mockito:mockito-core:3.12.4'
}
- Now sync the project.
10. Test the adapter
This particular app does not lend itself to unit testing as there is not much logic to test. However, we can gain some more experience testing various components as a preparation for future testing.
- Place the following line in the unit test class:
private val context = mock(Context::class.java)
The mock()
method comes from the library we just implemented in our project. Mocking is an integral part of unit tests, but it is out of the scope of this codelab. We will go over mocking in more detail in a separate codelab. In Android, Context
is the context of the current state of the app, but remember that unit tests run on the JVM and not on an actual device, so there is no Context
. The mock method allows us to create a "mocked" instance of a Context
. It doesn't have any real functionality, but it can be used to test methods that require a context.
- Create a function called
adapter_size()
and annotate it as a test. The goal of this test is to make sure that the size of the adapter is the size of the list that was passed to the adapter. To do this, create an instance ofItemAdapter
and pass in the list returned by theloadAffirmations()
method in theDatasource
class. Alternatively, create a new list and test that. For unit tests, it's best practice to create our own data unique to the test, so we'll create a custom list for this test.
val data = listOf(
Affirmation(R.string.affirmation1, R.drawable.image1),
Affirmation(R.string.affirmation2, R.drawable.image2)
)
- Now create an instance of the
ItemAdapter
, passing in thecontext
anddata
variables created in the previous steps.
val adapter = ItemAdapter(context, data)
Recycler view adapters have a method that returns the size of the adapter called getItemCount()
. For this app the method looks like this:
/**
* Return the size of your dataset (invoked by the layout manager)
*/
override fun getItemCount() = dataset.size
- This is the method that should be tested. Make sure that the returned value from this method matches the size of the list you created in step 2. Use the
assertEquals()
method and compare the values of the list size and the adapter size.
assertEquals("ItemAdapter is not the correct size", data.size, adapter.itemCount)
You're already familiar with the assertEquals()
method, but it's worth examining the line to be thorough. The first parameter is a string that displays in the test result if the test fails. The second parameter is the expected value. The third parameter is the actual value. Your test class should look like this:
- Now run the test!
11. Solution code
12. Congratulations
In this codelab you:
- Learned how to add testing-specific dependencies.
- Learned how to interact with a
RecyclerView
with instrumentation tests. - Discussed some fundamental best practices for testing.