JUnit高效实践

一、概述

单元测试(Unit Testing)是指对软件中的最小可测试单元进行检查和验证,一般要根据实际情况去判定其具体含义,比如在 Java 语言中,单元可以指一个类或一个方法。JUnit 是一个 Java 语言简单、开源的单元测试框架,在 Java 开发中比较流行且常用的测试框架,希望读者能通过这篇文章来深入了解 JUnit 的特性与使用。

该文章使用的版本为 JUnit4

二、JUnit 基本用法

1. 测试运行器

JUnit 测试的运行器可以指定当前测试通过怎样的方式去运行,也可以配置运行的上下文,可以在类或父类上使用 @RunWith 注解来指定一个特定的运行器,而不是使用 JUnit 默认的运行器,很多第三方框架整合 JUnit 框架,一般都会有自定义的 Runner 接口实现。

@RunWith(JUnit4.class)
public class DemoTest {

    @Test
    public void assertTest() {
        // do something
    }
}

下面是 JUnit 专门提供的运行器,在后面我们会详细讲解到它们的用法:

  • Suite:可以手动构建多个类的测试套件的标准运行器
  • Parameterized:可以为测试方法以参数的形式提供测试数据的标准运行器
  • Categories:可以手动指定一组测试,配置包含或不包含一些测试方法或类的运行器

2. 断言与假设

2.1 断言

Java 提供了关键字 assert 来实现断言,断言功能可以通过 Java 运行参数来启用或者禁用,当断言判断失败时,会抛出 AssertionError 错误,它的用法如下:

assert condition;
assert condition : message;

JUnit 为所有基本类型、对象和数组类型提供了一组断言方法,这些方法都位于 Assert 类中,当断言判断失败时,会抛出 AssertionError 错误,示例代码如下:

public class DemoAssertTest {

    @Test
    public void assertTest() {
        int a = 5;
        assert a < 0;
        assert a < 0 : "整数为正数";
    }

    @Test
    public void assertJUnitTest() {
        int a = 2;
        Object expected = new Object();
        Object actual = new Object();
        assertTrue("条件为假", a > 1);
        assertFalse("条件为真", a > 4);
        assertEquals("整数与期望的数不相等", 100, 80);
        assertNotEquals("整数与期望的数相等", 100, 80);
        assertArrayEquals("", new Integer[]{1, 2, 3, 4, 5}, new Integer[]{1, 2, 3, 4, 5});
        assertNotNull("", actual);
        assertNull("", actual);
        assertSame("", expected, actual);
        assertNotSame("", expected, actual);
        assertThat(actual, is(expected));
    }
}

断言方法的错误提示消息参数都是可选的,一般断言方法的参数是布尔类型,来进行条件判断,也有判断实际值与期望值是否相符的。期望值不一定是确定的值,例如在 assertThat 方法中,参数 Matcher 接口提供了一个匹配的操作,用来判断实际值是否符合期望。

2.2 假设

假设与断言类似,它一般用于测试代码之前,判断测试的参数和执行环境条件,可以避免一些不关注的点来影响测试结果,Assume 类提供了与断言类似的方法,当假设判断失败时,会抛出 AssumptionViolatedException 异常,示例代码如下:

public class DemoAssumeTest {

    @Test public void filenameIncludesUsername() {
        // 假设前提条件
        assumeThat(File.separatorChar, is('/'));
        assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
    }

    @Test public void correctBehaviorWhenFilenameIsNull() {
        // 假设前提条件
        assumeTrue(bugFixed("13356"));  // bugFixed is not included in JUnit
        assertThat(parse(null), is(new NullDocument()));
    }

    @Test
    public void test() {
        int a = 2;
        Object object = new Object();
        assumeTrue("Assume true", true);
        assumeFalse("Assume false", false);
        assumeNotNull(object);
        assumeNoException("Assume no exception", new RuntimeException());
        assumeThat("Assume that", a, is(3));
    }
}

断言方法的错误提示消息参数都是可选的,与断言的方法参数类似,例如在 assertThat 方法中,参数 Matcher 接口提供了一个匹配的操作,用来判断实际值是否符合期望。

3. 套件测试

套件测试可以手动构建多个测试类一起运行,它使用 Suite 运行器来实现,假设我们存在如下三个测试类:

public class Test1 {

    @Test
    public void test() {
        System.out.println("Test1.test");
    }

    @Test
    public void test2() {
        System.out.println("Test1.test2");
    }
}

public class Test2 {

    @Test
    public void test() {
        System.out.println("Test2.test");
    }

    @Test
    public void test2() {
        System.out.println("Test2.test2");
    }
}

public class Test3 {

    @Test
    public void test() {
        System.out.println("Test3.test");
    }
}

新建一个测试类并使用 @RunWith(Suite.class) 注解来指定运行器,然后使用 @SuiteClasses(TestClass1.class, ...) 注解来指定包含的测试类,运行此类就会运行套件包含的所有测试类。

@RunWith(Suite.class)
@Suite.SuiteClasses({Test1.class, Test2.class, Test3.class})
public class DemoSuiteTest {
}

4. 分类测试

分类测试可以指定将某些测试类或测试方法进行分类,然后运行其中的某些类别的测试方法,或者不运行其中的某些类别的测试方法,它使用 Categories 运行器来实现,首先我们定义两个接口来标记类别:

public interface FastTests {
}

public interface SlowTests {
}

再定义两个测试类示例,使用 @Category 注解来标记测试类或测试方法的分类:

public class Test1 {

    @Test
    public void test() {
        System.out.println("Test1.test");
    }

    @Test
    @Category(FastTests.class)
    public void test2() {
        System.out.println("Test1.test2");
    }
}

@Category({FastTests.class, SlowTests.class})
public class Test2 {

    @Test
    public void test() {
        System.out.println("Test2.test");
    }

    @Test
    public void test2() {
        System.out.println("Test2.test2");
    }
}

分类测试是套件测试的扩展,运行器类也是其子类,所以我们在运行分类测试的时候,首先得使用 @Suite.SuiteClasses 注解来指定需要分类的所有测试类,然后使用 @RunWith(Categories.class) 来指定分类测试运行器,@Categories.IncludeCategory 注解指定运行的类别,@Categories.ExcludeCategory 指定不运行的类别,示例如下:

@RunWith(Categories.class)
@Categories.IncludeCategory(FastTests.class)
@Categories.ExcludeCategory(SlowTests.class)
@Suite.SuiteClasses({Test1.class, Test2.class, Test3.class})
public class DemoCategoriesTest {
}

5. 顺序测试

JUnit 支持指定测试方法的执行顺序,在一个测试类上,可以使用 @FixMethodOrder 注解来指定,通过选择不同的方法 Comparator 接口实现来选择执行顺序。

@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DemoOrderTest {

    @Test
    public void testB() {
        System.out.println("DemoOrderTest.testB");
    }

    @Test
    public void testA() {
        System.out.println("DemoOrderTest.testA");
    }

    @Test
    public void testC() {
        System.out.println("DemoOrderTest.testC");
    }
}

6. 忽略测试

在方法上面使用 @Ignore 来指定忽略的测试方法,也可以指明忽略的理由。

public class DemoIgnoreTest {

    @Test
    @Ignore("This test method is ignored.")
    public void test() {
        System.out.println("DemoIgnoreTest.test");
    }

    @Test
    public void test2() {
        System.out.println("DemoIgnoreTest.test2");
    }
}

7. 超时测试

在测试方法上的 @Test 注解上添加 timeout 属性可以指定该测试方法的执行超时时间,如果测试方法执行的耗时超过了该时间,则会抛出 TestTimedOutException 异常。

public class DemoTimeoutTest {

    @Test(timeout = 100)
    public void test() throws InterruptedException {
        TimeUnit.SECONDS.sleep(3);
        System.out.println("DemoTimeoutTest.test");
    }
}

8. 参数测试

在测试的过程中,我们经常会给定一些测试数据,来跑单元测试方法,这在测试场景中非常有用。参数测试使用 Parameterized 运行器来实现,首先定义一个静态方法,并且使用 @Parameters 注解,静态方法的返回值是数组或者列表,然后对应数据在测试类上定义字段,使用构造方法来注入测试数据。

@RunWith(Parameterized.class)
public class DemoParameterTest {

    private int input1;
    private int input2;

    public DemoParameterTest(int input1, int input2) {
        this.input1 = input1;
        this.input2 = input2;
    }

    @Parameters
    public static Integer[][] data() {
        return new Integer[][]{
                {1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
        };
    }

    @Test
    public void test() {
        System.out.println("DemoParameterTest.test::" + input1 + "-" + input2);
    }
}

除了使用构造方法外,还可以在字段上面使用 @Parameter 注解来注入测试数据,在注解的属性中指定数据列与字段的对应,注意字段的访问权限必须是 public

@RunWith(Parameterized.class)
public class DemoParameterTest {

    @Parameter
    public int input1;
    @Parameter(1)
    public int input2;

    @Parameters
    public static Integer[][] data() {
        return new Integer[][]{
                {1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
        };
    }

    @Test
    public void test() {
        System.out.println("DemoParameterTest.test::" + input1 + "-" + input2);
    }
}

有多少组数据,测试方法就会执行多少次,为了区分每个测试数据执行的展示结果,可以在 @Parameters 注解上使用 name 属性来指定测试结果命名,命名的规则如下:

  • {index}:当前参数的索引
  • {0}, {1}, …:第1个,第2个等等的参数值
@Parameters(name = "{index}:{0}-{1}")
public static Integer[][] data() {
    return new Integer[][]{
        {1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
    };
}

例如上面示例在 IDEA 中执行的结果如下:
在这里插入图片描述

9. 理论测试

和参数测试类似,我们也可以使用理论测试来指定测试数据。理论测试使用 Theories 运行器来实现,可以在测试类中定义常量字段来指定测试数据,在字段上使用 @DataPoint 注解来标记,然后在测试方法定义和字段类型一致的参数就能注入数据。

@RunWith(Theories.class)
public class DemoTheoriesTest {

    @DataPoint
    public static String GOOD_USERNAME = "CodeArtist";
    @DataPoint
    public static String USERNAME_WITH_SLASH = "Hello CodeArtist";

    @Theory
    public void test(String str) {
        System.out.println("DemoTheoriesTest.test::" + str);
    }
}

也可以直接在测试方法的参数上使用 @TestedOn 来指定数据,如果有多个参数,会运行参数组合的每一种情况。

@RunWith(Theories.class)
public class DemoTheoriesTest {

    @Theory
    public void test(@TestedOn(ints = {1, 2, 3}) int input1,
                     @TestedOn(ints = {11, 22, 33}) int input2) {
        System.out.println("DemoTheoriesTest.test::" + input1 + "-" + input2);
    }
}

除了前面的方法指定数据外,JUnit 也支持自定义注解来指定数据。自定义一个注解如下:

@Target(PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {

    int first();

    int last();
}

实现 ParameterSupplier 接口编写自定义注解的规则实现如下:

public class BetweenSupplier extends ParameterSupplier {

    @Override
    public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable {
        List<PotentialAssignment> list = new ArrayList<>();
        Between between = sig.getAnnotation(Between.class);
        for (int i = between.first(); i <= between.last(); i++) {
            list.add(PotentialAssignment.forValue("between", i));
        }
        return list;
    }
}

使用自定义的注解来指定参数,也会运行所有参数组合的情况。

@RunWith(Theories.class)
public class DemoTheoriesTest {

    @Theory
    public void test(@Between(first = 1, last = 3) int input1,
                     @Between(first = 4, last = 6) int input2) {
        System.out.println("DemoTheoriesTest.test::" + input1 + "-" + input2);
    }
}

10. 测试规则

规则可以为测试类中的每个测试方法灵活地添加扩展或重定义测试方法的行为,开发人员可以使用 JUnit 内置规则或自定义测试规则,方法级规则在规则字段上使用 @Rule 注解来指定,类级规则在规则字段上使用 @ClassRule 来指定。

10.1 内置规则
ExternalResource

ExternalResource 规则类是一个抽象类,可以为测试环境提供外部资源的访问,比如在测试前建立一个访问(文件、套接字、服务、数据库连接等等),在测试后关闭一个访问。

public class DemoExternalResourceTest {

    @Rule
    public final ExternalResource externalResource = new ExternalResource() {
        @Override
        protected void before() {
            System.out.println("DemoExternalResourceTest.before");
        }

        @Override
        protected void after() {
            System.out.println("DemoExternalResourceTest.after");
        }
    };

    @Test
    public void test() {
        System.out.println("DemoExternalResourceTest.test");
    }
}
TemporaryFolder

TemporaryFolder 规则继承了 ExternalResource 抽象类,实现了在测试过程中创建临时文件或文件夹的功能,在测试过程中创建的文件都会在测试后自动删除。

public class DemoTemporaryFolderTest {

    @Rule
    public final TemporaryFolder folder = new TemporaryFolder();

    @Test
    public void test() throws IOException {
        File createdFile = folder.newFile("codeartist.txt");
        File createdFolder = folder.newFolder("CodeArtist");
        System.out.println("DemoTemporaryFolderTest.test#createdFile: " + createdFile);
        System.out.println("DemoTemporaryFolderTest.test#createdFolder: " + createdFolder);
    }
}
Verifier

Verifier 规则类是一个抽象类,它可以在测试方法运行完后执行一些逻辑,所以可以用来校验测试的结果。

public class DemoVerifierTest {

    private static String result = "";

    @Rule
    public final Verifier verifier = new Verifier() {
        @Override
        protected void verify() {
            result += "verify";
        }
    };

    @Test
    public void test() {
        System.out.println("DemoVerifierTest.test::" + result);
    }

    @Test
    public void test2() {
        System.out.println("DemoVerifierTest.test::" + result);
    }
}
ErrorCollector

ErrorCollector 规则继承了 Verifier 抽象类,它是一个错误收集器,在测试方法运行时,遇到第一个异常会继续执行,先收集所有的异常然后一次性报告异常信息。

public class DemoErrorCollectorTest {

    @Rule
    public final ErrorCollector collector = new ErrorCollector();

    @Test
    public void test() {
        try {
            System.out.println("DemoErrorCollectorTest.test::First exception");
            int a = 1 / 0;
        } catch (Exception e) {
            collector.addError(e);
        }
        try {
            System.out.println("DemoErrorCollectorTest.test::Second exception");
            String a = null;
            System.out.println(a.getBytes());
        } catch (Exception e) {
            collector.addError(e);
        }
    }
}
TestWatcher

TestWatcher 规则类是一个抽象类,它可以监视一个测试方法执行的生命周期动作,但不能修改,例如可以在测试开始时、成功执行时、执行结束时添加逻辑。

public class DemoTestWatcherTest {

    @Rule
    public final TestWatcher watcher = new TestWatcher() {
        @Override
        protected void starting(Description description) {
            System.out.println("DemoTestWatcherTest.starting");
        }

        @Override
        protected void succeeded(Description description) {
            System.out.println("DemoTestWatcherTest.succeeded");
        }

        @Override
        protected void finished(Description description) {
            System.out.println("DemoTestWatcherTest.finished");
        }
    };

    @Test
    public void test() {
        System.out.println("DemoTestWatcherTest.test");
    }
}
TestName

TestName 规则可以获取当前运行的测试方法的名称。

public class DemoTestNameTest {

    @Rule
    public final TestName testName = new TestName();

    @Test
    public void test() {
        System.out.println("DemoTestNameTest.test::" + testName.getMethodName());
    }
}
Timeout

Timeout 规则可以指定测试类中所有测试方法的超时时间。

public class DemoTimeoutTest {

    @Rule
    public final Timeout timeout = Timeout.seconds(1);

    @Test
    public void test() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("DemoTimeoutTest.test");
    }
}
ExpectedException

ExpectedException 规则可以指定测试方法抛出预期的异常或存在预期的异常消息描述,如果没有达到预期会抛出 AssertionError

public class DemoExpectedExceptionTest {

    @Rule
    public final ExpectedException exception = ExpectedException.none();

    @Test
    public void test() {
        exception.expect(NullPointerException.class);
        exception.expectMessage("happened?");
        exception.expectMessage(startsWith("What"));
        throw new NullPointerException("What happened?");
    }
}
ClassRule

@ClassRule 注解扩展了 @Rule 的方法级规则,在定义的规则字段上需要添加 static 修饰,类级别规则在测试类运行时执行,与测试方法无关。

public class DemoClassRuleTest {

    @ClassRule
    public static final ExternalResource r = new ExternalResource() {
        @Override
        protected void before() {
            System.out.println("DemoClassRuleTest.before");
        }

        @Override
        protected void after() {
            System.out.println("DemoClassRuleTest.after");
        }
    };

    @Test
    public void test() {
        System.out.println("DemoClassRuleTest.test");
    }

    @Test
    public void test2() {
        System.out.println("DemoClassRuleTest.test2");
    }

}
RuleChain

RuleChain 规则可以定义规则链,也就是说可以在当前测试类中定义多个规则。

public class DemoRuleChainTest {

    @Rule
    public final RuleChain chain = RuleChain
            .outerRule(new TestLogger("outer rule"))
            .around(new TestLogger("around1 rule"))
            .around(new TestLogger("around2 rule"));

    @Test
    public void test() {
        System.out.println("DemoRuleChainTest.test");
    }
}
10.2 自定义规则

我们通过实现 TestRule 接口来自定义规则,也可以继承内置的基础规则抽象类来实现。

public class TestLogger implements TestRule {

    private final String message;

    public TestLogger(String message) {
        this.message = message;
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                System.out.println("TestLogger.evaluate::" + message);
                base.evaluate();
            }
        };
    }
}

11. 测试卡具

测试卡具可以在测试方法执行的前面或者后面执行一些逻辑,@Before 注解修饰的方法在所有测试方法执行前都会执行,相反 @After 注解修饰的方法在所有测试方法执行后都会执行,它们是方法级别的,而 @BeforeClass@AfterClass 注解修饰的方法是类级别的,方法必须添加 static 修饰,而且不管测试类中存在多个测试方法,它们在测试方法执行前或执行后只执行一次。

public class DemoFixtureTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("DemoFixtureTest.beforeClass");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("DemoFixtureTest.afterClass");
    }

    @Before
    public void before() {
        System.out.println("DemoFixtureTest.before");
    }

    @After
    public void after() {
        System.out.println("DemoFixtureTest.after");
    }

    @Test
    public void test() {
        System.out.println("DemoFixtureTest.test");
    }

    @Test
    public void test2() {
        System.out.println("DemoFixtureTest.test2");
    }
}

测试卡具可以实现下面功能:

  • 在测试前准备输入数据,或配置 Mock 对象
  • 加载一个特定的数据库来准备数据
  • 为某些测试类创建一些初始化状态

三、JUnit 第三方扩展

一些流行的第三方实现运行器使用 @RunWith 注解来指定:

JUnit4 已经是老版本的 JUnit 测试框架,在后面的 JUnit5 中新增了很多新特性,而且简化了单元测试的写法,功能更为强大,掌握了 JUnit4 后,迁移和学习 JUnit5 也会很轻松,读者可以去参考官方文档。



参考文献

JUnit4 官方文档:https://junit.org/junit4/

JUnit5 官方文档:https://junit.org/junit5/docs/current/user-guide/

示例代码

码云:https://gitee.com/code_artist/spring




查看更多教程请关注码匠公众号:CodeArtist

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码匠_CodeArtist

指出错误和提出建议也是一种打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值