Why Write unit Tests
The first step is to introduce the practice of unit testing for the financial side of Mushroom Street payment. It's a bit of a coincidence, just at the beginning, I would write unit tests alone. Then the bosses knew and thought it was a valuable thing, so they called me in charge of the unit test for our group. So slowly, unit testing has become a normal practice on our side. Later, at the company level also began to have a certain promotion.
To say why you write unit tests, I believe most people can admit and understand the role of unit testing in ensuring code quality, preventing bugs or finding bugs early, which may be the most important part of unit testing. However, I think that in addition to this role, unit testing can greatly improve the design of the code, but also save time, so that people can work more confident, happier, and other benefits. These are my personal feelings, and I believe that most of the people who have really practiced unit testing have a real feeling, not a nice big talk about this stuff.
When it comes to saving time, you might be curious, it takes time to write unit tests, it takes time to maintain unit test code, and it should be more time-consuming.
This is what I want to clarify before I start sharing, that is, unit testing itself does not take much time, on the contrary, it will save time. It's just that:
Learning how to do unit tests takes time;
Adding unit tests to a project that does not have a unit test requires a certain amount of structural adjustment, because there is a large difference in structure between a unit test and a project without unit tests.
For example, does it take a lot of time to drive this thing? I believe very few people will say that driving this thing takes a lot of time, but instead:
Learning to drive requires a certain amount of time;
If the road is uneven, it will take some time for the road to be repaired. Unit testing is a similar scenario.
So why is unit testing saving time? A few simple points to say:
Without unit testing, you can only run the app to test it, which is much slower than running a unit test at a time.
Early detection of bugs reduces debug and fixbug time.
Refactoring, significantly reducing the time to manually verify the correctness of the refactoring.
Therefore, I hope that we can remove the "No time to write unit test" This impression, if the work is too tight, there is no time to learn how to do unit testing, you can learn privately, and then slowly applied to the project.
Unit Test Brief introduction
Next, we'll show you how to do the Android unit test on our side. First clarify the concept, write tests on Android, there are many technical solutions. There are junit, instrumentation test, Espresso, Uiautomator and so on, there are third-party appium, Robotium, Calabash and so on. We're talking about using JUnit and some other frameworks to write unit tests that can run directly on the JVM in our development environment, other than unit tests, but integration testing or functional test, and so on. The obvious difference between the two is that the former can be run directly on the development computer, or the JVM above the CI, and can run only a fraction of the code, very quickly. The latter must have a simulator or a real machine, package the entire project into an app, then upload it to the emulator or the real machine, and then run the relevant code, which is relatively slow.
Definition of unit tests we all know that the test code is written for one of the code units (such as a method) that we write. A unit test can probably be divided into three parts:
Setup: The new class to be tested, set some prerequisites
Execute action: Call the measured method of the tested class and get the return result
Validation result: Verify that the obtained result is the same as the expected result
However, the method of a class is divided into two kinds, one is the method with the return value. One is a method that has no return value, that is, the void method. For a method that has a return value, it is easy to test, but how to test for a method that has no return value? The key here is, how to get the "return result" of this method?
Here is an example to clarify a very common misunderstanding. For example, there is an activity, called dataactivity, which has a public void LoadData () method that invokes the underlying Datamodel class and asynchronously executes some network requests. Updates the user interface when a network request returns.
The LoadData () method here is void, how should it be tested? One of the most straightforward reactions might be to call the LoadData () method (which, of course, might actually be triggered by other events), and then after a while, the validation interface is updated. This approach is wrong, however, and this test is called integration testing, not unit testing. Because it involves a lot of aspects, it involves datamodel, the network server, and when the network returns correctly, dataactivity internal processing, and so on. Integration testing is certainly a necessity, but not the place where we should be most concerned, nor the most valuable place. The most we should focus on IS unit testing. On this, there is a theory of test pyramid:
Test pyramid theory is basically that unit testing is the basis for what we should spend most of our time writing, and integration testing should be a small part of what the iceberg can see.
So for this case, the correct unit test method should be to verify that the LoadData () method invokes a Datamodel request data method, and that the parameters passed are correct. "The Datamodel method is called, and the parameter is ... "This is the" return result "of this method of LoadData ().
The concept of mock and the Mockito framework
To verify that a method of an object has been called, it involves the use of a mock. Here is a simple introduction to the concept of mock, lest many classmates unfamiliar, mock is to create a false, simulated object. In the test environment, it is used to replace the real object. This can be achieved for two purposes:
You can specify at any time what value a method of a mock object returns, or what action to perform.
You can verify that a method of a mock object has not been called, or how many times it was called, what the parameter is, and so on.
To use a mock, you typically use a mock frame, which currently has two, Mockito, and jmockit that are most commonly used by Android. The difference between the two is that the former cannot mock static method and final class, Final method, the latter can. We are still using the Mockito, the reason is ashamed, because at first did not know Jmockit this thing, later looked up some information, read a lot of contrast mockito and Jmockit articles, seemingly most still very optimistic about Jmockit, but there is a problem, That's the combination with Robolectric there are some bugs, while the use of posture and Mockito have a larger difference, so has not been time to practice. This hope will be able to do a further investigation, the time to share with you the use of feelings.
But using Mockito, there is a problem, that is, the static method and the final class, the final method is no way to mock, to this point how to solve, we will introduce later.
Using mocks in a test environment: Dependency Injection
The next question is, how do you change Datamodel to mock objects in a test environment, and Datamodel is a normal object in formal code?
This problem also has two solutions, one is to use specialized testing product flavor, and the other is to use dependency injection. The first solution is to use a specialized product flavor to do testing, in this testing flavor, inside the class that needs to mock a mock implementation, and then through the factory to provide to the client, This factory interface is the same in testing flavor and the formal flavor, in the run testing, dedicated to use this testing flavor, so through the factory to get is the mock class. This looks very simple, but it is very inflexible, because there is only one kind of mock implementation, in addition, the code will become very ugly, because you need to provide each dependency a factory, will feel very deliberate; Moreover, one more flavor, Many gradle tasks will become very slow. For this scenario, you can refer to this video (Https://www.youtube.com/watch?v=vdasFFfXKOY).
Therefore, we use the second type, which is dependency injection. A brief introduction to the dependency injection model, his basic idea is that a certain class (for example, dataactivity), the use of internal objects (such as Datamodel) of the creation process is not inside the dataactivity to new, Instead, an instance of Datamodel is created externally, and then set to dataactivity in some way. This pattern application is very extensive, especially at the time of testing. To make dependency injection more convenient, there are many frameworks that specialize in doing this, such as Roboguice, Dagger, Dagger2, and so on. We use Dagger2 for a simple reason, which is the best di framework for the time being.
The article about Dagger2, before our group has also shared a lot of, but it seems I did not see the story about how to use the Dagger2 in the test environment of the article, this is slightly regrettable. Leaving unit tests, using dependency injection is a compelling reason.
So I'll introduce you here, how to apply Dagger2 to unit tests. Familiar with the dagger2 boots may know, Dagger2 inside the most critical there are two concepts,Module and Component. Module is responsible for generating classes such as Datamodel that are used by others (such as dataactivity). In terms of terminology, the class Datamodel used by others is called dependency, and the class dataactivity called the client is used in other classes. The component is a unified interface for client to use dependency. In other words, dataactivity through component, to get a copy of the Datamodel instance.
Now, the key place to come, component itself is not to produce dependency, it is just a porter, the real production dependency place in the module. Therefore, the creation of component need to use the module, different module production of different dependency. In the official code, we use the normal module to produce the normal Datamodel. In the test environment, we write a testingmodule, let it inherit the normal module, and then override the production Datamodel method, let it produce mock datamodel. When running unit tests, use this testingmodule to create component, so that the Datamodel object dataactivity through component is a mock Datamodel object.
In this way, all production codes do not have to add any extra code specifically for testing, but also have other benefits of dependency injection.
Robolectric: Solving the biggest pain point in Android unit testing
Next, the biggest pain point for Android unit testing is that the JVM does not use Android-related classes when running pure JUnit unit tests, because the Android environment we have developed is not implemented, only some interfaces are defined, and all methods are implemented as throw new RuntimeException ("stub"); If we use the code inside the unit test code for Android, then the runtime will encounter RuntimeException ("stub").
To solve this problem, there are generally three options:
Use the instrumentation system provided by Android to run the unit test code on the emulator or the real machine.
With a certain architecture, such as MVP and so on, the Android-related code is separated, the middle of the presenter or model is in Java implementation, can be tested on the JVM. View or other Android-related code is unexpected.
Using the Robolectric framework, this framework can basically be understood to implement a set of Android simulation environment on the JVM, while adding some other enhancements to the Android-related classes to facilitate unit testing, using this framework, we can run unit tests on the JVM, You can use an Android-related class.
The first scenario works, but it's very slow, because every time you run a unit test, you need to package the whole project into an apk, upload it to the emulator or the real machine, and run an app like this, which is obviously not the speed of the unit test, or TDD. Such a scheme was first rejected.
At first, we used Robolectric for two reasons:
Our project did not have a clear structure at the time, Android and pure Java code isolation did not do well;
A lot of Android-related code, still need to test, such as Custom view and so on.
However, slowly, our attitude from embracing robolectric, to try not to use it, as far as possible using pure Java code to achieve. You may think that Android-related code will be many, and pure Java is very few, but slowly you will find that, in fact, is not so, pure Java code is actually quite a lot, and often is the core of the logic. As far as possible without robolectric, because robolectric although compared to instrumentation testing is much faster. But after all, he also needs to merge some resources, build out a simulated app, so the speed is still very slow relative to pure Java and junit.
Use specific figures to compare the instructions:
Run instrumentation testing: A few 10 seconds, depending on the size of the app
Robolectric:10 seconds or so
JUnit: Within seconds
Of course, although running the robolectric in about 10 seconds, but the comparison of running an app, or too much faster. So, at first, it was OK to start from robolectric.
These are some of the basic techniques that we've used for unit testing here: JUNIT4 + Mockito + Dagger2 + robolectric. Basically, there is no black technology, it is the industry standard.
A specific case
Next, I passed a specific case, to introduce you, our side of an app, specifically how to test.
Here's what our checkout interface looks like:
Assuming the activity name is checkoutactivity, when it starts, Checkoutactivity will tune a Checkoutmodel Loadcheckoutdata () method, This method will also go to the bottom of a package of user authentication and other information of the network Request API Class (MAPI) Get method, and passed to the API Class A callback. The callback's job is to post the results via Otto Bus (MBus). The checkoutactivity inside Subscribe the event (the method name is oncheckoutdataloaded ()) and then displays the data or error message according to the value of the event.
The code is abbreviated as follows:
Here, checkoutactivity inside the Mcheckoutmodel, Checkoutmodel inside of the MAPI, Checkoutmodel inside the MBus, are injected through the Dagger2. These are mock-up when doing unit tests.
For this process, we have done the following unit tests:
Checkoutactivity Start Unit Test: Start an activity with the method provided by Robolectric. Verify that the Loadcheckoutdata () method inside the Mcheckoutmodel has been called, and that the parameters (order ID, etc.) are right.
Checkoutmodel loadcheckoutdata Unit Test 1: Call Checkoutmodel's Loadcheckoutdata () method, verify that the MAPI corresponding get method is called, and that the parameters are right.
The Loadcheckoutdata unit test 2:mock API class for Checkoutmodel specifies that when its get method calls the incoming callback, the Onsuccess method is called directly, Then call Checkoutmodel's Loadcheckoutdata () method to verify that the post method of the Otto bus has been called, and that the parameters are right.
The Loadcheckoutdata unit test 3:mock API class for Checkoutmodel specifies that when its get method calls the incoming callback, the OnFailure method is called directly, Then call Checkoutmodel's Loadcheckoutdata () method to verify that the post method of the Otto bus has been called, and that the parameters are right.
checkoutactivity oncheckoutdataloaded Unit Test 1: Start a checkoutactivity, call his oncheckoutdataloaded (), and pass in an event containing the correct data, Verify that the corresponding data view is displayed.
checkoutactivity oncheckoutdataloaded Unit Test 2: Start a checkoutactivity, call his oncheckoutdataloaded (), and pass in an event containing an error message, Verify that the appropriate error message view is displayed.
One thing to note here is that each of the above tests is done independently, not that the following unit tests are dependent on the above. Or you have to do the above first, and then do the following.
This part of the more detailed code is on GitHub (https://github.com/ChrisZou/android-unit-testing-tutorial), groupshare the package.
The other questions
These are the techniques we use to do unit testing, and a basic process, and let's talk about a few other issues.
What needs to be tested?
Public methods for all the model, Presenter/viewmodel, API, Utils, and other classes
The data class in addition to the general auto-generated methods such as getter, Setter, toString, hashcode, etc.
Custom View features: such as set data after the text is not displayed and so on, simple interaction, such as the Click event, responsible for the interaction of general contingency, such as touch, sliding events and so on.
Activity's main functions: such as whether the view is present, displaying data, error messages, simple click events, etc. More complex user interactions such as Ontouch, and view style, location, etc. can be unexpected. Because it's hard to measure.
CI and code Coverage:jacoco
To formalize the unit test, CI is a very important step, we have a CI server running Jenkins, each time the developer push code to master branch, will run a unit test Gradle task, while using Jacoco to do code Coverage
There's a pit to pay special attention to, and that's the compatibility of the Gradle Jacoco plugin and Jenkins Jacoco plugin in the project. We used the Gradle Jacoco plugin is 7.1, the higher version seems to have a problem. Then the corresponding Jenkins Jacoco plug-in needs to be 1.0.19 or lower, and a later version of Jenkins Plugin does not support the low version of the Gradle Jacoco project version. In fact, this is stated on the home page of Jenkins ' Jacoco plugin:
(Click to enlarge image)
But I didn't pay attention at that time, so the coverage data has not come out, toss for a while, and finally in the help of colleagues to find the problem.
Encountered the pits, and the good practice suggested
Let's talk about some of the pits we met and some good practice suggestions.
1. Native libary
Both pure junit and robolectric do not support the load native library, which will report Unsatisfiedlinkerror's fault. So if you use native Lib in your code, you may need to add a try catch to system.loadlibrary.
If the code is used in the third-party LIB, and the use of native Lib, there are generally two solutions, one is to use the third party native Lib outside their own layer, and then in the case of testing mock off. The second is to create a shadow class for that class with Robolectric.
The advantage of the first approach is that you can change the return value or behavior of the class at any time while testing, and the disadvantage is that you need to create another wrapper class, which is a bit cumbersome. The second method cannot change the behavior of this class at any time, but it is very simple to write. So, look at your own needs, choose the appropriate method.
These two methods are also the main way to solve the static method, final Class/method cannot mock.
2. Try to write code that is easy to test
The static method, the direct new object, the Singleton, the Global state, and so on are some of the code methods that are not conducive to testing, and should be avoided and replaced by dependency injection.
3. Do not repeat your unit test
For example, you use a builder mode to create a class, and the builder has a validator to validate some of the parameters. In this case, the builder and validator separate test, with a variety of correct error parameters for testing validator, and then the builder, you do not have to traverse a variety of valid and invalid parameters to test.
Because if this is the case, then the logic of the validator change, then the test for validator and for the builder to modify the test, this is actually repeated. Just test this builder with a validator in it.
4. Public Unit Test Library
If your company is also a component development, pull out a common unit test class library to do unit testing, which can put some public helper, utils, rules and so on, which can greatly improve the speed of writing unit tests.
5. Copy the "Plain Java" code from Android to your project
There are some classes in Android that are not much related to Android, such as textutils, color and so on, these classes can be copied out of the code, put it in their own projects, and then other places to use this class, this can also partially get rid of Android dependencies, Use JUnit instead of robolectric to increase the speed at which test is run.
6. Giving full play to the role of JUnit rule
JUnit rule is a powerful tool, but few people know it. Its basic role is to allow you to do something before and after you execute a test method. If you have a lot of common setup, teardown work in several test classes, you may prefer to use inheritance, combined with @before, @After to reduce duplication, it is recommended to use JUnit rule to achieve this purpose, Instead of inheriting, this allows for greater flexibility.
For example, to facilitate testing of the activity method, we have a activityrule that runs a test method that starts target activity and then finishes the activity automatically after running.
One of the more interesting features of the JUnit rule implementation is the implementation of a naming scheme similar to the BDD test framework. When doing unit tests, you often need to write several test methods for the same method, and each test method tests different points. In order to make the naming more readable, we tend to write the names very long, in this case, if the name of the hump, you need to constantly switch the case, writing trouble, readability is not high. If you underline it, it will be cumbersome to write. If you have used some of the BDD frameworks (such as RSpec, Cucumber, jasmine, etc.), you will miss the "naming" approach. If you don't use it, the "naming" approach is probably like this:
The key here is that when the test method fails, the string is to be added to the error message. We made a junit rule to achieve this effect. The practice is to combine a custom annotation, which annotation receives a string that describes the test intent of this test method. Read this annotation in rule, and if the test does not pass, add the descriptive string to the error message in the output. This way, when running in batches, it is known that the tests that have not passed are measured. The name of the test method can be more arbitrary.
The following results are achieved:
If the run fails, you get the following result
For the use of JUnit rule, it is not difficult for you to Google yourself.
7. Be good at using Androidstudio to speed up your writing test
Androidstudio has a lot of feature that can help us write code faster, like codes generation and live template. This is also true for writing formal code, but for writing test code, the effect is even more pronounced. Because most of the test code is similar in structure and style, live template can play a very big role here. In addition, if you write the test first, you can write some of the non-existent class or method, and then Alt+enter let Androidstudio automatically help you build.
8. Don't pursue perfection
At first, there is no need to pursue the quality of the test code, nor the pursuit of perfection, if some places are not good to write tests, you can put it, and then to make up, there is part of the test is better than no test. Martin Fowler said
Imperfect tests, run frequently, is much better than perfect tests that is never written at all.
However, when you are familiar with the method of writing tests, it is highly recommended to write tests first! Because if you write the official code first, you already have an impression of how the code is working, so you tend to write tests that pass smoothly, ignoring situations where the test doesn't pass. If you write the test first, you can consider it more comprehensively.
9. Future plans
Using groovy and Robospock, or Kotlin and Spek, to really implement BDD is a very likely thing, but we don't have a lot of practice on our side at the moment, so we don't say too much. After a certain practice, then we can communicate more.
Part of the article code: https://github.com/ChrisZou/android-unit-testing-tutorial
Original source: http://www.infoq.com/cn/articles/mogujie-android-unit-testing
Android Unit Test Practice