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;
}
......
}
|
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" );
}
}
|
这个单元测试是通过的。

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));
}
|
运行一下:

诶,还真通过了!第一个问题解决了,鼓掌..... 稍等,我们把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());
}
}
|
再运行单元测试:

果然出错了!
我们分析一下问题,修改前的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));:

稍等,这样我们不是回到老路,每个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 ;
}
}
|
运行单元测试,通过!

用反射直接对比成员值,思路是正确的。这里解决了“对比两个对象的成员值是否相同,不需要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 . 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();
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());
return obj;
}
}
|
写个单元测试,生成并随机赋值的Bean,输出Bean所有成员值:
1 2 3 4 5 6 7 | @Test
public void testNewBean() throws Exception {
Bean bean = ObjectHelpter.random(Bean. class );
System.out.println(bean.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);
}
}
|
代码少了许多,很爽有没有?
运行一下,通过:

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.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);
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='我来捣乱'}

居然通过了!(不符合预期结果)这是怎么回事?
笔者给大家分析下:我们希望返回的结果是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 );
Assert.assertEquals(id, actual.getId());
Assert.assertEquals(name, actual.getName());
}
|
运行,测试不通过(符合预期结果):
org.junit.ComparisonFailure:
Expected :name_825
Actual :我来捣乱

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

我们把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);
return new Bean(bean.getId(), bean.getName());
}
}
|
再运行单元测试,通过:

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));
}
}
|
运行:

测试失败的场景:
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 ));
}
|
运行,测试不通过(符合预计结果):

咋看没什么问题。但如果成员变量很多,这时单元测试报错呢?
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 ));
}
|

你看出哪里错了吗?你要把窗口滚动到右边,才看到哪个字段不一样;而且当对象比较复杂,就更难看了。怎么才能更人性化提示?
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);
}
|
运行,测试不通过(符合预期结果):

读者可以看到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 ...;
}
}
|
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));
}
|
运行,单元测试不通过(预期结果):

对于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();
assertJsonEquals(expectJson, actual, JsonAssert.when(Option.IGNORING_ARRAY_ORDER));
}
|
运行单元测试,通过:

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);
}
|
运行,测试通过:

一个json,一个bean作为参数,都没问题;如果是Gson的话,还要把Bean转成json去比较。
小结
感觉这次谈了没多少东西,但文章很冗长,繁杂的代码挺多。唠唠叨叨地讲了一大堆,不知道读者有没看明白,本文写作顺序,就是笔者当时探索校验参数的经历。这次没什么高大上的概念,就是基础的、容易忽略的东西,在单元测试中也十分好用,希望读者好好体会。