1. 概述

在这个简短的教程中,我们将讨论JUnit 5中的@Nested注解。我们将首先通过一个简单的示例来理解嵌套测试类的工作方式,然后学习如何利用这个新特性处理更像生产环境的用例。

2. @Nested如何工作

我们可以使用JUnit 5的@Nested注解创建嵌套测试类。这个注解需要添加在每个包含测试的内部类的类级别上:

public class NestedTest {
    @Nested
    class FirstNestedClass {
        @Test
        void test() {
            System.out.println("FirstNestedClass.test()");
        }
    }

    @Nested
    class SecondNestedClass {
        @Test
        void test() {
            System.out.println("SecondNestedClass.test()");
        }
    }
}

因此,当我们运行父测试类时,所有嵌套测试类中的测试都会被执行。大多数IDE会以美观的层次结构显示测试:

IntelliJ中嵌套测试

此外,如果我们添加了设置或清理方法,它们会在声明时执行。例如,如果我们在三个类中分别为每个类添加一个@BeforeEach方法,我们期望每个测试执行父类的设置方法,然后是它自己的类,最后执行测试本身:

public class NestedTest {
    @BeforeEach()
    void beforeEach() {
        System.out.println("NestedTest.beforeEach()");
    }

    @Nested
    class FirstNestedClass {
        @BeforeEach()
        void beforeEach() {
            System.out.println("FirstNestedClass.beforeEach()");
        }

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

    @Nested
    class SecondNestedClass {
        @BeforeEach()
        void beforeEach() {
            System.out.println("SecondNestedClass.beforeEach()");
        }

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

让我们运行测试类并检查控制台中打印语句的顺序:

嵌套测试执行

正如预期的那样,两个测试都使用了由父类定义的通用设置。然后,它们运行自己类的设置方法,再执行测试。另外,其他设置和清理方法,如@BeforeAll@AfterEach@AfterAll遵循相同的模式。

3. 何时使用@Nested

如果我们有一些测试的设置或清理方法在某些测试中重复,但不适用于所有测试,那么使用嵌套测试类非常有用。

此外,将测试集的设置用嵌套类表示,可以导致更具表达性的测试场景,并明确测试之间的关系。

3.1. 重用测试场景

让我们使用@Nested注解为更复杂的情况编写一些测试。

假设我们在测试一个在线出版物的后端应用。该出版物的客户端可以有三种类型的会员资格:

public enum Membership {
    FREE, SILVER, GOLD;
}

根据他们的会员资格,用户可以阅读或查看文章,或者将它们视为锁定状态。

首先,我们创建一个Publication和三个Article对象:

class OnlinePublicationTest {
    private Publication publication;

    @BeforeEach
    void setupArticlesAndPublication() {
        Article freeArticle = new Article("free article", Membership.FREE);
        Article silverArticle = new Article("silver level article", Membership.SILVER);
        Article goldArticle = new Article("gold level article", Membership.GOLD);
        publication = new Publication(Arrays.asList(freeArticle, silverArticle, goldArticle));
    }

    @Test
    void shouldHaveThreeArticlesInTotal() {
        List<Article> allArticles = publication.getArticles();
        assertThat(allArticles).hasSize(3);
    }
}

现在,假设一个免费会员用户查看出版物。我们期望他只能阅读一篇,而其他两篇被视为“锁定”。让我们在一个嵌套测试类中分别写出这两个不同的单元测试:

@Nested
class UserWithoutMembership {
    User freeFreya = new User("Freya", Membership.FREE);

    @Test
    void shouldOnlyReadFreeArticles() {
        List<String> articles = publication.getReadableArticles(freeFreya);
        assertThat(articles).containsExactly("free article");
    }

    @Test
    void shouldSeeSilverAndGoldLevelArticlesAsLocked() {
        List<String> articles = publication.getLockedArticles(freeFreya);
        assertThat(articles).containsExactlyInAnyOrder("silver level article", "gold level article");
    }
}

现在,为“银”级和“金”级会员用户运行类似的测试类,并运行整个类:

在线出版物测试场景

3.2. 使用带有@DisplayName的嵌套测试

值得注意的是,类名和测试方法的名称一起描述了测试的整体场景。同样,我们可以在测试类和方法上使用@DisplayName注解来进一步定制它们的显示方式。例如,我们可以使用“给定-当-然后”模式:

带有显示名称的嵌套测试

4. 总结

在这篇短文中,我们了解了JUnit 5的@Nested注解的工作原理以及如何使用它创建具有表达力的测试场景。我们了解到,当多个测试的设置或清理相似,但并不适用于类中的所有测试时,我们可以创建一个嵌套测试类。

一如既往,本文的完整源代码可在GitHub上找到。