关于Java单元测试,你需要知道的一切

发表于:2018-1-08 11:00

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Lam    来源:个人博客

  简介
  单元测试是先mock一些正常边界异常条件来对接口进行操作,并且期望接口返回什么内容,最后接口实现了之后再重新测试一遍。
  TDD:Test-Driven Development,测试驱动开发模式——旨在强调在开发功能代码之前,先编写测试代码。开发未动,测试先行。
  为什么我们要进行单元测试:
  最后才修改一个 bug 的代价是在 bug 产生时修改它的代价的10倍。——《快速软件开发
  测试任何可能的错误。单元测试不是用来证明您是对的,而是为了证明您没有错。
  单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能。单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。
  单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。
  JUNIT
  JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。JUnit 测试是程序员测试,即所谓白盒测试。它是一个 Java 语言的测试框架,多数 Java 的开发环境都已经集成了 JUnit 作为单元测试的工具。
  特性
  ●用于测试期望结果的断言(Assertion)
  ●用于共享共同测试数据的测试工具
  ●用于方便的组织和运行测试的测试套件
  ●图形和文本的测试运行器
  基本流程
  ●写测试类并继承 TestCase 类
  ●写测试方法 testXXX(), 测试方法必须以 test 开头
  ●写测试套件类,将 test case 加入到 test suite, 如果没有为 test case 写 test suite 的话,系统会默认为每个 test case 生成一个 test suite
  ●运行JUnit Test 进行测试
  剖析概念
  TestXXX
  ●TestCase(测试用例
  当一个类继承 JUnit 的 TestCase 类,即成为一个测试类,而且,测试类中的方法必须以 test 开头,比如:testAdd() 等。
  ●TestSuite(测试套件)
  TestSuite 是一组测试,目的在于将相关的测试用例归入一组。当执行一个 Test Suite 时,就会执行组内所有的测试方法,这就避免了繁琐的测试步骤。当然,如果没有为 test case 写 test suite 的话,系统会默认为每个 test case 生成一个 test suite。
  Assert(断言)
  Assert 用于检查条件是否成立,当条件成立则 Assert 方法通过,否则会抛出异常。例如,Assert.assertEquals(3, result); 判断 result 是否跟期望的3想等,如果想等则通过,否则测试失败。
  主要有如下几个断言方法:
  ●assertTrue/False():判断一个条件是 true 还是 false。
  ●fail():失败,可以有消息,也可以没有消息。
  ●assertEquals():判断是否想等,可以指定输出错误信息。注意不同数据类型所使用的 assertEquals 方法参数有所不同。
  ●assertNotNull/Null():判断一个对象是否为空。
  ●assertSame/NotSame():判断两个对象是否指向同一个对象。
  ●failNotSame/failNotEquals():当不指向同一个内存地址或者不相等的时候,输出错误信息。错误信息为指定的格式。
  ●setUp
  每次测试方法执行之前,都会执行 setUp 方法,此方法用于进行一些固定的准备工作,比如,实例化对象,打开网络连接等。
  ●tearDown
  每次测试方法执行之后,都会执行 tearDown 方法,此方法用于进行一些固定的善后工作,比如,关闭网络连接等。
  Junit3.x与4.x区别
  Failure与Error区别
  单元测试的失败(Failure)与测试出现了错误(Error)
  JUnit 将测试失败的情况分为两种:Failure 和 Error 。 Failure 一般是由单元测试使用的断言方法判断失败引起的,它表示在测试点发现了问题(程序中的 bug);而 Error 则是有代码异常引起的,这是测试目的之外的发现,它可能产生于测试代码本身的错误(也就是说,编写的测试代码有问题),也可能是被测试代码中的一个隐藏 bug 。不过,一般情况下是第一种情况。
  常用注解
  ●@Before
  初始化方法,在任何一个测试方法执行之前,必须执行的代码。对比 JUnit 3 ,和 setUp()方法具有相同的功能。在该注解的方法中,可以进行一些准备工作,比如初始化对象,打开网络连接等。
  ●@After
  释放资源,在任何一个测试方法执行之后,需要进行的收尾工作。对比 JUnit 3 ,和 tearDown()方法具有相同的功能。
  ●@Test
  测试方法,表明这是一个测试方法。在 JUnit 中将会自动被执行。对与方法的声明也有如下要求:名字可以随便取,没有任何限制,但是返回值必须为 void ,而且不能有任何参数。如果违反这些规定,会在运行时抛出一个异常。不过,为了培养一个好的编程习惯,我们一般在测试的方法名上加 test ,比如:testAdd()。
  同时,该 Annotation(@Test) 还可以测试期望异常和超时时间,如 @Test(timeout=100),我们给测试函数设定一个执行时间,超过这个时间(100毫秒),他们就会被系统强行终止,并且系统还会向你汇报该函数结束的原因是因为超时,这样你就可以发现这些 bug 了。而且,它还可以测试期望的异常,例如,我们刚刚的那个空指针异常就可以这样:@Test(expected=NullPointerException.class),此时如果出现空指针异常,反正会认为测试通过
  ●@Ignore
  忽略的测试方法,标注的含义就是“某些方法尚未完成,咱不参与此次测试”;这样的话测试结果就会提示你有几个测试被忽略,而不是失败。一旦你完成了相应的函数,只需要把 @Ignore 注解删除即可,就可以进行正常测试了。
  ●@BeforeClass
  针对所有测试,也就是整个测试类中,在所有测试方法执行前,都会先执行由它注解的方法,而且只执行一次。当然,需要注意的是,修饰符必须是 public static void xxxx ;此 Annotation 是 JUnit 4 新增的功能。
  ●@AfterClass
  针对所有测试,也就是整个测试类中,在所有测试方法都执行完之后,才会执行由它注解的方法,而且只执行一次。当然,需要注意的是,修饰符也必须是 public static void xxxx ;此 Annotation 也是 JUnit 4 新增的功能,与 @BeforeClass 是一对。
  执行顺序
  所以,在 JUnit 4 中,单元测试用例的执行顺序为:
  每一个测试方法的调用顺序为:
  规范
  ●单元测试代码应位于单独的 Source Folder 下
  此 Source Folder 通常为 test ,这样可以方便的管理业务代码与测试代码。
  ●测试类应该与被测试类位于同一 package名 下
  ●选择有意义的测试方法名
  无论是 JUnit 4 ,还是 JUnit 3 ,单元测试方法名均需使用 test<待测试方法名称>[概要描述]
  ●保存测试的独立性
  每项单元测试都必须独立于其他所有单元测试而运行,因为单元测试需能以任何顺序运行。
  ●为暂时未实现的测试代码忽略(@Ignore)或抛出失败(fail)
  在 JUnit 4 中,可以在测试方法上使用注解 @Ignore 。在 JUnit 3 中,可以在未实现的测试方法中使用 fail(“测试方法未实现”); 以告知失败是因为测试方法未实现。
  ●在调用断言(assert)方法时给出失败的原因
  在使用断言方法时,请使用带有 message 参数的 API ,并在调用时给出失败时的原因描述,如 assertNotNull(”对象为空”, new Object())。
  运行器Runner
  类注解@RunWith,指定不同的类可以改变测试类的行为
  参数化测试
  主要是针对一些相同功能却要进行多组参数测试的情况,开发步骤如下:
  1.参数化测试的类和普通测试类不同,要用特殊的Runner,类注解需要改为@RunWith(Parameterized.class)
  2.定义该测试类的成员变量,一个放测试参数,另一个放该参数产生的预期结果
  3.定义测试数据集合方法 public static Collection data() {…},注意该方法要用@Parameters修饰(数据格式为二维数组)
  4.定义带参数的构造函数,注意定义数据集合时,要和构造函数参数次序一致
  //指定@RunWith  
  @RunWith(Parameterized.class)  
  public class ParamTest {  
    //定义成员变量,i为测试参数,j为测试结果  
    private int i;  
    private int j;  
    //构造函数  
    public ParamTest(int i, int j) {  
        super();  
        this.i = i;  
        this.j = j;  
    }  
    //测试数据集合,注意使用的注解,数据结构及次序  
    @Parameters  
    public static Collection data() {  
        return Arrays.asList(new Object[][]{{1,2},{3,4},{4,6}});  
    }  
    @Test  
    public void testMethod1() {  
        System.out.println(i);  
        System.out.println(j);  
        //简单测试,只测试参数加1会不会等于预期结果  
        Assert.assertEquals(i+1, j);  
    }  
  } 
 
  Mock/Stub
  如果你真的去把所有获取测试你的服务的前置条件的代码都完成,然后再写你的单元测试代码的话,这就不是单元测试了,这叫自动化测试,所以我们需要mock。
  Mockito是Google Code上的一个开源项目,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。
  Mockito创建mock对象不能对final,Anonymous ,primitive类和静态方法进行mock, powermock通过修改字节码来支持了此功能。
  验证调用行为
  import static org.mockito.Mockito.*;  
  //创建Mock  
  List mockedList = mock(List.class);  
  //使用Mock对象  
  mockedList.add("one");  
  mockedList.clear();  
  //验证行为  
  verify(mockedList).add("one");  
  verify(mockedList).clear(); 
 
  ●对Mock对象进行Stub
  //也可以Mock具体的类,而不仅仅是接口  
  LinkedList mockedList = mock(LinkedList.class);  
  //Stub  
  when(mockedList.get(0)).thenReturn("first"); // 设置返回值  
  when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常  
  //第一个会打印 "first"  
  System.out.println(mockedList.get(0));  
  //接下来会抛出runtime异常  
  System.out.println(mockedList.get(1));  
  //接下来会打印"null",这是因为没有stub get(999)  
  System.out.println(mockedList.get(999));  
  // 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1)  
  verify(mockedList).get(0); 
 
  注意:android SDK中的库,大部分都只定义了接口而在jvm直接运行时是没有实现的,需要在虚拟机或者真机上运行,所以尽量使用自己导入的库,因为这些库带有具体实现,测试方法才能有效。


上文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号