前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈基于 JUnit 的单元测试

浅谈基于 JUnit 的单元测试

作者头像
CG国斌
发布2022-11-28 08:57:15
9710
发布2022-11-28 08:57:15
举报
文章被收录于专栏:维C果糖维C果糖维C果糖

文章目录

1 简介

JUnit 是一个 Java 语言的单元测试框架,它由 Kent Beck 和 Erich Gamma 建立,逐渐成为 xUnit 家族中最为成功的一个。 JUnit 有它自己的 JUnit 扩展生态圈,多数 Java 的开发环境都已经集成了 JUnit 作为单元测试的工具。在这里,一个单元可以是一个方法、类、包或者子系统。因此,单元测试是指对代码中的最小可测试单元进行检查和验证,以便确保它们正常工作。例如,我们可以给予一定的输入测试输出是否是所希望得到的结果。在本文中,我们将着重介绍 JUnit 4.X 版本的特性,这也是我们在日常开发中使用最多的版本。

2 特点

  • JUnit 提供了注释以及确定的测试方法;
  • JUnit 提供了断言用于测试预期的结果;
  • JUnit 测试优雅简洁不需要花费太多的时间;
  • JUnit 测试让大家可以更快地编写代码并且提高质量;
  • JUnit 测试可以组织成测试套件包含测试案例,甚至其他测试套件;
  • Junit 显示测试进度,如果测试是没有问题条形是绿色的,测试失败则会变成红色;
  • JUnit 测试可以自动运行,检查自己的结果,并提供即时反馈,没有必要通过测试结果报告来手动梳理。

3 内容

3.1 注解

  • @Test:该注释表示,用其附着的公共无返回值方法(即用public修饰的void类型的方法 )可以作为一个测试用例;
  • @Before:该注释表示,用其附着的方法必须在类中的每个测试之前执行,以便执行测试某些必要的先决条件;
  • @BeforeClass:该注释表示,用其附着的静态方法必须执行一次并在类的所有测试之前,发生这种情况时一般是测试计算共享配置方法,如连接到数据库;
  • @After:该注释表示,用其附着的方法在执行每项测试后执行,如执行每一个测试后重置某些变量,删除临时变量等;
  • @AfterClass:该注释表示,当需要执行所有的测试在 JUnit 测试用例类后执行,@AfterClass注解可以使用以清理建立方法,如断开数据库连接,注意:附有此注解(类似于@BeforeClass)的方法必须定义为静态;
  • @Ignore:该注释表示,当想暂时禁用特定的测试执行可以使用忽略注释,每个被注解为@Ignore的方法将不被执行。
/**
*  JUnit 注解示例
*/
@Test
public void testMethod(){
	Syetem.out.println("用@Test标示测试方法!");
}

@AfterClass
public static void testAfterClassMethod(){
	Syetem.out.println("用@AfterClass标示的方法在测试用例类执行完之后!");
}

3.2 断言

在这里,将介绍一些断言方法,所有这些方法都来自org.junit.Assert类,其扩展了java.lang.Object类并为它们提供编写测试,以便检测故障。简而言之,我们就是通过断言方法来判断实际结果与我们预期的结果是否相同,如果相同,则测试成功,反之,则测试失败。

  • void assertEquals([String message], expected value, actual value):断言两个值相等,值的类型可以为intshortlongbytechar或者java.lang.Object,其中第一个参数是一个可选的字符串消息;
  • void assertTrue([String message], boolean condition):断言一个条件为真;
  • void assertFalse([String message],boolean condition):断言一个条件为假;
  • void assertNotNull([String message], java.lang.Object object):断言一个对象不为空;
  • void assertNull([String message], java.lang.Object object):断言一个对象为空;
  • void assertSame([String message], java.lang.Object expected, java.lang.Object actual):断言两个对象引用相同的对象;
  • void assertNotSame([String message], java.lang.Object unexpected, java.lang.Object actual):断言两个对象不是引用同一个对象;
  • void assertArrayEquals([String message], expectedArray, resultArray):断言预期数组和结果数组相等,数组的类型可以为intlongshortcharbyte或者java.lang.Object

4 JUnit 3.X 和 JUnit 4.X 的区别

4.1 JUnit 3.X

  • 使用 JUnit 3.X 版本进行单元测试时,测试类必须要继承于TestCase父类;
  • 测试方法需要遵循的原则:
    • public的;
    • void的;
    • 无方法参数;
    • 方法名称必须以test开头
  • 不同的测试用例之间一定要保持完全的独立性,不能有任何的关联;
  • 要掌握好测试方法的顺序,不能依赖于测试方法自己的执行顺序。
/**
* 用 JUnit 3.X 进行测试
*/
import junit.framework.Assert;
import junit.framework.TestCase;

public class TestOperation extends TestCase {

	private Operation operation;
	
	public TestOperation(String name) {  // 构造函数
		super(name);
	}
	
	@Override
	public void setUp() throws Exception {  // 在每个测试方法执行 [之前] 都会被调用,多用于初始化
		System.out.println("欢迎使用Junit进行单元测试...");
		operation = new Operation();
	}
	
	@Override
	public void tearDown() throws Exception {  // 在每个测试方法执行 [之后] 都会被调用,多用于释放资源
		System.out.println("Junit单元测试结束...");
	}
	
	public void testDivideByZero() {
		Throwable te = null;
		try {
			operation.divide(6, 0);
			Assert.fail("测试失败");  //断言失败
		} catch (Exception e) {
			e.printStackTrace();
			te = e;
		}
		
		Assert.assertEquals(Exception.class, te.getClass());
		Assert.assertEquals("除数不能为 0 ", te.getMessage());
	}
}

4.2 JUnit 4.X

  • 使用 JUnit 4.X 版本进行单元测试时,不用测试类继承TestCase父类;
  • JUnit 4.X 版本,引用了注解的方式进行单元测试;
  • JUnit 4.X 版本我们常用的注解包括:
    • @Before注解:与 JUnit 3.X 中的setUp()方法功能一样,在每个测试方法之前执行,多用于初始化;
    • @After注解:与 JUnit 3.X 中的tearDown()方法功能一样,在每个测试方法之后执行,多用于释放资源;
    • @Test(timeout = xxx)注解:设置当前测试方法在一定时间内运行完,否则返回错误;
    • @Test(expected = Exception.class)注解:设置被测试的方法是否有异常抛出,抛出异常类型为Exception.class
/**
* 用 JUnit 4.X 进行测试
*/
import static org.junit.Assert.*;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class TestOperation {

    private Operation operation;

    @BeforeClass
    public static void globalInit() {  // 在所有方法执行之前执行
        System.out.println("@BeforeClass标注的方法,在所有方法执行之前执行...");
    }

    @AfterClass
    public static void globalDestory() {  // 在所有方法执行之后执行
        System.out.println("@AfterClass标注的方法,在所有方法执行之后执行...");
    }

    @Before
    public void setUp() {  // 在每个测试方法之前执行
        System.out.println("@Before标注的方法,在每个测试方法之前执行...");
        operation = new Operation();
    }

    @After
    public void tearDown() {  // 在每个测试方法之后执行
        System.out.println("@After标注的方法,在每个测试方法之后执行...");
    }

    @Test(timeout=600)
    public void testAdd() {  // 设置限定测试方法的运行时间 如果超出则返回错误
        System.out.println("测试 add 方法...");
        int result = operation.add(2, 3);
        assertEquals(5, result);
    }

    @Test
    public void testSubtract() {
        System.out.println("测试 subtract 方法...");
        int result = operation.subtract(1, 2);
        assertEquals(-1, result);
    }

    @Test
    public void testMultiply() {
        System.out.println("测试 multiply 方法...");
        int result = operation.multiply(2, 3);
        assertEquals(6, result);
    }

    @Test
    public void testDivide() {
        System.out.println("测试 divide 方法...");
        int result = 0;
        try {
            result = operation.divide(6, 2);
        } catch (Exception e) {
            fail();
        }
        assertEquals(3, result);
    }

    @Test(expected = Exception.class)
    public void testDivideAgain() throws Exception {
        System.out.println("测试 divide 方法,除数为 0 的情况...");
        operation.divide(6, 0);
        fail("test Error");
    }

    public static void main(String[] args) {
    }
}

4.3 特别提醒

通过以上两个例子,我们已经可以大致知道 JUnit 3.X 和 JUnit 4.X 两个版本的区别了。首先,如果我们使用 JUnit 3.X,那么在我们写的测试类的时候,一定要继承TestCase类,但是如果我们使用 JUnit 4.X,则不需继承TestCase类,直接使用注解即可。在 JUnit 3.X 中,还强制要求测试方法的命名为testXxxx这种格式;在 JUnit 4.X 中,则不要求测试方法的命名格式,但作者还是建议测试方法统一命名为testXxxx这种格式,简洁明了。

此外,在上面的两个示例中,我们只给出了测试类,但是在这之前,还应该有一个被测试类,也就是我们真正要实现功能的类。现在,作者将给出上面示例中被测试的类,即Operation类:

/**
* 定义了加减乘除的法则
*/
public class Operation {

    public static void main(String[] args) {
        System.out.println("a + b = " + add(1,2));
        System.out.println("a - b = " + subtract(1,2));
        System.out.println("a * b = " + multiply(1,2));
        System.out.println("a / b = " + divide(4,2));
        System.out.println("a / b = " + divide(1,0));
    }

    public static int add(int a, int b) {
        return a + b;
    }

    public static int subtract(int a, int b) {
        return a - b;
    }

    public static int multiply(int a, int b) {
        return a * b;
    }

    public static int divide(int a, int b) {
        return a / b;
    }
}

5 测试示例

5.1 示例一:简单的 JUnit 3.X 测试

import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import java.util.ArrayList;
import java.util.Collection;

/**
 * 1、创建一个测试类,继承TestCase类
 */
public class SimpleTestDemo extends TestCase {

    public SimpleTestDemo(String name) {
        super(name);
    }

    /**
     * 2、写一个测试方法,断言期望的结果
     */
    public void testEmptyCollection(){
        Collection collection = new ArrayList();
        assertTrue(collection.isEmpty());
    }

    /**
     * 3、写一个suite()方法,它会使用反射动态的创建一个包含所有的testXxxx方法的测试套件
     */
    public static Test suit(){
        return new TestSuite(SimpleTestDemo.class);
    }

    /**
     * 4、写一个main()方法,以文本运行器的方式方便的运行测试
     */
    public static void main(String[] args) {
        junit.textui.TestRunner.run(suit());
    }
}

5.2 示例二:套件测试

首先,介绍一下套件测试,简单来讲,测试套件是指:一些测试不同类的用例,可以使用@RunWith@Suite注解把所有的测试类套在一起,从而形成测试套件。如果有很多测试类,想让它们都运行在同一时间,而不是单一地运行每个测试,套件测试是非常有用的。当一个类被注解为@RunWith, JUnit 将调用其中的注解,以便运行测试类,而不使用内置的 JUnit 运行方法。

/**
* 待测试类
*/

import java.util.Arrays;

public class GotoWork {

    public String[] prepareSkills() {

        String[] skill = { "Java", "MySQL", "JSP" };
        System.out.println("My skills include : " + Arrays.toString(skill));
        return skill;
    }

    public String[] addSkills() {
        String[] skill = { "Java", "MySQL", "JSP", "JUnit" };
        System.out.println("Look, my skills include : " + Arrays.toString(skill));
        return skill;
    }
}
  • 测试类 1
import org.junit.Test;
import static org.junit.Assert.*;

public class PrepareSkillsTest {

    GotoWork gotoWork = new GotoWork();

    String[] skill = { "Java", "MySQL", "JSP" };

    @Test
    public void testPrepareSkills() {

        System.out.println("Inside testPrepareSkills()");
        assertArrayEquals(skill, gotoWork.prepareSkills());
    }
}
  • 测试类 2
import org.junit.Test;
import static org.junit.Assert.*;

public class AddSkillsTest {

    GotoWork gotoWork = new GotoWork();

    String[] skill = { "Java", "MySQL", "JSP", "JUnit" };

    @Test
    public void testAddSkills() {

        System.out.println("Inside testAddPencils()");
        assertArrayEquals(skill, gotoWork.addSkills());
    }
}
  • 套件测试
import org.junit.runner.RunWith;
import org.junit.runners.Suite;

@RunWith(Suite.class)
@Suite.SuiteClasses({ PrepareSkillsTest.class, AddSkillsTest.class })
public class SuitTest {

}

使用@Suite.SuiteClasses注解,可以定义测试类,将被列入执行,并且执行的顺序就是在@Suite.SuiteClasses注解中定义的顺序。

5.3 示例三:参数化测试

首先介绍一下参数化测试,一个测试类可以被看作是一个参数化测试类,当其满足下列所有要求:

  • 该类被注解为@RunWith(Parameterized.class)
  • 该类有一个构造函数,存储测试数据;
  • 该类有一个静态方法生成并返回测试数据,并标注@Parameters注解;
  • 该类有一个测试方法,即用注解@Test标注的方法。
public class Calculate {

    public int sum(int var1, int var2) {
        System.out.println("此方法的参数值分别为 : " + var1 + " + " + var2);
        return var1 + var2;
    }
}
  • 参数化测试类
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

@RunWith(Parameterized.class)
public class CalculateTest {

    private int expected;
    private int first;
    private int second;

    public CalculateTest(int expectedResult, int firstNumber, int secondNumber) {

        this.expected = expectedResult;
        this.first = firstNumber;
        this.second = secondNumber;
    }

    @Parameters
    public static Collection addedNumbers() {

        return Arrays.asList(new Integer[][] { { 3, 1, 2 }, { 5, 2, 3 }, { 7, 3, 4 }, { 9, 4, 5 }, });   
    }

    @Test
    public void testSum() {

        Calculate add = new Calculate();
        System.out.println("Addition with parameters : " + first + " and " + second);
        assertEquals(expected, add.sum(first, second));
    }
}

观察CalculateTest类,它满足上述所有的要求,因此它就可以称为一个参数化测试类。addedNumbers方法使用注释@Parameters返回数组的集合,每个数组包括每个测试执行输入和输出数字,每个数组中的元素数必须相同好与构造参数的个数相匹配。所以,在这种特定的情况下,每个数组包括三个元素,即表示要加入的两个元素和一个结果元素。

6 个人建议

有些童鞋可能会有一些误解,认为写测试代码没有用,而且还会增大自己的压力,浪费时间。但事实上,写测试代码与否,还是有很大区别的,如果是在小的项目中,或许这种区别还不太明显,但如果在大型项目中,一旦出现错误或异常,用人力去排查的话,那将会浪费很多时间,而且还不一定排查的出来,但是如果用测试代码的话,JUnit 就是自动帮我们判断一些代码的结果正确与否,从而节省的时间将会远远超过你写测试代码的时间。

因此,个人建议:要养成编写测试代码的习惯。在我们不断编写与测试代码的过程中,我们将会对类的行为有一个更为深入的了解,从而可以有效的提高我们的工作效率。下面,作者就给出一些具体的编写测试代码的技巧和较好的实践方法:

  1. 不要用TestCase的构造函数初始化Fixture,而要用setUp()tearDown()方法;
  2. 不要依赖或假定测试运行的顺序,因为 JUnit 会利用Vector保存测试方法,所以不同的平台会按不同的顺序从Vector中取出测试方法;
  3. 避免编写有副作用的TestCase,例如:如果随后的测试依赖于某些特定的交易数据,就不要提交交易数据,只需要简单的回滚就可以了;
  4. 当继承一个测试类时,记得调用父类的setUp()tearDown()方法;
  5. 将测试代码和工作代码放在一起,同步编译和更新;
  6. 测试类和测试方法应该有一致的命名方案,如在工作类名前加上test从而形成测试类名;
  7. 确保测试与时间无关,不要使用过期的数据进行测试,以至于导致在随后的维护过程中很难重现测试;
  8. 如果编写的软件面向国际市场,那么编写测试时一定要考虑国际化的因素;
  9. 尽可能地利用 JUnit 提供地assertfail方法以及异常处理的方法,其可以使代码更为简洁;
  10. 测试要尽可能地小,执行速度快;
  11. 不要硬性规定数据文件的路径;
  12. 使用文档生成器做测试文档。

事实上,在 Junit 中使用try catch来捕获异常是没有必要的,因为 Junit 会自动捕获异常,那些没有被捕获的异常就会被当成错误处理。


参考资料

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-01-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 1 简介
  • 2 特点
  • 3 内容
    • 3.1 注解
      • 3.2 断言
      • 4 JUnit 3.X 和 JUnit 4.X 的区别
        • 4.1 JUnit 3.X
          • 4.2 JUnit 4.X
            • 4.3 特别提醒
            • 5 测试示例
              • 5.1 示例一:简单的 JUnit 3.X 测试
                • 5.2 示例二:套件测试
                  • 5.3 示例三:参数化测试
                  • 6 个人建议
                  相关产品与服务
                  云数据库 MySQL
                  腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档