[[175275]] Original link: http://www.jianshu.com/p/77ee7c0270bc Preface Have readers noticed that I like to have a preface or foreword when I write an article? The truth is, half of it is to show off and make up the word count, and half of it is because I don’t know what to write next, so I want to chat for a while to calm down. ^_^ Hahaha... I still have to say what I should say. The previous article "Android Unit Testing - How to Test Sqlite, SharedPreference, Assets, and File Operations?" talked about some details of DAO (Data Access Object) unit testing. This article explains parameter validation. Verifying parameter passing and function return values is a very important part of unit testing. I believe many readers have verified parameters, but is your unit test code really correct? I encountered some problems in my early practice and accumulated some experience, which I would like to share with you in this issue. 1. General form Bean - public class Bean {
- int id;
- String name ;
-
- public Bean( int id, String name ) {
- this.id = id;
- this.name = name ;
- }
- // getter and setter
- ......
- }
DAO - public class DAO {
- public Bean get( int id) {
- return new Bean(id, "bean_" + id);
- }
- }
Presenter - public class Presenter {
-
- DAO dao;
-
- public Presenter(DAO dao) {
- this.dao = dao;
- }
-
- public Bean getBean( int id) {
- Bean bean = dao.get(id);
-
- return bean;
- }
- }
Unit test PresenterTest (hereinafter referred to as "Example 1") - public class PresenterTest {
-
- DAO dao;
- Presenter presenter;
-
- @Before
- public void setUp() throws Exception {
- dao = mock(DAO.class);
- presenter = new Presenter(dao);
- }
-
- @Test
- public void testGetBean() throws Exception {
- Bean bean = new Bean(1, "bean_1" );
-
- when (dao.get(1)).thenReturn(bean);
-
- Bean result = presenter.getBean(1);
-
- Assert.assertEquals(result.getId(), 1);
- Assert.assertEquals(result.getName(), "bean_1" );
- }
- }
This unit test passes. 2. Problem: Many variables in the object The Bean above has only two parameters, but in actual projects, objects often have many parameters, for example, user information User: - public class User {
- int id;
- String name ;
-
- String country;
- String province;
- String city;
- String address;
- int zipCode;
-
- long birthday;
-
- double height;
- double width;
-
- ...
- }
Unit Tests: - @Test
- public void testUser() throws Exception {
- User user = new User (1, "bean_1" );
- user .setCountry( "中国" );
- user .setProvince( "Guangdong" );
- user .setCity( "Guangzhou" );
- user .setAddress( "Haixinsand Park, Linjiang Avenue, Tianhe District" );
- user .setZipCode(510000);
- user .setBirthday(631123200);
- user .setHeight(173);
- user .setWeight(55);
- user .setXX(...);
-
- .....
-
- User result = presenter.getUser(1);
-
- Assert.assertEquals(result.getId(), 1);
- Assert.assertEquals(result.getName(), "bean_1" );
- Assert.assertEquals(result.getCountry(), "China" );
- Assert.assertEquals(result.getProvince(), "Guangdong" );
- Assert.assertEquals(result.getCity(), "Guangzhou" );
- Assert.assertEquals(result.getAddress(), "Haixinsand Park, Linjiang Avenue, Tianhe District" );
- Assert.assertEquals(result.getZipCode(), 510000);
- Assert.assertEquals(result.getBirthday(), 631123200);
- Assert.assertEquals(result.getHeight(), 173);
- Assert.assertEquals(result.getWeigth(), 55);
- Assert.assertEquals(result.getXX(), ...);
- ......
- }
In a general unit test, if there are 10 parameters, you need to set() 10 times and get() 10 times. If there are more parameters, a project will have dozens or even hundreds of such tests... Do you feel the pain in your balls? There are two pain points here: - The generated object must call all setter() to assign member variables
- When verifying the return value or callback parameter, all getter() must be called to obtain member values
3. Is it possible to compare objects using equals()? Calling equals() directly At this time, student A raised his hand and said, "Isn't it just comparing objects? Can't we use equal()?" For the convenience of demonstration, let's use Bean as an example: - @Test
- public void testGetBean() throws Exception {
- Bean bean = new Bean(1, "bean_1" );
-
- when (dao.get(1)).thenReturn(bean);
-
- Bean result = presenter.getBean(1);
-
- Assert.assertTrue(result.equals(bean));
- }
Run it: Hey, it actually passed! The first problem has been solved. Applause... Wait a minute, let's modify the Presenter code to see if it still works: - public class Presenter {
-
- public Bean getBean( int id) {
- Bean bean = dao.get(id);
-
- return new Bean(bean.getId(), bean.getName());
- }
- }
Run the unit test again: Something went wrong! Let's analyze the problem. Before the modification, the Presenter.getBean() method used the Bean object obtained by dao.get() as the return value directly, so Assert.assertTrue(result.equals(bean)); in PresenterTest passed the test because bean and result are the same object. After the modification, in Presenter.getBean(), the return value is a deep copy of the Bean obtained by dao.get(), and bean and result are different objects, so result.equals(bean)==false, and the test failed. If we use the general form Assert.assertEquals(result.getXX(), ...);, the unit test passes. Whether returning the object directly or making a deep copy, as long as the parameters are the same, the expected result will be achieved. Therefore, simply calling equals() will not solve the problem. Override the equals() method Student B: "Since we are only comparing member values, rewrite equals()!" - public class Bean {
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof Bean) {
- Bean bean = (Bean) obj;
-
- boolean isEquals = false ;
-
- if (isEquals) {
- isEquals = id == bean.getId();
- }
-
- if (isEquals) {
- isEquals = ( name == null && bean.getName() == null ) || ( name != null && name .equals(bean.getName()));
- }
-
- return isEquals;
- }
-
- return false ;
- }
- }
Run the unit test again Assert.assertTrue(result.equals(bean));: Wait a minute, aren't we back to the old ways, where every Java bean has to override equals()? Although the overall code will be reduced throughout the project, this is really not a good idea. Reflective comparison of member values Student C: "We can use reflection to get all member values of two objects and compare them one by one." Hahahaha, classmate C is smarter than classmates A and B, and he also has reflexes! - public class PresenterTest{
- @Test
- public void testGetBean() throws Exception {
- ...
- ObjectHelper.assertEquals(bean, result);
- }
- }
- public class ObjectHelper {
-
- public static boolean assertEquals(Object expect, Object actual) throws IllegalAccessException {
- if (expect == actual) {
- return true ;
- }
-
- if (expect == null && actual != null || expect != null && actual == null ) {
- return false ;
- }
-
- if (expect != null ) {
- Class clazz = expect.getClass();
-
- while (!(clazz.equals(Object.class))) {
- Field[] fields = clazz.getDeclaredFields();
-
- for (Field field : fields) {
- field.setAccessible( true );
-
- Object value0 = field.get(expect);
- Object value1 = field.get(actual);
-
- Assert.assertEquals(value0, value1);
- }
-
- clazz = clazz.getSuperclass();
- }
- }
-
- return true ;
- }
- }
Run the unit test, it passes! The idea of using reflection to directly compare member values is correct. This solves the problem of "comparing whether the member values of two objects are the same without getting() n times". However, this unit test still has problems just comparing two objects. Let's talk about Section 4 first, and leave this problem to Section 5. 4. Omit unnecessary setter() In testUser(), the first pain point is: "When generating an object, all setter() must be called to assign member variables." In the previous section, student C used reflection to get the object member values and compare them one by one. This solution reminds us that the same solution can be used for assignment. ObjectHelper: - public class ObjectHelper {
-
- protected static final List numberTypes = Arrays.asList( int .class, long.class, double .class, float .class, boolean.class);
-
- public static <T> T random(Class<T> clazz) throws IllegalAccessException, InstantiationException {
- try {
- T obj = newInstance(clazz);
-
- Class tClass = clazz;
-
- while (!tClass.equals(Object.class)) {
-
- Field[] fields = tClass.getDeclaredFields();
-
- for (Field field : fields) {
- field.setAccessible( true );
-
- Class type = field.getType();
- int modifiers = field.getModifiers();
-
- // final not assigned
- if (Modifier.isFinal(modifiers)) {
- continue ;
- }
-
- // Randomly generate values
- if (type.equals( Integer .class) || type.equals( int .class)) {
- field.set (obj, new Random().nextInt(9999));
- } else if (type.equals(Long.class) || type.equals(long.class)) {
- field.set (obj, new Random().nextLong());
- } else if (type.equals( Double .class) || type.equals( double .class)) {
- field.set (obj, new Random().nextDouble());
- } else if (type.equals( Float .class) || type.equals( float .class)) {
- field.set (obj, new Random().nextFloat());
- } else if (type.equals(Boolean.class) || type.equals(boolean.class)) {
- field.set (obj, new Random().nextBoolean());
- } else if (CharSequence.class.isAssignableFrom(type)) {
- String name = field.getName();
- field.set (obj, name + "_" + ( int ) (Math.random() * 1000));
- }
- }
- tClass = tClass.getSuperclass();
- }
- return obj;
- } catch (Exception e) {
- e.printStackTrace();
- }
- return null ;
- }
-
- protected static <T> T newInstance(Class<T> clazz) throws IllegalAccessException, InvocationTargetException, InstantiationException {
-
- Constructor constructor = clazz.getConstructors()[0]; // The constructor may have multiple parameters
-
- Class[] types = constructor.getParameterTypes();
-
- List<Object> params = new ArrayList<>();
-
- for (Class type : types) {
- if (Number.class.isAssignableFrom(type) || numberTypes. contains (type)) {
- params.add (0);
- } else {
- params.add ( null );
- }
- }
-
- T obj = (T) constructor.newInstance(params.toArray());//clazz.newInstance();
-
- return obj;
- }
- }
Write a unit test to generate and randomly assign values to a Bean and output all member values of the Bean: - @Test
- public void testNewBean() throws Exception {
- Bean bean = ObjectHelpter.random(Bean.class);
-
- // Output bean
- System. out .println(bean.toString()); // rewrite toString() yourself
- }
Run the tests: - Bean {id: 5505, name : "name_145" }
Modifying unit tests Unit test PresenterTest: - public class PresenterTest {
- @Test
- public void testUser() throws Exception {
- User expect = ObjectHelper.random( User .class);
-
- when (dao.getUser(1)).thenReturn(expect);
-
- User actual = presenter.getUser(1);
-
- ObjectHelper.assertEquals(expect, actual);
- }
- }
There are a lot less codes, isn’t it great? Run it, through: 5. Comparison object bug The solution mentioned by the author above has a problem. See the following code: Presenter: - public class Presenter {
-
- DAO dao;
-
- public Bean getBean( int id) {
- Bean bean = dao.get(id);
-
- // Temporarily modify the bean value
- bean.setName( "I'm here to make trouble" );
-
- return new Bean(bean.getId(), bean.getName());
- }
- }
- @Test
- public void testGetBean() throws Exception {
- Bean expect = random(Bean.class);
-
- System. out .println( "expect: " + expect); // Output expect in advance
-
- when (dao.get(1)).thenReturn(expect);
-
- Bean actual = presenter.getBean(1);
-
- System.out.println ( "actual: " + actual) ; // Output result
-
- ObjectHelper.assertEquals(expect, actual);
- }
Run the modified unit test: - Pass
- expect: Bean {id=3282, name = 'name_954' }
- actual: Bean {id=3282, name = 'I'm here to make trouble' }
It actually passed! (Not in line with the expected result) What happened? The author analyzes it for you: we hope that the returned result is Bean{id=3282, name='name_954'}, but the return object Bean specified by the mock in the Presenter is modified, and the returned Bean deep copy object and the variable name are also changed; when running the unit test, the member values of the two objects are compared at the end, and the names of the two objects are modified, which causes equals() to think it is correct. Here's the problem: The member value of the mock specified return object is tampered in the Presenter The simplest solution: Before calling the Presenter method, take out the member parameters of the mock return object in advance and compare them at the end of the unit test. Modify the unit test: - @Test
- public void testGetBean() throws Exception {
- Bean expect = random(Bean.class);
- int id = expect.getId();
- String name = expect.getName();
-
- when (dao.get(1)).thenReturn(expect);
-
- Bean actual = presenter.getBean(1);
-
- // ObjectHelper.assertEquals(expect, actual);
-
- Assert.assertEquals(id, actual.getId());
- Assert.assertEquals( name , actual.getName());
- }
Run, the test fails (in line with expected results): - org.junit.ComparisonFailure:
- Expected: name_825
- Actual: I'm here to make trouble
It meets our expectations (test failed)! Wait... Isn't this going back to the old ways? When there are many member variables, won't it be too much to write? All the previous words are in vain? Next, we enter the climax of this article. 6. Solution 1: Deep copy the expect object in advance - public class ObjectHelpter {
- public static <T> T copy(T source) throws IllegalAccessException, InstantiationException, InvocationTargetException {
- Class<T> clazz = (Class<T>) source.getClass();
-
- T obj = newInstance(clazz);
-
- Class tClass = clazz;
-
- while (!tClass.equals(Object.class)) {
-
- Field[] fields = tClass.getDeclaredFields();
-
- for (Field field : fields) {
- field.setAccessible( true );
-
- Object value = field.get(source);
-
- field.set (obj, value);
- }
- tClass = tClass.getSuperclass();
- }
- return obj;
- }
- }
Unit Tests: - @Test
- public void testGetBean() throws Exception {
- Bean bean = ObjectHelpter.random(Bean.class);
- Bean expect = ObjectHelpter.copy(bean);
-
- when (dao.get(1)).thenReturn(bean);
-
- Bean actual = presenter.getBean(1);
-
- ObjectHelpter.assertEquals(expect, actual);
- }
Run it, the test fails, great (in line with the desired result): Let's change the Presenter back: - public class Presenter {
- DAO dao;
-
- public Bean getBean( int id) {
- Bean bean = dao.get(id);
-
- // bean.setName( "I'm here to make trouble" );
-
- return new Bean(bean.getId(), bean.getName());
- }
- }
Run the unit test again and it passes: 7. Solution 2: Object -> JSON, compare JSON Seeing the title of this section, everyone should understand what is going on. In this example, we will use Gson. Gson - public class PresenterTest{
- @Test
- public void testBean() throws Exception {
- Bean bean = random(Bean.class);
- String expectJson = new Gson().toJson(bean);
-
- when (dao.get(1)).thenReturn(bean);
-
- Bean actual = presenter.getBean(1);
-
- Assert.assertEquals(expectJson, new Gson().toJson(actual, Bean.class));
- }
- }
run: Test failure scenarios: - @Test
- public void testBean() throws Exception {
- Bean bean = random(Bean.class);
- String expectJson = new Gson().toJson(bean);
-
- when (dao.get(1)).thenReturn(bean);
-
- Bean actual = presenter.getBean(1);
- actual.setName( "I'm here to mess things up" ); // Deliberately make the unit test go wrong
-
- Assert.assertEquals(expectJson, new Gson().toJson(actual, Bean.class));
- }
Run, the test fails (in line with the expected results): At first glance, there is no problem. But if there are many member variables, what if the unit test reports an error? - @Test
- public void testUser() throws Exception {
- User user = random( User .class);
- String expectJson = new Gson().toJson( user );
-
- when (dao.getUser(1)).thenReturn( user );
-
- User actual = presenter.getUser(1);
- actual.setWeigth(10);// Wrong value
-
- Assert.assertEquals(expectJson, new Gson().toJson(actual, User .class));
- }
Do you see what's wrong? You have to scroll the window to the right to see which field is different; and when the object is complex, it becomes even more difficult to read. How can we make the prompt more user-friendly? JsonUnit The author introduces a very powerful json comparison library - Json Unit. Gradle introduction: - dependencies {
- compile group : 'net.javacrumbs.json-unit' , name : 'json-unit' , version: '1.16.0'
- }
Maven introduction: - <dependency>
- <groupId>net.javacrumbs.json-unit</groupId>
- <artifactId>json-unit</artifactId>
- <version>1.16.0</version>
- </dependency>
- import static net.javacrumbs.jsonunit.JsonAssert.assertJsonEquals;
-
- @Test
- public void testUser() throws Exception {
- User user = random( User .class);
- String expectJson = new Gson().toJson( user );
-
- when (dao.getUser(1)).thenReturn( user );
-
- User actual = presenter.getUser(1);
- actual.setWeigth(10);// Wrong value
-
- assertJsonEquals(expectJson, actual);
- }
Run, the test fails (in line with expected results): You can see Different value found in node "weigth". Expected 0.005413020868182183, got 10.0., which means the expected value of the node weigth is 0.005413020868182183, but the actual value is 10.0. No matter how complex the json is, JsonUnit can show which fields are different, allowing users to locate the problem most intuitively. JsonUnit has many other advantages. The front and back parameters can be json+objects, not all json or all objects; when comparing lists, the list order can be ignored... DAO - public class DAO {
-
- public List<Bean> getBeans() {
- return ...; // sql, sharePreference operations, etc.
- }
- }
Presenter - public class Presenter {
- DAO dao;
-
- public List<Bean> getBeans() {
- List<Bean> result = dao.getBeans();
-
- Collections.reverse(result); // Reverse the list
-
- return result;
- }
- }
PresenterTest - @Test
- public void testList() throws Exception {
- Bean bean0 = random(Bean.class);
- Bean bean1 = random(Bean.class);
-
- List<Bean> list = Arrays.asList(bean0, bean1);
- String expectJson = new Gson().toJson(list);
-
- when (dao.getBeans()).thenReturn(list);
-
- List<Bean> actual = presenter.getBeans();
-
- Assert.assertEquals(expectJson, new Gson().toJson(actual));
- }
Run, unit test fails (expected result): For junit, the order of the list is different, the generated json string is different, and junit reports an error. For the scenario where "the code cares a lot about the order of the list", this logic is correct. But many times, we don't care so much about the order of the list. In this scenario, junit + gson is a pain in the ass, but JsonUnit can easily solve it: - @Test
- public void testList() throws Exception {
- Bean bean0 = random(Bean.class);
- Bean bean1 = random(Bean.class);
-
- List<Bean> list = Arrays.asList(bean0, bean1);
- String expectJson = new Gson().toJson(list);
-
- when (dao.getBeans()).thenReturn(list);
-
- List<Bean> actual = presenter.getBeans();
-
- // Assert.assertEquals(expectJson, new Gson().toJson(actual));
-
- // expect is json, actual is object, both are OK with jsonUnit
- assertJsonEquals(expectJson, actual, JsonAssert. when ( Option .IGNORING_ARRAY_ORDER));
- }
Run the unit tests with: JsonUnit has many other uses. Readers can go to GitHub to see the introduction. There are a large number of test cases for users to refer to. Parsing JSON scenario For the scenario of testing JSON parsing, the introduction of JsonUnit is even more obvious. - public class Presenter {
- public Bean parse(String json) {
- return new Gson().fromJson(json, Bean.class);
- }
- }
- @Test
- public void testParse() throws Exception {
- String json = "{\"id\":1,\"name\":\"bean\"}" ;
-
- Bean actual = presenter.parse(json);
-
- assertJsonEquals(json, actual);
- }
Run, the test passes: It is fine to use a json or a bean as a parameter; if it is Gson, the bean needs to be converted into json for comparison. summary I feel like I didn't talk about much this time, but the article is very lengthy and there are a lot of complicated codes. I've talked a lot, and I don't know if the readers understand it. The order of writing this article is the author's experience in exploring the verification parameters at that time. There are no high-level concepts this time, just basic and easily overlooked things, which are also very useful in unit testing. I hope readers will understand it well. I have already covered the details of unit testing. In the next article, I will give guidance on how to use unit testing in projects, and the series on unit testing will be almost complete. Of course, I will write more about my experience in the future. About the Author I am a keyboard guy. I live in Guangzhou, work in an Internet company, and am a sleazy literary programmer. I like science, history, investment, and occasionally travel alone. |