原文连接:http://www.jianshu.com/p/77ee7c0270bcjava
读者有没发觉我写文章时,喜欢有个前言、序?真相是,一半用来装逼凑字数,一半是由于不知道接下来要写什么,先闲聊几句压压惊^_^ 哈哈哈......该说的仍是要说。node
上一篇《Android单元测试 - Sqlite、SharedPreference、Assets、文件操做 怎么测?》 讲了一些DAO(Data Access Object)单元测试的细节。本篇讲解参数验证。git
验证参数传递、函数返回值,是单元测试中十分重要的环节。笔者相信很多读者都有验证过参数,可是你的单元测试代码真的是正确的吗?笔者在早期实践的时候,遇到一些问题,积累了一点心得,本期与你们分享一下。github
Bean
sql
public class Bean { int id; String name; public Bean(int id, String name) { this.id = id; this.name = name; } // getter and setter ...... }
DAO
json
public class DAO { public Bean get(int id) { return new Bean(id, "bean_" + id); } }
Presenter
dom
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”)maven
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"); } }
这个单元测试是经过的。ide
上面的Bean
只有2个参数,但实际项目,对象每每有不少不少参数,例如,用户信息User
:函数
public class User { int id; String name; String country; String province; String city; String address; int zipCode; long birthday; double height; double weigth; ... }
单元测试:
@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()
获取成员值
这时同窗A举手了:“不就是比较对象吗,用
equal()
还不行?”
为了演示方便,仍是用回Bean
作例子:
@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
代码改改,看还能不能凑效:
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()
解决不了问题。
同窗B:“既然只是比较成员值,重写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; } }
再次运行单元测试Assert.assertTrue(result.equals(bean));
:
稍等,这样咱们不是回到老路,每一个java bean
都要重写equals()
吗?尽管整个工程下来,整体代码会减小,但这真不是好办法。
同窗C:“咱们能够用反射获取两个对象全部成员值,并逐一对比。”
哈哈哈,同窗C比同窗A、B都要聪明点,还会反射!
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; } }
运行单元测试,经过!
用反射直接对比成员值,思路是正确的。这里解决了“对比两个对象的成员值是否相同,不须要get()
n次”问题。不过,仅仅比较两个对象,这个单元测试仍是有问题的。咱们先讲第4节,这个问题留在第5节给你们说明。
setter()
在testUser()
中,第一个痛点:“生成对象必须 调用全部setter()
赋值成员变量”。 上一节同窗C用反射方案,把对象成员值拿出来,逐一比较。这个方案提醒了咱们,赋值也能够一样方案。
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 不赋值 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
全部成员值:
@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
:
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); } }
代码少了许多,很爽有没有?
运行一下,经过:
上述笔者提到的解决方案,有一个问题,看如下代码:
Presenter
:
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()); } }
@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='我来捣乱'}
竟然经过了!(不符合预期结果)这是怎么回事?
笔者给你们分析下:咱们但愿返回的结果是Bean{id=3282, name='name_954'}
,可是在Presenter
里mock指定的返回对象Bean
被修改了,同时返回的Bean
深拷贝对象,变量name
也跟着变;运行单元测试时,在最后才比较两个对象的成员值,两个对象的name
都被修改了,致使equals()
认为是正确。
这里的问题:
在
Presenter
内部篡改了mock指定返回对象的成员值
最简单的解决方法:
在调用
Presenter
方法前,把的mock返回对象的成员参数,提早拿出来,在单元测试最后比较。
修改单元测试:
@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 :我来捣乱
符合咱们指望值(测试不经过)!等等....这不就回到老路了吗?当有不少成员变量,不就写到手软?前面讲的都白费了?
接下来,进入本文高潮。
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; } }
单元测试:
@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
改回去:
public class Presenter { DAO dao; public Bean getBean(int id) { Bean bean = dao.get(id); // bean.setName("我来捣乱"); return new Bean(bean.getId(), bean.getName()); } }
再运行单元测试,经过:
看到这节标题,你们都明白怎么回事了吧。例子中,咱们会用到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)); } }
运行:
测试失败的场景:
@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)); }
运行,测试不经过(符合预计结果):
咋看没什么问题。但若是成员变量不少,这时单元测试报错呢?
@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)); }
你看出哪里错了吗?你要把窗口滚动到右边,才看到哪一个字段不同;并且当对象比较复杂,就更难看了。怎么才能更人性化提示?
笔者给你们介绍一个很强大的json比较库——Json Unit.
gradle引入:
dependencies { compile group: 'net.javacrumbs.json-unit', name: 'json-unit', version: '1.16.0' }
maven引入:
<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);// 错误值 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
public class DAO { public List<Bean> getBeans() { return ...; // sql、sharePreference操做等 } }
Presenter
public class Presenter { DAO dao; public List<Bean> getBeans() { List<Bean> result = dao.getBeans(); Collections.reverse(result); // 反转列表 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)); }
运行,单元测试不经过(预期结果):
对于junit来讲,列表顺序不一样,生成的json string不一样,junit报错。对于“代码很是在乎列表顺序”场景,这逻辑是正确的。可是不少时候,咱们并不那么在乎列表顺序。这种场景下,junit + gson就蛋疼了,可是JsonUnit能够简单地解决:
@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)); }
运行单元测试,经过:
JsonUnit还有不少用法,读者能够上github看看介绍,有大量测试用例,供使用者参考。
对于测试json解析的场景,JsonUnit的简介就更明显了。
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); }
运行,测试经过:
一个json,一个bean做为参数,都没问题;若是是Gson的话,还要把Bean
转成json去比较。
感受此次谈了没多少东西,但文章很冗长,繁杂的代码挺多。唠唠叨叨地讲了一大堆,不知道读者有没看明白,本文写做顺序,就是笔者当时探索校验参数的经历。此次没什么高大上的概念,就是基础的、容易忽略的东西,在单元测试中也十分好用,但愿读者好好体会。
单元测试的细节,已经讲得七七八八了。下一篇再指导一下项目使用单元测试,单元测试的系列就差很少完结。固然之后有更多心得,还会写的。
关于做者
我是键盘男。在广州生活,在互联网公司上班,猥琐文艺码农。喜欢科学、历史,玩玩投资,偶尔独自旅行。