Automated testing is important to ensure the quality of your project’s App or Library. Android development tools have historically lacked support for automated testing, but as Android has evolved over the years, Google has made a lot of efforts to make it easier for developers to test Android code. Some of the older frameworks have been updated and new ones added to ensure that we can thoroughly test the App and Library. Not only can we run them from Android Studio, but we can also run test cases directly from the command line interface using Gradle.
In this chapter, we’ll explore different testing methods for testing Android applications and libraries, and see how Gradle builds automated testing processes.
This chapter mainly includes the following contents:
- Unit testing
- Functional (UI) testing
- Test coverage
Unit testing
Well-written unit tests in a project not only ensure program quality, but also make it easy to check whether new code breaks the correctness of existing functional code. Android Studio and The Android Gradle Plugin support unit testing natively, but we still need to fit it properly.
JUnit
JUnit is a very popular unit testing library that has been around for over a decade. It is very easy to write test code and the test code is easy to understand and read. Keep in mind that these specific unit tests are only used to test business logic code, Android SDK-related code is not available through Junit. If we want to test Android SDK-related code in unit test code, we can do so using the Roboletric framework, as described in the following sections.
Before we start writing JUnit tests for our Android project, we need to create a directory for our tests. By convention, this directory is called test, and it should be at the same level as the home directory. The directory structure is as follows:
app
|-src
| |-main
| | |-java
| | |-com.example.app
| |-res
|-test
|-java
|-com.example.app
Copy the code
We can in the SRC/test/Java/com. The example. The app to create test cases
We need to add JUnit library dependencies before we can use the functionality. We can use JUnit 4.x to ensure this by adding test-built dependencies:
Dependencies {testImplementation 'junit:junit:4.12'}Copy the code
Note that the testImplementation is used instead of implementation. This configuration is used to ensure that Junit dependencies are compiled only when tests are run (and Junit library code cannot be referenced in non-test code) and not built when the application is packaged. Dependencies added with testImplementation are not included in the APK generated by normal packaging.
If we have any special conditions in a Build Type or Product Flavor, we can separately add test-only dependencies to that particular Type. For example, if you just want to add JUnit tests to the paid version, you can do the following:
Dependencies {testPaidImplementation 'junit:junit:4.12'}Copy the code
When the configuration is complete, you can start writing test cases. Here is a simple example to test the method of adding two numbers:
import org.junit.Test; import static org.junit.Assert.assertEquals; public class LogicTest { @Test public void addingNegativeNumberShouldSubtract() { Logic logic = new Logic(); assertEquals("6 + -2 must be 4", 4, logic.add(6, -2)); assertEquals("2 + -5 must be -3", -3, logic.add(2, -5)); }}Copy the code
To run all tests using Gradle, simply execute the gradlew test command. If you want to run tests for only one Build Variant, simply add the name of the corresponding Build Variant. For example, if we only want to run tests on debug variant, we just need to run gradlew testDebug. If the test fails, Gradle displays an error message in the command line interface. If all tests run smoothly, Gradle displays a BUILD SUCCESSFUL message.
A single failed test will cause the test task to fail and immediately stop the whole process. This means that some test cases are never executed in the event of a failure. To ensure that the entire Test suite is executed for all builds, use the continue flag:
$ gradlew test --continue
Copy the code
To write tests for different Build variants, you need to add the test code in the specified Build Variant code directory. For example, if you want to test the application of the paid version of a particular behavior, you need to put the test class testPaid/SRC/Java/com. Example. In the app.
If you don’t want to run the entire Test Suit, but only a specific class of tests, you can use:
$ gradlew testDebug --tests="*.LogicTest"
Copy the code
Test tasks not only run all tests, and finally to create a test report, can be in the app/build/reports/tests/debug/index. The HTML. If there are any failures, this report makes it easy to find problems with the tests. Test reports can be particularly useful in situations where tests are executed automatically. By default, Gradle creates a test report for each Build Variant that runs a test.
If all tests run successfully, our unit test report shows the following:
You can also run tests in Android Studio. Running a unit test in AS gets immediate feedback from the IDE, and if the test fails, we can click on the failed test and navigate to the corresponding code. If all tests pass, the Run window will look like this:
Regular unit testing is not ideal if the test code includes testing a specific Android class or resource portion. We try to run, but we will get the following Java lang. RuntimeException: Stub! Error. To solve this problem, we need to implement each method in the Android SDK ourselves, or use a Mock framework. Fortunately, several third-party libraries already handle the Android SDK. The most popular is Robolectric, which provides an easy way to test the functionality of Android code without the need for a device or emulator.
Robolectric
Using Robolectric for unit testing, you can write test cases using the Android SDK and Resource. These test cases can run tests directly in the Java virtual machine, which means we don’t need to use a real machine or emulator to run the test cases, so we can test the behavior of the UI components of the application or library more quickly.
To start using Robolectric, set includeAndroidResources to true in the build.gradlew file so that unit tests can access android-related resources files. Such as assets, manifest, pictures, character resources and so on; The next step is to add the Robolectric framework configuration to the dependency configuration
android { testOptions { unitTests { includeAndroidResources = true } } } dependencies { // testImplementation 'org. Robolectric: robolectric: 4.6.2'}Copy the code
Robolectric test class like conventional unit testing, should be placed in the SRC/test/Java/com. The example. The app directory. The difference is that we can now write tests that involve Android classes and resources. For example, the following test code verifies that the text of a TextView changes after a particular button is clicked:
MainActivity logic is relatively simple, click BTN in the interface,textView text becomes “Hello World!”
The subsequent encoding uses Kotlin, please note
class MainActivity : AppCompatActivity() {
lateinit var button: Button
lateinit var message: TextView
override fun onCreate(savedInstanceState: Bundle?). {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button = findViewById(R.id.btn_click)
button.setOnClickListener { message.text = "Hello World!" }
message = findViewById(R.id.textView)
}
}
Copy the code
ActivityScenario is an API related to the AndroidX Test Core framework, which is an official test adaptation of Robolectric and other three-party frameworks. In the mock android related components (Activity, Service, Intent, Context, etc.) should be through the test the core framework of API access.
'1.4.0 testImplementation' androidx. Test: the core.Copy the code
Add the Test Core dependency and run the following code
@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
@Test
fun clickingButton_shouldChangeMessage(a) {
val activityScenario = ActivityScenario.launch(MainActivity::class.java)
activityScenario.onActivity { activity ->
activity.button.performClick()
assertEquals(activity.message.text, "Hello World!")}}}Copy the code
Functional (UI) testing
Functional tests are used to test that several UI components of an application work as expected. For example, we could create a functional test to confirm that clicking a button opens a new Activity. There are several third-party functional testing frameworks for Android available in the open source community, but the easiest way is to use the official suggested Espresso framework.
Espresso
Google created the Espresso library to make it easier for developers to write functional test code. We can rely on the Espresso module through the AndroidX Library.
To run tests on devices, you need to define a Test Runner. Google provides AndroidJUnitRunner Test Runner through the Test Support Library, which helps you run JUnit Test classes on Android devices. The test runner loads the application APK and test APK into the device, runs all the tests, and then generates a test report.
If you have downloaded the Espresso Support Library, you can introduce Espresso by:
android {
defaultConfig {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
}
Copy the code
You also need to set up a few dependencies before you can start using Espresso:
dependencies { compile fileTree(dir: 'libs', include: [' *. Jar ']) androidTestImplementation 'androidx. Test. Espresso: espresso - core: 3.1.0' androidTestImplementation 'androidx. Test: runner: 1.1.0' androidTestImplementation 'androidx. Test: rules: 1.1.0'}Copy the code
You need to reference the AndroidX Test and Espresson libraries for testing to get started with Espresso.
Note that these dependencies use the androidTestCompile configuration, not the testCompile configuration we used earlier. This is to distinguish unit testing from functional testing.
With all of the above Settings in place, we can start adding tests. Functional tests are placed in a different directory than regular unit tests. Like use depend on the configuration, you need to use androidTest instead of the test, so the correct directory is androidTest/SRC/Java/com. The example. The app below is a sample test class, It checks that the text of the TextView in the MainActivity is correct:
@RunWith(AndroidJUnit4::class) @LargeTest class MainActivityTest { @get:Rule val activityRule = ActivityTestRule(MainActivity::class.java) @Test fun listGoesOverTheFold() { onView(withText("Hello!" )).check(matches(isDisplayed())) } }Copy the code
Before running Espresso tests, we need to make sure that the real machine or emulator is properly connected. If you forget to connect the device, the test task will throw the following exception:
Execution failed for task ':app:connectedAndroidTest'.
>com.android.builder.testing.api.DeviceException:
java.lang.RuntimeException: No connected devices!
Copy the code
After connecting to the real machine or starting the emulator, you can run Espresso tests with the gradlew connectedCheck command. This task will be executed connectedAndroidTest task to run all the connections of equipment on the Debug build type test, and use createDebugCoverageReport create test report.
If you run Espresson, you get the following error:
Error: duplicate files during packaging of APK app-androidTest.apk Path in archive: LICENSE.txt Origin 1: ... \ hamcrest - library - 1.1. Jar Origin 2:... \ junit dep - 4.10 - jarCopy the code
The error message itself makes it very clear that Gradle could not complete the build due to duplicate files. We can fix this by excluding the LICENTSE file:
//You can ignore those files in your build.gradle:
android {
packagingOptions {
exclude 'LICENSE.txt'
}
}
Copy the code
The generated test reports can be found under Build/Outputs /reports/ androidTests/ Connected in the app directory. Open index.html to view the report, as shown below:
The functional test report shows the device and Android version on which the test is running. We can run these tests on multiple devices at the same time, so this information makes it easier to find bugs on a particular device or on a particular version of Android.
To get feedback on our tests in Android Studio, we simply set the Run /debug property to run the tests directly from the IDE. The Run/DEBUG configuration represents a set of startup properties to run or debug programs. The Android Studio toolbar configuration selector allows you to select the Run/Debug configuration to use.
To set a new configuration, click Edit Configurations… Open the configuration editor and create a new Android test configuration. Select the module and set Instrumentation Runner to AndroidJUnitRunner, as shown below:
After you save this new configuration, you can select it in the configuration selector, and then click the Run button to Run all the tests.
Running Espresso tests from Android Studio has a warning: “The test report is not generated.” The reason is that Android Studio performs the connectedAndroidTest task instead of connectedCheck, which is the task responsible for generating test reports.
Test coverage
Once we start writing tests for the Android project, code test coverage will become an important indicator of how much code is tested. There are many test coverage tools available in Java, but Jacoco is the most popular. Gradle uses Jacoco as the test coverage tool by default, and it’s easy to get started.
Jacoco
Enabling coverage reporting is very easy. All we need to do is set testCoverageEnabled = true on the build type we want to test. For example, enable Debug Build Type test coverage, as shown below:
buildTypes {
debug {
testCoverageEnabled = true
}
}
Copy the code
When test coverage is enabled, the test coverage report is created when we execute Gradlew connectedCheck. Create a report of the task itself is createDebugCoverageReport. Even if it has no record and does not appear in the task list when running the Gradlew task, we can run it directly. However, because createCoverageReport relies on connectedCheck, they cannot be executed separately. The dependency on connectedCheck also means that we need a connected device or emulator to generate test coverage reports.
After task execution, we can in the app/build/outputs/reports/coverage/debug/index. The HTML directory to find coverage reports. Each Build Variant has its own report directory because each Variant can have different tests. The test coverage report style is as follows:
The report gives a good overview of coverage at the Class level, and you can click here for more information. In the most detailed view, you can see which code is tested and which is not.
To specify a specific version of Jacoco, simply add a Jacoco configuration block to BuildType, defining the version:
ToolVersion = "0.7.1.201405082137"}Copy the code
conclusion
In this chapter, we’ve learned several ways to test Android applications and libraries. Started with simple unit tests, then learned more about Android-related tests, as well as the third party Robolectric testing framework. Functional testing and using Espresso were introduced. Finally, how to implement test coverage report is studied. Now we know how to run the entire test suite using Gradle and Android Studio, and we can generate coverage reports.
We’ll also learn how to automate testing using continuous integration tools in Chapter 8, “Setting up continuous Integration.”
The next chapter covers one of the most important aspects of the custom build process: creating custom tasks and plug-ins. It includes a brief introduction to Groovy. Learning the basics of the Groovy language will not only help you create tasks and plug-ins, but also make it easier to understand how Gradle works.