Java 单元测试利器 JUnit

本贴最后更新于 2033 天前,其中的信息可能已经时移世易

单元测试是软件开发中非常重要的一个环节,对我们编程人员来说,要对自己的代码负责,单元测试是校验代码行为是否符合期望的有效手段。JUnit 是用 Java 语言编写的一个单元测试工具,它具有非常强大的功能,使用起来也非常方便。

1. JUnit 快速入门

如果你是使用 maven 管理项目,那么只需在 pom 文件添加下面的依赖即可引入 junit 包。

<dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.11</version>
</dependency>

下面我们开始介绍如何使用 JUnit 进行测试吧。下面是一段代码:这是一个非常简单的工具类,用来处理字符串的。

public class StringUtils {
    /**
     * 将大写字符串全部转换为小写
     * @param str 要转换的字符串
     * @return 转换后的字符串
     */
    public static String toLowerCase(String str){
        if(str==null){
            throw new NullPointerException();
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            char ch=str.charAt(i);
            if(ch>='a' && ch <= 'z'){
                sb.append(ch);
            } else if(ch>='A' && ch<='Z'){
                sb.append((char)(ch+32));
            }else {
                throw new RuntimeException("字母必须在[a-z]和[A-Z]中");
            }
        }
        return sb.toString();
    }
}

现在我们开始编写单元测试代码,对上面的 toLowerCase 方法进行校验。

public class TestStringUtils {

    /**
     * 测试大写字符组成的字符串
     */
    @Test
    public void testUpperCase(){
        Assert.assertEquals("username",StringUtils.toLowerCase("USERNAME"));

    }

    /**
     * 测试小写字符组成的字符串
     */
    @Test
    public void testLowerCase(){
        Assert.assertEquals("username",StringUtils.toLowerCase("username"));
    }

    /**
     * 测试大写和小写字符组成的字符串
     */
    @Test
    public void testLowerAndUpperCase(){
        Assert.assertEquals("username",StringUtils.toLowerCase("userNAME"));
    }
    
    /**
     * 测试大写以外字符组成的字符串,是否抛出RuntimeException异常
     */
    @Test(expected = RuntimeException.class)
    public void testNonLetter(){
        StringUtils.toLowerCase("user123NAME");
    }
}

测试类建议以 Test 开头,这样可以很明显识别出这是一个测试类。上面我们创建了一个测试类 TestStringUtils,包含四个使用**@Test**注解的方法,用来测试 toLowerCase 方法。

分别运行上述三个测试方法,就能看到测试通过了,说明我们的代码没有错误。

请牢记一条 JUnit 最佳实践:测试任何可能的错误。单元测试不是用来证明您是对的,而是为了证明您没有错。

测试方法的书写规范:

  • (1). 测试方法必须使用 org.junit.Test 注解
  • (2). 测试方法必须使用 public void 修饰,而且不能有任何参数

细心的同学可能会发现,测试方法 testNonLetter 的注解 @Test 多了一个 expected 参数 ,下面我们深入研究一下 junit 的高级用法。

2. JUnit 高级用法

2.1 异常和时间测试

注解 org.junit.Test 有两个很有用的参数:expected 和 timeout。

参数 expected 代表测试方法期望抛出特定的异常,如果运行测试没有抛出该异常,那么 JUnit 就会告诉我们测试失败,这为我们验证异常情况是否抛出指定异常提供了便利。比如在上面的例子中,我们测试了在传入大小写以外的字符时,是否抛出 RuntimeException 异常。

参数 timeout 用于指定测试方法能运行的最长时间是多少(单位:毫秒),如果超过指定时间测试方法仍未结束,那么 JUnit 就会告诉我们测试失败,该参数对于性能测试会有一定的帮助。

2.2 @Before 和 @After 注解

我们经常会碰到这样一种情况,多个测试方法需要一些公共的资源,而我们又不想重复编写代码来处理这些公共资源,这会造成大量重复代码。那 JUnit 有没有为我们提供什么方式来处理这种情况呢,答案是 yes,@Before 和 @After 就是用来做这事的,设置公共资源。

@Before 注解的方法会在 @Test 注解的方法之前运行,因此我们可以使用该注解做一些初始化操作

@After 注解的方法会在 @Test 注解的方法之后运行,因此我们可以用该注解做一些清理工作。

注意:@Before 注解的方法会在每个 @Test 注解的方法之前运行,@After 注解的方法会在每个 @Test 注解的方法之后运行

@Before 和 @After 注解的方法必须使用 public void 修饰,而且不能带有任何参数。

2.3 @BeforeClass 和 @AfterClass 注解

和 @Before 和 @After 类似,@BeforeClass 和 @AfterClass 注解也是用来设置公共资源的。不同的是,@BeforeClass 注解的方法在当前测试类中所有方法之前运行,@AfterClass 注解的方法在当前测试类中所有方法之后运行,并且都只运行一次。这在设置一些耗时的公共资源时非常有用(比如说数据库连接),提升测试效率。

2.4 测试运行器

JUnit 中所有的测试方法都是有测试运行器来负责的,JUnit 提供了默认的测试运行器,但是我们也可以通过继承 org.junit.runner.Runner 自定义测试运行器。

在运行一个测试方法时,如果没有显示指定测试运行器,那么 JUnit 就使用默认的测试运行器。我们可以使用 @RunWith 注解显示指定测试运行器,比如下面一段代码

@RunWith(CustomRunner.class)
public class TestStringUtils {
}

2.5 测试套件

在实际项目中,我们编写的测试类肯定不只一个,当测试类的数量很多时,一个一个测试肯定时不现实的。JUnit 提供了一种批量运行测试类的方法:测试套件。我们可以在一个测试套件中添加多个测试类,一次运行多个测试类。定义测试套件规则如下:

  • (1). 创建一个空类,使用 @RunWith 和 @Suite.SuiteClasses 注解修饰,作为测试套件的入口
  • (2). 使用 org.junit.runners.Suite 作为测试运行器
  • (3). 将测试类组成的数组作为 @Suite.SuiteClasses 的参数
  • (4). 必须使用 public 修改该空类,且不能存在带有参数的构造函数
@RunWith(Suite.class)
@Suite.SuiteClasses({FirstTest.class,SecondTest.class})
public class MyTestSuit {
}

上面我们定义了一个测试套件类 MyTestSuit,包含两个测试类:FristTest 和 SecondTest。直接运行该类,就会看到 FristTest 和 SecondTest 方法中的测试类都被运行了。

2.6 参数化测试

回顾一下我们的测试类 TestStringUtils 中的前三个方法,他们代码结构基本相同,除了期望值和目标值不同以外。那么我们是否有一种更好的方法,能把代码中相同的结构抽离出来,减少代码的冗余量呢。从 JUnit4.0 开始引入了参数化测试概念,很好地解决了我们刚才提到的问题。

定义参数化测试类的过程如下:

  • (1). 定义一个测试类,使用 @RunWith 注解修饰,其参数值为 Parameterized.class
  • (2). 在该测试类中声明一个 public static 修饰的方法,其返回值是 Collection,并在该方法中初始化需要测试的参数对
  • (3). 在该测试类中声明几个变量,用于保存期望数据和实际的测试数据
  • (4). 为该类声明一个 public 修饰的有参的构造参数,并在其中为(3)中声明的变量赋值
  • (5). 编写测试方法,使用(3)中定义的变量进行测试
@RunWith(Parameterized.class)
public class TestStringUtilsParameter {
    private String expected;
    private String actual;

    @Parameterized.Parameters
    public static Collection param(){
       return Arrays.asList(new String[][]{{"username","USERNAME"},{"username","username"},{"username","userName"}});
    }

    public TestStringUtilsParameter(String expected, String actual) {
        this.expected = expected;
        this.actual = actual;
    }

    @Test
    public void testStringUtils(){
        Assert.assertEquals(expected,StringUtils.toLowerCase(actual));
    }
}

看一下上面这个测试类和 TestStringUtils 相比,是不是简洁多了,没有冗余的代码结构,而且当我们想要增加测试情况时,只需要添加相应的数组即可。

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...