返回Android......登陆

Android单元测试 - 验证函数参数、返回值的正确姿势

高洛峰2016-11-03 16:10:19548

1.一般形式

Bean

1

2

3

4

5

6

7

8

9

10

11

public class Bean {

    int    id;

    String name;

 

    public Bean(int id, String name) {

        this.id = id;

        this.name = name;

    }

    // getter and setter

    ......

}

DAO

1

2

3

4

5

public class DAO {

    public Bean get(int id) {

        return new Bean(id, "bean_" + id);

    }

}

Presenter

1

2

3

4

5

6

7

8

9

10

11

12

13

14

public class Presenter {

 

    DAO dao;

 

    public Presenter(DAO dao) {

        this.dao = dao;

    }

 

    public Bean getBean(int id) {

        Bean bean = dao.get(id);

 

        return bean;

    }

}

单元测试PresenterTest(下文称为“例子1”)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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");

    }

}

这个单元测试是通过的。

wKioL1gayqiA24f3AAGc29LICkg728.png

2.问题:对象很多变量

上面的Bean只有2个参数,但实际项目,对象往往有很多很多参数,例如,用户信息User :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

public class User {

    int    id;

    String name;

 

    String country;

    String province;

    String city;

    String address;

    int    zipCode;

 

    long birthday;

 

    double height;

    double weigth;

 

    ...

}

单元测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

    @Test

    public void testUser() throws Exception {

        User user = new User(1"bean_1");

        user.setCountry("中国");

        user.setProvince("广东");

        user.setCity("广州");

        user.setAddress("天河区临江大道海心沙公园");

        user.setZipCode(510000);

        user.setBirthday(631123200);

        user.setHeight(173);

        user.setWeigth(55);

        user.setXX(...);

 

        .....

 

        User result = presenter.getUser(1);

 

        Assert.assertEquals(result.getId(), 1);

        Assert.assertEquals(result.getName(), "bean_1");

        Assert.assertEquals(result.getCountry(), "中国");

        Assert.assertEquals(result.getProvince(), "广东");

        Assert.assertEquals(result.getCity(), "广州");

        Assert.assertEquals(result.getAddress(), "天河区临江大道海心沙公园");

        Assert.assertEquals(result.getZipCode(), 510000);

        Assert.assertEquals(result.getBirthday(), 631123200);

        Assert.assertEquals(result.getHeight(), 173);

        Assert.assertEquals(result.getWeigth(), 55);

        Assert.assertEquals(result.getXX(), ...);

        ......

    }

一般形式的单元测试,有10个参数,就要set()10次,get()10次,如果参数更多,一个工程有几十上百个这种测试......感受到那种蛋蛋的痛了吗?

这里有两个痛点:

1.生成对象必须 调用所有setter() 赋值成员变量
2.验证返回值,或者回调参数时,必须 调用所有getter() 获取成员值

3.equals()对比对象,可行吗?

直接调用equals()

这时同学A举手了:“不就是比较对象吗,用equal()还不行?”

为了演示方便,还是用回Bean做例子:

1

2

3

4

5

6

7

8

9

10

    @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));

    }

运行一下:

wKioL1gayqiA24f3AAGc29LICkg728.png

诶,还真通过了!第一个问题解决了,鼓掌..... 稍等,我们把Presenter代码改改,看还能不能凑效:

1

2

3

4

5

6

7

8

public class Presenter {

 

    public Bean getBean(int id) {

        Bean bean = dao.get(id);

 

        return new Bean(bean.getId(), bean.getName());

    }

}

再运行单元测试:

wKioL1gayqiA24f3AAGc29LICkg728.png

果然出错了!

我们分析一下问题,修改前的Presenter.getBean()方法, dao.get()得到的Bean对象,直接作为返回值,所以PresenterTest中Assert.assertTrue(result.equals(bean));通过测试,因为bean和result是同一个对象;修改后,Presenter.getBean()里,返回值是dao.get()得到的Bean的深拷贝,bean和result是不同对象,因此result.equals(bean)==false,测试失败。如果我们使用一般形式Assert.assertEquals(result.getXX(), ...);,单元测试是通过的。

无论是直接返回对象,深拷贝,只要参数一致,都符合我们期望的结果。所以,仅仅调用equals()解决不了问题。

重写equals()方法

同学B:“既然只是比较成员值,重写equals()!”

1

2

3

4

5

6

7

8

9

10

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;

    }

}

再次运行单元测试Assert.assertTrue(result.equals(bean));:

wKioL1gayqiA24f3AAGc29LICkg728.png

稍等,这样我们不是回到老路,每个java bean都要重写equals()吗?尽管整个工程下来,总体代码会减少,但这真不是好办法。

反射比较成员值

同学C:“我们可以用反射获取两个对象所有成员值,并逐一对比。”

哈哈哈,同学C比同学A、B都要聪明点,还会反射!

1

2

3

4

5

6

7

public class PresenterTest{

    @Test

    public void testGetBean() throws Exception {

        ...

        ObjectHelper.assertEquals(bean, result);

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

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;

    }

}

运行单元测试,通过!

wKioL1gayqiA24f3AAGc29LICkg728.png

用反射直接对比成员值,思路是正确的。这里解决了“对比两个对象的成员值是否相同,不需要get()n次”问题。不过,仅仅比较两个对象,这个单元测试还是有问题的。我们先讲第4节,这个问题留在第5节给大家说明。

4.省略不必要setter()

在testUser()中,第一个痛点:“生成对象必须 调用所有setter() 赋值成员变量”。 上一节同学C用反射方案,把对象成员值拿出来,逐一比较。这个方案提醒了我们,赋值也可以同样方案。

ObjectHelper:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

public class ObjectHelper {

 

    protected static final List numberTypes = Arrays.asList(int.classlong.classdouble.classfloat.classboolean.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 不赋值

                    if (Modifier.isFinal(modifiers)) {

                        continue;

                    }

 

                    // 随机生成值

                    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];// 构造函数可能是多参数

 

        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;

    }

}

写个单元测试,生成并随机赋值的Bean,输出Bean所有成员值:

1

2

3

4

5

6

7

@Test

public void testNewBean() throws Exception {

    Bean bean = ObjectHelpter.random(Bean.class);

 

    // 输出bean

    System.out.println(bean.toString()); // toString()读者自己重写一下吧

}

运行测试:

Bean {id: 5505, name: "name_145"}

修改单元测试

单元测试PresenterTest:

1

2

3

4

5

6

7

8

9

10

11

12

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);

    }

}

代码少了许多,很爽有没有?

运行一下,通过:

wKioL1gayqiA24f3AAGc29LICkg728.png

5.比较对象bug

上述笔者提到的解决方案,有一个问题,看以下代码:

Presenter:

1

2

3

4

5

6

7

8

9

10

11

12

13

public class Presenter {

 

    DAO dao;

 

    public Bean getBean(int id) {

        Bean bean = dao.get(id);

 

        // 临时修改bean值

        bean.setName("我来捣乱");

 

        return new Bean(bean.getId(), bean.getName());

    }

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

    @Test

    public void testGetBean() throws Exception {

        Bean expect = random(Bean.class);

 

        System.out.println("expect: " + expect);// 提前输出expect

 

        when(dao.get(1)).thenReturn(expect);

 

        Bean actual = presenter.getBean(1);

 

        System.out.println("actual: " + actual);// 输出结果

 

        ObjectHelper.assertEquals(expect, actual);

    }

运行一下修改后的单元测试:

Pass
expect: Bean {id=3282, name='name_954'}
actual: Bean {id=3282, name='我来捣乱'}

wKioL1gayqiA24f3AAGc29LICkg728.png

居然通过了!(不符合预期结果)这是怎么回事?

笔者给大家分析下:我们希望返回的结果是Bean{id=3282, name='name_954'},但是在Presenter里mock指定的返回对象Bean被修改了,同时返回的Bean深拷贝对象,变量name也跟着变;运行单元测试时,在最后才比较两个对象的成员值,两个对象的name都被修改了,导致equals()认为是正确。

这里的问题:

在Presenter内部篡改了mock指定返回对象的成员值

最简单的解决方法:

在调用Presenter方法前,把的mock返回对象的成员参数,提前拿出来,在单元测试最后比较。

修改单元测试:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

    @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());

    }

运行,测试不通过(符合预期结果):

org.junit.ComparisonFailure: 
Expected :name_825
Actual :我来捣乱

wKioL1gayqiA24f3AAGc29LICkg728.png

符合我们期望值(测试不通过)!等等....这不就回到老路了吗?当有很多成员变量,不就写到手软?前面讲的都白费了?
接下来,进入本文高潮。

6.解决方案1:提前深拷贝expect对象

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

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;

    }

}

单元测试:

1

2

3

4

5

6

7

8

9

10

11

     @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);

    }

运行一下,测试不通过,great(符合想要的结果):

wKioL1gayqiA24f3AAGc29LICkg728.png

我们把Presenter改回去:

1

2

3

4

5

6

7

8

9

10

11

public class Presenter {

    DAO dao;

 

    public Bean getBean(int id) {

        Bean bean = dao.get(id);

 

//        bean.setName("我来捣乱");

 

        return new Bean(bean.getId(), bean.getName());

    }

}

再运行单元测试,通过:

wKioL1gayqiA24f3AAGc29LICkg728.png

7.解决方案2:对象->JSON,比较JSON

看到这节标题,大家都明白怎么回事了吧。例子中,我们会用到Gson。

Gson

1

2

3

4

5

6

7

8

9

10

11

12

13

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));

    }

}

运行:

wKioL1gayqiA24f3AAGc29LICkg728.png

测试失败的场景:

1

2

3

4

5

6

7

8

9

10

11

12

    @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("我来捣乱");// 故意让单元测试出错

 

        Assert.assertEquals(expectJson, new Gson().toJson(actual, Bean.class));

    }

运行,测试不通过(符合预计结果):

wKioL1gayqiA24f3AAGc29LICkg728.png

咋看没什么问题。但如果成员变量很多,这时单元测试报错呢?

1

2

3

4

5

6

7

8

9

10

11

12

    @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);// 错误值

 

        Assert.assertEquals(expectJson, new Gson().toJson(actual, User.class));

    }

wKioL1gayqiA24f3AAGc29LICkg728.png

你看出哪里错了吗?你要把窗口滚动到右边,才看到哪个字段不一样;而且当对象比较复杂,就更难看了。怎么才能更人性化提示?

JsonUnit

笔者给大家介绍一个很强大的json比较库——Json Unit.

gradle引入:

1

2

3

dependencies {

    compile group: 'net.javacrumbs.json-unit', name: 'json-unit', version: '1.16.0'

}

maven引入:

1

2

3

4

5

<dependency>

    <groupId>net.javacrumbs.json-unit</groupId>

    <artifactId>json-unit</artifactId>

    <version>1.16.0</version>

</dependency>

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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);// 错误值

 

    assertJsonEquals(expectJson, actual);

}

运行,测试不通过(符合预期结果):

wKioL1gayqiA24f3AAGc29LICkg728.png

读者可以看到Different value found in node "weigth". Expected 0.005413020868182183, got 10.0.,意思节点weigth期望值0.005413020868182183,但是实际值10.0。

无论json多复杂,JsonUnit都可以显示哪个字段不同,让使用者最直观地定位问题。JsonUnit还有很多好处,前后参数可以json+对象,不要求都是json或都是对象;对比List时,可以忽略List顺序.....

DAO

1

2

3

4

5

6

public class DAO {

 

    public List<Bean> getBeans() {

        return ...; // sql、sharePreference操作等

    }

}

Presenter

1

2

3

4

5

6

7

8

9

public class Presenter {

    DAO dao;    

    public List<Bean> getBeans() {        List<Bean> result = dao.getBeans();

 

        Collections.reverse(result); // 反转列表 

 

        return result;

    }

}

PresenterTest

1

2

3

4

5

6

7

8

9

10

11

12

13

 @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));

    }

运行,单元测试不通过(预期结果):

wKioL1gayqiA24f3AAGc29LICkg728.png

对于junit来说,列表顺序不同,生成的json string不同,junit报错。对于“代码非常在意列表顺序”场景,这逻辑是正确的。但是很多时候,我们并不那么在意列表顺序。这种场景下,junit + gson就蛋疼了,但是JsonUnit可以简单地解决:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

   @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是json,actual是对象,jsonUnit都没问题

        assertJsonEquals(expectJson, actual, JsonAssert.when(Option.IGNORING_ARRAY_ORDER));

    }

运行单元测试,通过:

wKioL1gayqiA24f3AAGc29LICkg728.png

JsonUnit还有很多用法,读者可以上github看看介绍,有大量测试用例,供使用者参考。

解析json的场景

对于测试json解析的场景,JsonUnit的简介就更明显了。

1

2

3

4

5

public class Presenter {  

  public Bean parse(String json) {  

        return new Gson().fromJson(json, Bean.class);

    }

}

1

2

3

4

5

6

7

8

    @Test

    public void testParse() throws Exception {

        String json = "{\"id\":1,\"name\":\"bean\"}";

 

        Bean actual = presenter.parse(json);

 

        assertJsonEquals(json, actual);

    }

运行,测试通过:

wKioL1gayqiA24f3AAGc29LICkg728.png

一个json,一个bean作为参数,都没问题;如果是Gson的话,还要把Bean转成json去比较。

小结

感觉这次谈了没多少东西,但文章很冗长,繁杂的代码挺多。唠唠叨叨地讲了一大堆,不知道读者有没看明白,本文写作顺序,就是笔者当时探索校验参数的经历。这次没什么高大上的概念,就是基础的、容易忽略的东西,在单元测试中也十分好用,希望读者好好体会。



最新手记推荐

• 用composer安装thinkphp框架的步骤• 省市区接口说明• 用thinkphp,后台新增栏目• 管理员添加编辑删除• 管理员添加编辑删除

全部回复(0)我要回复

暂无评论~
  • 取消回复发送