series
- Introduction to Android Test
- How to Test ViewModel and LiveData
- How to Test Doubles as a Repository
preface
❝
Development that cannot test is not good development — Lu Xun
❞
For a long time, there are few content resources about how to write test code. I saw the video of this part in Youda University before, but it may not be friendly to some friends because there is no Chinese subtitles. So I decided to organize this into a series of articles, starting with getting to know Test
This article is from Udacity Advanced Android with Kotlin-Lesson 10-5.1 Testing:Basics
structure
As Android developers we know there are three pieces of code in Android Studio’s Android view
- App logic code (main source set)
- AndroidTest code
- The local test code
The test code knows all the code in the Main Source set, so it can test these classes. But the app code doesn’t know about the code in Test, and neither androidTest nor Test knows about the other. In fact, when you build an APK and submit it to the app market, the test code is not included
Rely on the reference
The following tag rely on using the reference of the test methods testImplementation and androidTestImplementation
❝
“Note: The test code is not packaged into the final APK file“
❞
The JUnit dependency referenced by testImplementation can only be used in the test source set. Gradle implements this dependency limitation
To summarize briefly:
- Three kinds of“source sets“:
main
.test
.androidTest
- The test code can access the APP code
- The app code “cannot” access the test code
- Tests are not driven into Apk
- The scope of dependence includes:
testImplementation
和androidTestImplementation
Run the first test
We open the Test source set and see that there is an ExampleUnitTest
You can see that there is only one addition_isCorrect() method inside
There are two elements that make it a test:
- Using the
@Test
annotations - It exists in one of the two Test source sets
With these two elements, the method can be run independently as test
This example test is on line 15. It’s called an assertion
Assertions are at the heart of Test, which checks whether your code or app behaves as you expect
In this example, the assertion checks whether 4 is equal to 2 + 2
By convention, you need to pass your “expected result” into the Expected parameter and your “actual result” into the actual parameter
The @test annotation and assertion statements are under JUnit
For more detailed information on JUnit, please refer to the official documentation
Let’s start running the test by right-clicking the method and clicking Run
Immediately after that, the Run window appears
You can see that the window displays information about the test, showing whether and how many tests passed
Let’s try a case where test does not pass and add an assertion, as shown below
This time we’ll Run Test by clicking the green button in the Run window
We can see that even if only one assertion fails, the entire test fails
The window indicates that the expected result is 5, but the actual result is 4, and that the error occurred at line 15 in the tag below, so you can see that this is indeed a bug
With the bug fixed, let’s run test again. This time we’re going to do it in a different way
Here are some other ways to run Test
You can right-click the class name to select the Run option
You can also right-click the Test Source Set in the left view and select the Run button, which will Run all tests. At the top, you can toggle the test you want to run by clicking the green button. You can also switch back to app
androidTest VS test
Let’s compare androidTest with Test
test | androidTest |
---|---|
Local Tests | Instrumented Tests |
Local machine JVM | Real or emulated devices |
Faster | Slower |
So let’s run an androidTest, and you can see the emulator starts up
Write a test
First we for a function to create a test, as is shown above, there is a getForkAndOriginRepoStats () method is used to get the fork warehouse and raw data and return StatsResult, The first parameter of StatsResult is the percentage of the fork item and the second parameter is the percentage of the original item. We call Generate, check the Test option, select JUnit4 in the pop-up box, click OK and select Store in Local Test. So we create a test
Next we write test. You can see that the automatically created test path corresponds to the package name of the code path in app Code. We first test that the list of items has only one item and no forks, and then calculate the percentage of fork items and the percentage of the original items. In theory, the percentage of fork items is 0 and the percentage of original items is 100%. The code is shown below, and we click to run after we write it
You can see that the test passed
This is a normal flow, but we also need to test the flow for exceptions, such as repos being empty List or the repos variable itself being NULL
It can be seen that our code does not judge whether the list is empty or null, so the null pointer is caused. After we modify the code, we can pass the test
In fact, our coding process above is called “Test Driven Development(TDD).” For more information about TDD, Test-driven Development on Android with the Android Testing Support Library (Google I/O ’17)
Make your test more readable
As with normal code, you need to make your Test code more readable, and you can do so in three directions
- Good naming
- Given/When/Then
- With the assertion library
Good naming
First, let’s talk about naming. We know that the test method uses the @test annotation tag. In theory, method names can be arbitrarily named
“Test module _ action or input _ result status“
For example the example above we named: getForkAndOriginRepoStats_noForked_returnHundredZero
The first part shows that we want to test is getForkAndOriginRepoStats () method, the second part represents what we need is not the fork warehouse data source, the third part is the result of state, 0%
Given/When/Then
With naming out of the way, let’s talk about Given/When/Then
“The basic structure of the test is Given X, When Y, Then Z“
Again, the example above
- Given provides the data source for your test logic
- When is your actual operation
- Then Checks whether the test passes
With the assertion library
The assertion code at the end of the example above looks awkward, but we can make this part more readable with the help of the assertion library
/ / before
assertEquals(result.forkPercent, 0f)
/ / after
assertThat(result.forkPercent, `is` (0f))
Copy the code
The following statement, like a human sentence, translates to the assertion that forkPercent is 0F
This notation requires the introduction of a library Hamcrest
testImplementation "Org. Hamcrest: hamcrest - all: 1.3"
Copy the code
❝
“Note: Due to
is
Is a keyword in Kotlin, so escape with ‘is’“❞
A common assertion library
- Hamcrest
- Truth Library
Test range
Test scope refers to how much code a test tests
For example, automated tests can be divided into two types according to the test scope
- Unit Tests
- Integration Tests
- End to End Tests
Your test strategy needs to cover all types
Unit Tests
We have already written Unit Tests for the above example
- A scope is a single method or class
- Help identify the cause of the failure
- Should run fast, usually local tests
- Low-fidelity degrees
Their scope is a single method or class
If The Unit Tests fail, you know where your code went wrong. Because it focuses on a very small piece of code
Unit Tests are also meant to run fast, requiring speed because you change the code so frequently that it runs frequently. Unit Tests are usually local Tests
They have low fidelity because in the real world your app executes a lot of code, not just a method or class
Unit Tests are like checking that each link in a chain works properly
But it does not check that the combination of these steps works, and for that you need Integration Tests
Integration Tests
Integration Tests have much greater scope
- Scope can be several classes or a single function
- Make sure several classes run together
- You can use local tests or machine tests
Just like the word Integration, Integration Tests integrate classes to make sure they behave as expected when combined
Integration Tests are built to have them test individual functions, like getting a Github repository for a given user
Integration Tests have a larger scope than Unit Tests, but they still run fast and have good fidelity
Decide whether to use local or machine Tests depending on the situation. For example, if you write Integration Tests that involve UI components, then you need to use the real machine
End to end Tests
The third type is End to End Tests, which run a list of functions together
- Scope is most of the app
- High fidelity
- Test the app as a whole
- For realistic use, equipment testing should be used
End to End Tests Tests most of the app, which is very close to real-world use and therefore slow
It has the highest fidelity and ensures that your application works as a whole
These tests should use device tests
The proportion of test
The recommended ratio of testing is 70% unit testing, 20% integration testing, and 10% end-to-end testing
“Whether you can easily test your app in various parts depends on the structure your app uses“
For example, if your application places all the logic in one activity’s large method, you might be able to write end-to-end tests, but not unit and integration tests
A better architecture would split the application logic into multiple methods and classes, allowing each part to be tested independently
For unit tests, you can test ViewModel, Repository, and DAO
For integration testing, you can test a combination of fragments and ViewModel, or you can test the entire database code
End-to-end testing tests the entire application
Please refer to the official documentation for the principles of testing
The test of the codelab
About me
I am a Fly_with24
-
The Denver nuggets
-
Jane’s book
-
Github