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

rerg4954 8年前
   <p style="text-align: center;"><img src="https://simg.open-open.com/show/4abdff4ddc50ad41f079ff383b384f2b.jpg"></p>    <h2><strong>前言</strong></h2>    <p>读者有没发觉我写文章时,喜欢有个前言、序?真相是,一半用来装逼凑字数,一半是因为不知道接下来要写什么,先闲聊几句压压惊^_^ 哈哈哈......该说的还是要说。</p>    <p>本篇讲解参数验证。验证参数传递、函数返回值,是单元测试中十分重要的环节。笔者相信不少读者都有验证过参数,但是你的单元测试代码真的是正确的吗?笔者在早期实践的时候,遇到一些问题,积累了一点心得,本期与大家分享一下。</p>    <h2><strong>1.一般形式</strong></h2>    <p>Bean</p>    <pre>  <code class="language-java">public class Bean {      int    id;      String name;        public Bean(int id, String name) {          this.id = id;          this.name = name;      }      // getter and setter      ......  }</code></pre>    <p>DAO</p>    <pre>  <code class="language-java">public class DAO {      public Bean get(int id) {          return new Bean(id, "bean_" + id);      }  }</code></pre>    <p>Presenter</p>    <pre>  <code class="language-java">public class Presenter {        DAO dao;        public Presenter(DAO dao) {          this.dao = dao;      }        public Bean getBean(int id) {          Bean bean = dao.get(id);            return bean;      }  }</code></pre>    <p>单元测试 PresenterTest (下文称为 <strong>“例子1”</strong> )</p>    <pre>  <code class="language-java">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");      }  }</code></pre>    <p>这个单元测试是通过的。</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9c5887c99416216f04f0bd262f02e047.png"></p>    <p style="text-align:center">Presenter Pass</p>    <h2><strong>2.问题:对象很多变量</strong></h2>    <p>上面的 Bean 只有2个参数,但实际项目,对象往往有很多很多参数,例如,用户信息 User :</p>    <pre>  <code class="language-java">public class User {      int    id;      String name;        String country;      String province;      String city;      String address;      int    zipCode;        long birthday;        double height;      double weigth;        ...  }</code></pre>    <p>单元测试:</p>    <pre>  <code class="language-java">@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(), ...);          ......      }</code></pre>    <p>一般形式的单元测试,有10个参数,就要 set() 10次, get() 10次,如果参数更多,一个工程有几十上百个这种测试......感受到那种蛋蛋的痛了吗?</p>    <p>这里有两个痛点:</p>    <p>1.生成对象必须 <strong> 调用所有 setter() </strong> 赋值成员变量</p>    <p>2.验证返回值,或者回调参数时,必须 <strong> 调用所有 getter() </strong> 获取成员值</p>    <h2><strong>3.equals()对比对象,可行吗?</strong></h2>    <h3>直接调用equals()</h3>    <p>这时同学A举手了:“不就是比较对象吗,用 equal() 还不行?”</p>    <p>为了演示方便,还是用回 Bean 做例子:</p>    <pre>  <code class="language-java">@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));      }</code></pre>    <p>运行一下:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9ed1542ac14f4aff91b445d6187a59a0.png"></p>    <p style="text-align:center">Bean Equals Pass</p>    <p>诶,还真通过了!第一个问题解决了,鼓掌..... 稍等,我们把 Presenter 代码改改,看还能不能凑效:</p>    <pre>  <code class="language-java">public class Presenter {        public Bean getBean(int id) {          Bean bean = dao.get(id);            return new Bean(bean.getId(), bean.getName());      }  }</code></pre>    <p>再运行单元测试:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/67894e8fe5bc91947134a10a6a41f837.png"></p>    <p style="text-align:center">Presenter Error</p>    <p>果然出错了!</p>    <p>我们分析一下问题,修改前的 Presenter.getBean() 方法, dao.get() 得到的 Bean 对象,直接作为返回值,所以 PresenterTest 中 Assert.assertTrue(result.equals(bean)); 通过测试,因为 bean 和 result 是 <strong>同一个对象</strong> ;修改后, Presenter.getBean() 里,返回值是 dao.get() 得到的 Bean 的 <strong>深拷贝</strong> , bean 和 result 是 <strong>不同对象</strong> ,因此 result.equals(bean)==false ,测试失败。如果我们使用 <strong>一般形式</strong> Assert.assertEquals(result.getXX(), ...); ,单元测试是通过的。</p>    <p>无论是直接返回对象,深拷贝,只要参数一致,都符合我们期望的结果。所以,仅仅调用 equals() 解决不了问题。</p>    <h3><strong>重写equals()方法</strong></h3>    <p>同学B:“既然只是比较成员值,重写equals()!”</p>    <pre>  <code class="language-java">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;      }  }</code></pre>    <p>再次运行单元测试 Assert.assertTrue(result.equals(bean)); :</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9ed1542ac14f4aff91b445d6187a59a0.png"></p>    <p>稍等,这样我们不是回到老路,每个 java bean 都要重写 equals() 吗?尽管整个工程下来,总体代码会减少,但这真不是好办法。</p>    <h3><strong>反射比较成员值</strong></h3>    <p>同学C:“我们可以用反射获取两个对象所有成员值,并逐一对比。”</p>    <p>哈哈哈,同学C比同学A、B都要聪明点,还会反射!</p>    <pre>  <code class="language-java">public class PresenterTest{      @Test      public void testGetBean() throws Exception {          ...          ObjectHelper.assertEquals(bean, result);      }  }</code></pre>    <pre>  <code class="language-java">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;      }  }</code></pre>    <p>运行单元测试,通过!</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/9ed1542ac14f4aff91b445d6187a59a0.png"></p>    <p>用反射直接对比成员值,思路是正确的。这里解决了 <strong> “对比两个对象的成员值是否相同,不需要 get() n次” </strong> 问题。不过,仅仅比较两个对象,这个单元测试还是有问题的。我们先讲 <strong>第4节</strong> ,这个问题留在 <strong>第5节</strong> 给大家说明。</p>    <h2><strong>4.省略不必要 setter()</strong></h2>    <p>在 testUser() 中,第一个痛点: <strong> “生成对象必须 调用所有 setter() 赋值成员变量” </strong> 。 上一节同学C用反射方案,把对象成员值拿出来,逐一比较。这个方案提醒了我们,赋值也可以同样方案。</p>    <p>ObjectHelper :</p>    <pre>  <code class="language-java">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;      }  }</code></pre>    <p>写个单元测试,生成并随机赋值的 Bean ,输出 Bean 所有成员值:</p>    <pre>  <code class="language-java">@Test  public void testNewBean() throws Exception {      Bean bean = ObjectHelpter.random(Bean.class);        // 输出bean      System.out.println(bean.toString()); // toString()读者自己重写一下吧  }</code></pre>    <p>运行测试:</p>    <p>Bean {id: 5505, name: "name_145"}</p>    <h3><strong>修改单元测试</strong></h3>    <p>单元测试 PresenterTest :</p>    <pre>  <code class="language-java">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);      }  }</code></pre>    <p>代码少了许多,很爽有没有?</p>    <p>运行一下,通过:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/b5479491cff73c4f575d44e1a97ebc65.png"></p>    <p style="text-align:center">Presenter Test User Pass</p>    <h2><strong>5.比较对象bug</strong></h2>    <p>上述笔者提到的解决方案,有一个问题,看以下代码:</p>    <p>Presenter :</p>    <pre>  <code class="language-java">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());      }  }</code></pre>    <pre>  <code class="language-java">@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);      }</code></pre>    <p>运行一下修改后的单元测试:</p>    <p>Pass</p>    <p>expect: Bean {id=3282, name='name_954'}</p>    <p>actual: Bean {id=3282, name='我来捣乱'}</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/5e4301b69b6cbeb6d87b70012530e29b.png"></p>    <p style="text-align:center">test bean</p>    <p>居然通过了! <strong>(不符合预期结果)</strong> 这是怎么回事?</p>    <p>笔者给大家分析下:我们希望返回的结果是 Bean{id=3282, name='name_954'} ,但是在 Presenter 里 <strong>mock</strong> 指定的返回对象 Bean 被修改了,同时返回的 Bean 深拷贝对象,变量 name 也跟着变;运行单元测试时,在最后才 <strong>比较两个对象的成员值</strong> ,两个对象的 name 都被修改了,导致 equals() 认为是正确。</p>    <p>这里的问题:</p>    <p>在 Presenter 内部篡改了mock指定返回对象的成员值</p>    <p>最简单的解决方法:</p>    <p>在调用 Presenter 方法前,把的mock返回对象的成员参数,提前拿出来,在单元测试最后比较。</p>    <p>修改单元测试:</p>    <pre>  <code class="language-java">@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());      }</code></pre>    <p>运行,测试不通过(符合预期结果):</p>    <p>org.junit.ComparisonFailure:</p>    <p>Expected :name_825</p>    <p>Actual :我来捣乱</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/0d4e1ecac73c3c06ca3b51a759a1f923.png"></p>    <p style="text-align:center">test bean error</p>    <p>符合我们期望值(测试不通过)!等等....这不就回到老路了吗?当有很多成员变量,不就写到手软?前面讲的都白费了?</p>    <p>接下来,进入本文 <strong>高潮</strong> 。</p>    <h2><strong>6.解决方案1:提前深拷贝expect对象</strong></h2>    <pre>  <code class="language-java">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;      }  }</code></pre>    <p>单元测试:</p>    <pre>  <code class="language-java">@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);      }</code></pre>    <p>运行一下,测试不通过,great(符合想要的结果):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/e2ef7a55a5c1b0c5141efc2dc1b0a7d6.png"></p>    <p style="text-align:center">test bean error</p>    <p>我们把 Presenter 改回去:</p>    <pre>  <code class="language-java">public class Presenter {      DAO dao;        public Bean getBean(int id) {          Bean bean = dao.get(id);    //        bean.setName("我来捣乱");            return new Bean(bean.getId(), bean.getName());      }  }</code></pre>    <p>再运行单元测试,通过:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/55c45101f5af985eda4350ee7b33b327.png"></p>    <p style="text-align:center">test bean pass</p>    <h2><strong>7.解决方案2:对象->JSON,比较JSON</strong></h2>    <p>看到这节标题,大家都明白怎么回事了吧。例子中,我们会用到 <a href="/misc/goto?guid=4959722995221023503" rel="nofollow,noindex">Gson</a> 。</p>    <h3>Gson</h3>    <pre>  <code class="language-java">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));      }  }</code></pre>    <p>运行:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3e1f1777b7d72ee9e6060fa09d5c7e51.png"></p>    <p style="text-align:center">test json string pass</p>    <p>测试失败的场景:</p>    <pre>  <code class="language-java">@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));      }</code></pre>    <p>运行,测试不通过(符合预计结果):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/4899042f1314b8220e6895721752f588.png"></p>    <p style="text-align:center">test json string error</p>    <p>咋看没什么问题。但如果成员变量很多,这时单元测试报错呢?</p>    <pre>  <code class="language-java">@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));      }</code></pre>    <p><img src="https://simg.open-open.com/show/baedee775269dc0dc9879d9c2526684b.png"></p>    <p style="text-align:center">test json string error</p>    <p>你看出哪里错了吗?你要把窗口滚动到右边,才看到哪个字段不一样;而且当对象比较复杂,就更难看了。怎么才能更人性化提示?</p>    <h3><strong>JsonUnit</strong></h3>    <p>笔者给大家介绍一个很强大的json比较库—— Json Unit .</p>    <p>gradle引入:</p>    <pre>  <code class="language-java">dependencies {      compile group: 'net.javacrumbs.json-unit', name: 'json-unit', version: '1.16.0'  }</code></pre>    <p>maven引入:</p>    <pre>  <code class="language-java"><dependency>      <groupId>net.javacrumbs.json-unit</groupId>      <artifactId>json-unit</artifactId>      <version>1.16.0</version>  </dependency></code></pre>    <pre>  <code class="language-java">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);  }</code></pre>    <p>运行,测试不通过(符合预期结果):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/2d6177aed82dfb99495a1ed86a3cad7f.png"></p>    <p style="text-align:center">json unit error</p>    <p>读者可以看到 Different value found in node "weigth". Expected 0.005413020868182183, got 10.0. ,意思节点 weigth 期望值 0.005413020868182183 ,但是实际值 10.0 。</p>    <p>无论json多复杂, <strong>JsonUnit</strong> 都可以显示哪个字段不同,让使用者最直观地定位问题。 <strong>JsonUnit</strong> 还有很多好处,前后参数可以json+对象,不要求都是json或都是对象;对比 List 时,可以忽略 List 顺序.....</p>    <p>DAO</p>    <pre>  <code class="language-java">public class DAO {        public List<Bean> getBeans() {          return ...; // sql、sharePreference操作等      }  }</code></pre>    <p>Presenter</p>    <pre>  <code class="language-java">public class Presenter {      DAO dao;        public List<Bean> getBeans() {          List<Bean> result = dao.getBeans();            Collections.reverse(result); // 反转列表             return result;      }  }</code></pre>    <p>PresenterTest</p>    <pre>  <code class="language-java">@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));      }</code></pre>    <p>运行,单元测试不通过(预期结果):</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/d1469c61a93f205ae46ec093cc0681c7.png"></p>    <p style="text-align:center">test list error</p>    <p>对于junit来说,列表顺序不同,生成的json string不同,junit报错。对于 <strong>“代码非常在意列表顺序”</strong> 场景,这逻辑是正确的。但是很多时候,我们并不那么在意列表顺序。这种场景下,junit + gson就蛋疼了,但是JsonUnit可以简单地解决:</p>    <pre>  <code class="language-java">@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));      }</code></pre>    <p>运行单元测试,通过:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/3911c6ae53735f88ee7ac7bde2e7aa1d.png"></p>    <p style="text-align:center">test list pass</p>    <p>JsonUnit还有很多用法,读者可以上github看看介绍,有大量测试用例,供使用者参考。</p>    <h3><strong>解析json的场景</strong></h3>    <p>对于测试json解析的场景,JsonUnit的简介就更明显了。</p>    <pre>  <code class="language-java">public class Presenter {      public Bean parse(String json) {          return new Gson().fromJson(json, Bean.class);      }  }</code></pre>    <pre>  <code class="language-java">@Test      public void testParse() throws Exception {          String json = "{\"id\":1,\"name\":\"bean\"}";            Bean actual = presenter.parse(json);            assertJsonEquals(json, actual);      }</code></pre>    <p>运行,测试通过:</p>    <p style="text-align: center;"><img src="https://simg.open-open.com/show/35560df1a114aa239a2ef6d174b40d7a.png"></p>    <p style="text-align:center">parse json jsonunit</p>    <p>一个json,一个bean作为参数,都没问题;如果是 <strong>Gson</strong> 的话,还要把 Bean 转成json去比较。</p>    <h2><strong>小结</strong></h2>    <p>感觉这次谈了没多少东西,但文章很冗长,繁杂的代码挺多。唠唠叨叨地讲了一大堆,不知道读者有没看明白,本文写作顺序,就是笔者当时探索校验参数的经历。这次没什么高大上的概念,就是基础的、容易忽略的东西,在单元测试中也十分好用,希望读者好好体会。</p>    <p> </p>    <p> </p>    <p>来自:http://www.jianshu.com/p/77ee7c0270bc</p>    <p> </p>