个性化阅读
专注于IT技术分析

JUnit健壮的单元和集成测试指南

点击下载

本文概述

自动化软件测试对于软件项目的长期质量, 可维护性和可扩展性至关重要, 对于Java, JUnit是实现自动化的途径。

尽管本文的大部分内容将重点放在编写健壮的单元测试以及利用存根, 模拟和依赖注入, 但我们还将讨论JUnit和集成测试。

JUnit测试框架是用于测试基于Java的项目的通用, 免费和开源工具。

在撰写本文时, JUnit 4是当前的主要发行版, 已经发布了10多年, 而最近的更新是在两年前。

JUnit 5(具有Jupiter编程和扩展模型)正在积极开发中。它更好地支持Java 8中引入的语言功能, 并包括其他有趣的新功能。有些团队可能会发现可以使用JUnit 5, 而其他团队可能会继续使用JUnit 4, 直到5正式发布为止。我们将看看这两个例子。

运行JUnit

JUnit测试可以直接在IntelliJ中运行, 但是它们也可以在其他IDE(例如Eclipse, NetBeans甚至命令行)中运行。

测试应始终在构建时运行, 尤其是单元测试。不管问题是在生产环境中还是在测试代码中, 具有任何失败测试的构建都应被视为失败–这需要团队的纪律和愿意将解决失败测试的优先权放在首位, 但是必须坚持自动化的精神。

JUnit测试也可以由诸如Jenkins之类的持续集成系统运行并报告。使用Gradle, Maven或Ant之类的工具的项目具有的额外优势是, 能够在构建过程中运行测试。

JUnit可以与Gradle,Maven,Netbeans,Ant等一起运行。

摇篮

作为JUnit 5的示例Gradle项目, 请参见JUnit用户指南的Gradle部分和junit5-samples.git存储库。请注意, 它还可以运行使用JUnit 4 API的测试(称为”年份”)。

可以通过菜单选项文件>打开…>导航到junit-gradle-consumer子目录>确定>以项目打开>确定从Gradle中导入项目, 从而在IntelliJ中创建项目。

对于Eclipse, 可以从”帮助”>” Eclipse Marketplace”安装Buildship Gradle插件。然后可以使用”文件”>”导入”>” Gradle”>” Gradle项目”>”下一步”>”下一步”>导入项目, 然后浏览到junit-gradle-consumer子目录>”下一步”。 >下一步>完成。

在IntelliJ或Eclipse中设置Gradle项目之后, 运行Gradle构建任务将包括运行所有带有测试任务的JUnit测试。请注意, 如果未对代码进行任何更改, 则可能在以后的构建执行时跳过测试。

对于JUnit 4, 请参阅JUnit与Gradle Wiki一起使用。

马文

对于JUnit 5, 请参阅用户指南的Maven部分和junit5-samples.git存储库, 以获取Maven项目的示例。这也可以运行老式测试(使用JUnit 4 API的测试)。

在IntelliJ中, 使用文件>打开…>导航到junit-maven-consumer / pom.xml>确定>以项目形式打开。然后可以从Maven项目> junit5-maven-consumer>生命周期>测试中运行测试。

在Eclipse中, 使用文件>导入…> Maven>现有Maven项目>下一步>浏览到junit-maven-consumer目录>选择pom.xml>完成。

可以通过在Maven构建中运行项目来执行测试…>指定测试目标>运行。

对于JUnit 4, 请参阅Maven存储库中的JUnit。

开发环境

除了通过Gradle或Maven之类的构建工具运行测试之外, 许多IDE都可以直接运行JUnit测试。

IntelliJ IDEA

JUnit 5测试需要IntelliJ IDEA 2016.2或更高版本, 而JUnit 4测试应在较旧的IntelliJ版本中运行。

出于本文的目的, 你可能想从我的一个GitHub存储库(JUnit5IntelliJ.git或JUnit4IntelliJ.git)中的IntelliJ中创建一个新项目, 其中包括简单Person类示例中的所有文件并使用内置文件。 JUnit库。可以通过运行>运行”所有测试”来运行测试。该测试还可以从PersonTest类的IntelliJ中运行。

这些存储库是使用新的IntelliJ Java项目创建的, 并构建了目录结构src / main / java / com / example和src / test / java / com / example。 src / main / java目录被指定为源文件夹, 而src / test / java被指定为测试源文件夹。在使用带@Test注释的测试方法创建PersonTest类之后, 它可能无法编译, 在这种情况下, IntelliJ提供了将JUnit 4或JUnit 5添加到可以从IntelliJ IDEA发行版加载的类路径的建议。有关堆栈溢出的详细信息, 请参见答案)。最后, 为所有测试添加了JUnit运行配置。

另请参阅《 IntelliJ测试操作指南》。

日食

Eclipse中的空Java项目将没有测试根目录。这是从项目属性> Java构建路径>添加文件夹…>创建新文件夹…>指定文件夹名称>完成中添加的。新目录将被选择为源文件夹。在其余两个对话框中单击”确定”。

可以使用文件>新建> JUnit测试用例创建JUnit 4测试。选择” New JUnit 4 test”和新创建的源文件夹进行测试。指定”被测类”和”包”, 确保包与被测类匹配。然后, 为测试类指定一个名称。完成向导后, 如果出现提示, 请选择”将JUnit 4库”添加到构建路径。然后可以将项目或单个测试类作为JUnit测试运行。另请参阅Eclipse编写和运行JUnit测试。

NetBeans

NetBeans仅支持JUnit 4测试。可以在NetBeans Java项目中使用文件>新建文件…>单元测试> JUnit测试或现有类测试来创建测试类。默认情况下, 测试根目录在项目目录中名为test。

简单生产类及其JUnit测试用例

我们来看一个非常简单的Person类的生产代码及其对应的单元测试代码的简单示例。你可以从我的github项目下载示例代码, 然后通过IntelliJ打开它。

src / main / java / com / example / Person.java
package com.example;

class Person {
   private final String givenName;
   private final String surname;

   Person(String givenName, String surname) {
       this.givenName = givenName;
       this.surname = surname;
   }

   String getDisplayName() {
       return surname + ", " + givenName;
   }
}

不可变的Person类具有构造函数和getDisplayName()方法。我们要测试getDisplayName()是否返回我们期望的格式化名称。这是单个单元测试(JUnit 5)的测试代码:

src / test / java / com / example / PersonTest.java
package com.example;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class PersonTest {

   @Test
   void testGetDisplayName() {
       Person person = new Person("Josh", "Hayden");
       String displayName = person.getDisplayName();
       assertEquals("Hayden, Josh", displayName);
   }
}

PersonTest使用JUnit 5的@Test和assert。对于JUnit 4, PersonTest类和方法必须是公共的, 并且应使用不同的导入。这是JUnit 4示例Gist。

在IntelliJ中运行PersonTest类时, 测试通过并且UI指示器为绿色。

通用JUnit约定

命名

尽管不是必需的, 但我们在命名测试类时使用了通用约定。具体来说, 我们以要测试的类的名称(Person)开始, 并在其后附加” Test”(PersonTest)。测试方法的命名是相似的, 从要测试的方法(getDisplayName())开始, 然后在它前面加上” test”(testGetDisplayName())。尽管有许多其他完全可接受的约定来命名测试方法, 但在整个团队和项目中保持一致很重要。

JUnit测试的一些命名约定。

配套

我们还采用了在与生产代码的Person类相同的包(com.example)中创建测试代码PersonTest类的约定。如果我们使用不同的程序包进行测试, 则即使在不合适的地方, 也需要在生产代码类, 构造函数和单元测试引用的方法中使用公共访问修饰符, 因此最好将它们保留在同一程序包中。但是, 我们确实使用单独的源目录(src / main / java和src / test / java), 因为我们通常不希望在已发布的生产版本中包含测试代码。

结构和注释

@Test批注(JUnit 4/5)告诉JUnit将testGetDisplayName()方法作为测试方法执行, 并报告其通过还是失败。只要所有断言(如果有)通过且不引发任何异常, 则认为该测试通过。

我们的测试代码遵循”安排行为声明”(AAA)的结构模式。其他常见的模式包括”何时给出”和”设置-锻炼-验证-拆除”(单元测试通常通常不需要拆除), 但是本文使用AAA。

我们将遵循"排列-作用-断言"模式。

让我们看看我们的测试示例如何遵循AAA。第一行, ” arrange”创建将要测试的Person对象:

       Person person = new Person("Josh", "Hayden");

第二行” act”执行生产代码的Person.getDisplayName()方法:

       String displayName = person.getDisplayName();

第三行”断言”验证结果是否符合预期。

       assertEquals("Hayden, Josh", displayName);

在内部, assertEquals()调用使用” Hayden, Josh” String对象的equals方法来验证生产代码(displayName)返回的实际值是否匹配。如果不匹配, 则测试将被标记为失败。

请注意, 对于这些AAA阶段中的每个阶段, 测试通常有多个行。

单元测试和生产代码

既然我们已经介绍了一些测试约定, 那么我们将注意力转移到使生产代码可测试上。

我们返回到Person类, 在该类中, 我实现了一种方法来根据一个人的出生日期返回其年龄。这些代码示例需要Java 8才能利用新的日期和功能性API。这是新的Person.java类的样子:

Person.java
// ...
class Person {
    // ...
    private final LocalDate dateOfBirth;

    Person(String givenName, String surname, LocalDate dateOfBirth) {
        // ...
        this.dateOfBirth = dateOfBirth;
    }

    // ...

    long getAge() {
        return ChronoUnit.YEARS.between(dateOfBirth, LocalDate.now());
    }

    public static void main(String... args) {
        Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12"));
        System.out.println(person.getDisplayName() + ": " + person.getAge() + " years");
        // Doe, Joey: 4 years
    }
}

上课(在撰写本文时)表明Joey今年4岁。让我们添加一种测试方法:

PersonTest.java
// ...
class PersonTest {
    // ...

    @Test
    void testGetAge() {
        Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12"));
        long age = person.getAge();
        assertEquals(4, age);
    }
}

它今天就过去了, 但是从现在起一年后运行呢?由于预期结果取决于运行测试的系统的当前日期, 因此该测试是不确定性和易碎性的。

存根和注入价值供应商

在生产环境中运行时, 我们希望使用当前日期LocalDate.now()来计算人员的年龄, 但是要进行确定性测试, 甚至是从现在开始的一年, 测试都需要提供自己的currentDate值。

这称为依赖注入。我们不希望我们的Person对象确定当前日期本身, 而是希望将此逻辑作为依赖项传递。单元测试将使用已知的存根值, 并且生产代码将允许系统在运行时提供实际值。

让我们将LocalDate供应商添加到Person.java:

Person.java
// ...
class Person {
    // ...
    private final LocalDate dateOfBirth;
    private final Supplier<LocalDate> currentDateSupplier;

    Person(String givenName, String surname, LocalDate dateOfBirth) {
        this(givenName, surname, dateOfBirth, LocalDate::now);
    }

    // Visible for testing
    Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier) {
        // ...
        this.dateOfBirth = dateOfBirth;
        this.currentDateSupplier = currentDateSupplier;
    }

    // ...

    long getAge() {
        return ChronoUnit.YEARS.between(dateOfBirth, currentDateSupplier.get());
    }

    public static void main(String... args) {
        Person person = new Person("Joey", "Doe", LocalDate.parse("2013-01-12"));
        System.out.println(person.getDisplayName() + ": " + person.getAge() + " years");
        // Doe, Joey: 4 years
    }
}

为了更轻松地测试getAge()方法, 我们将其更改为使用LocalDate供应商currentDateSupplier来检索当前日期。如果你不知道供应商是什么, 我建议阅读有关Lambda内置功能接口的信息。

我们还添加了一个依赖项注入:新的测试构造函数允许测试提供自己的当前日期值。原始构造函数调用此新构造函数, 并传递提供LocalDate对象的LocalDate :: now静态方法引用, 因此我们的main方法仍然像以前一样工作。那我们的测试方法呢?让我们更新PersonTest.java:

PersonTest.java
// ...
class PersonTest {
    // ...

    @Test
    void testGetAge() {
        LocalDate dateOfBirth = LocalDate.parse("2013-01-02");
        LocalDate currentDate = LocalDate.parse("2017-01-17");
        Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate);
        long age = person.getAge();
        assertEquals(4, age);
    }
}

现在, 该测试会注入自己的currentDate值, 因此我们的测试在明年或任何一年运行时仍将通过。通常将其称为存根, 或提供要返回的已知值, 但是我们首先必须更改Person以允许注入此依赖项。

构造Person对象时, 请注意lambda语法(()-> currentDate)。根据新构造函数的要求, 这被视为LocalDate的供应商。

模拟和存根Web服务

我们已经准备好将Person对象(其全部存在于JVM内存中)与外界进行通信。我们要添加两种方法:publishAge()方法(用于发布人员的当前年龄)和getThoseInCommon()方法(用于返回与我们的Person拥有相同生日或相同年龄的名人的姓名)。假设有一个我们可以与之交互的RESTful服务, 称为”人们的生日”。我们有一个Java客户端, 它由单个类BirthdaysClient组成。

com.example.birthdays.BirthdaysClient
package com.example.birthdays;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;

public class BirthdaysClient {

    public void publishRegularPersonAge(String name, long age) throws IOException {
        System.out.println("publishing " + name + "'s age: " + age);
        // HTTP POST with name and age and possibly throw an exception
    }

    public Collection<String> findFamousNamesOfAge(long age) throws IOException {
        System.out.println("finding famous names of age " + age);
        return Arrays.asList(/* HTTP GET with age and possibly throw an exception */);
    }

    public Collection<String> findFamousNamesBornOn(int month, int dayOfMonth) throws IOException {
        System.out.println("finding famous names born on day " + dayOfMonth + " of month " + month);
        return Arrays.asList(/* HTTP GET with month and day and possibly throw an exception */);
    }
}

让我们增强Person类。我们首先为publishAge()的所需行为添加新的测试方法。为什么要从测试开始, 而不是功能?我们遵循测试驱动开发(也称为TDD)的原则, 其中我们首先编写测试, 然后编写代码以使其通过。

PersonTest.java
// … 
class PersonTest {
    // … 

    @Test
    void testPublishAge() {
        LocalDate dateOfBirth = LocalDate.parse("2000-01-02");
        LocalDate currentDate = LocalDate.parse("2017-01-01");
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate);
        person.publishAge();
    }
}

此时, 测试代码无法编译, 因为我们尚未创建它所调用的publishAge()方法。一旦创建了空的Person.publishAge()方法, 一切都会通过。现在, 我们可以进行测试了, 以验证该人的年龄实际上已发布到BirthdaysClient。

添加模拟对象

由于这是一个单元测试, 因此应该可以快速运行并在内存中运行, 因此该测试将使用模拟的BirthdaysClient构造Person对象, 因此实际上不会发出网络请求。然后, 测试将使用此模拟对象来验证它是否按预期被调用。为此, 我们将添加对Mockito框架的依赖(MIT许可)以创建模拟对象, 然后创建模拟的BirthdaysClient对象:

PersonTest.java
// ...
import com.example.birthdays.BirthdaysClient;
// ...
import static org.mockito.Mockito.mock;

class PersonTest {
    private BirthdaysClient birthdaysClient = mock(BirthdaysClient.class);

    // ...

    @Test
    void testPublishAge() {
        // ...
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);
        // ...
    }
}

此外, 我们增加了Person构造函数的签名以采用BirthdaysClient对象, 并更改了测试以注入模拟的BirthdaysClient对象。

添加模拟期望

接下来, 在testPublishAge的末尾添加对BirthdaysClient被调用的期望。如我们的新PersonTest.java中所示, Person.publishAge()应该调用它:

PersonTest.java
// ...
class PersonTest {
    // ...

    @Test
    void testPublishAge() throws IOException {
        // ...
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);
        verifyZeroInteractions(birthdaysClient);
        person.publishAge();
        verify(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16);
    }
}

我们的Mockito增强型BirthdaysClient会跟踪对其方法的所有调用, 这是我们在调用publishAge()之前, 用verifyZeroInteractions()方法验证是否未对BirthdaysClient进行任何调用的方法。尽管可以说不是必需的, 但通过这样做, 我们可以确保构造函数不会进行任何恶意调用。在verify()行上, 我们指定对BirthdaysClient的调用的外观。

请注意, 由于publishRegularPersonAge的签名中具有IOException, 因此我们也将其添加到测试方法签名中。

此时, 测试失败:

Wanted but not invoked:
birthdaysClient.publishRegularPersonAge(
    "Joe Sixteen", 16L
);
-> at com.example.PersonTest.testPublishAge(PersonTest.java:40)

鉴于我们尚未对Person.java进行必要的更改, 这是可以预期的, 因为我们正在跟踪测试驱动的开发。现在, 我们将通过进行必要的更改使此测试通过:

Person.java
// ...
class Person {
    // ...
    private final BirthdaysClient birthdaysClient;

    Person(String givenName, String surname, LocalDate dateOfBirth) {
        this(givenName, surname, dateOfBirth, LocalDate::now, new BirthdaysClient());
    }

    // Visible for testing
    Person(String givenName, String surname, LocalDate dateOfBirth, Supplier<LocalDate> currentDateSupplier, BirthdaysClient birthdaysClient) {
        // ...
        this.birthdaysClient = birthdaysClient;
    }

    // ...

    void publishAge() {
        String nameToPublish = givenName + " " + surname;
        long age = getAge();
        try {
            birthdaysClient.publishRegularPersonAge(nameToPublish, age);
        }
        catch (IOException e) {
            // TODO handle this!
            e.printStackTrace();
        }
    }
}

测试异常

我们使生产代码构造函数实例化一个新的BirthdaysClient, 并且publishAge()现在调用BirthdaysClient。所有测试均通过;一切都是绿色的。大!但是请注意publishAge()正在吞没IOException。与其让它冒出来, 不如将其与我们自己的PersonException一起包装在一个名为PersonException.java的新文件中:

PersonException.java
package com.example;

public class PersonException extends Exception {
    public PersonException(String message, Throwable cause) {
        super(message, cause);
    }
}

我们在PersonTest.java中将这种情况实现为新的测试方法:

PersonTest.java
// ...
class PersonTest {
    // ...

    @Test
    void testPublishAge_IOException() throws IOException {
        LocalDate dateOfBirth = LocalDate.parse("2000-01-02");
        LocalDate currentDate = LocalDate.parse("2017-01-01");

        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);

        IOException ioException = new IOException();
        doThrow(ioException).when(birthdaysClient).publishRegularPersonAge("Joe Sixteen", 16);

        try {
            person.publishAge();
            fail("expected exception not thrown");
        }
        catch (PersonException e) {
            assertSame(ioException, e.getCause());
            assertEquals("Failed to publish Joe Sixteen age 16", e.getMessage());
        }
    }
}

当调用publishRegularPersonAge()方法时, Mockito doThrow()调用stubs BirthdaysClient引发异常。如果未抛出PersonException, 则测试将失败。否则, 我们断言该异常已与IOException正确链接, 并验证异常消息是否符合预期。目前, 由于我们尚未在生产代码中实施任何处理, 因此测试失败, 因为未引发预期的异常。我们需要在Person.java中进行更改以使测试通过:

Person.java
// ...
class Person {
    // ...

    void publishAge() throws PersonException {
        // ...
        try {
            // ...
        }
        catch (IOException e) {
            throw new PersonException("Failed to publish " + nameToPublish + " age " + age, e);
        }
    }
}

存根:何时和断言

现在, 我们实现Person.getThoseInCommon()方法, 使我们的Person.Java类看起来像这样。

与testPublishAge()不同, 我们的testGetThoseInCommon()不会验证是否对BirthdaysClient方法进行了特定的调用。相反, 它使用何时调用存根返回值来进行getThoseInCommon()需要进行的findFamousNamesOfAge()和findFamousNamesBornOn()调用。然后, 我们断言将返回我们提供的所有三个存根名称。

使用assertAll()JUnit 5方法包装多个断言可以将所有断言作为一个整体进行检查, 而不是在第一个失败的断言之后停止。我们还将在assertTrue()中包含一条消息, 以标识未包含的特定名称。这是我们的”幸福道路”(理想情况)测试方法的样子(请注意, 根据”幸福道路”的性质, 这并不是一组可靠的测试, 但是稍后我们将讨论原因。

PersonTest.java
// ...
class PersonTest {
    // ...

    @Test
    void testGetThoseInCommon() throws IOException, PersonException {
        LocalDate dateOfBirth = LocalDate.parse("2000-01-02");
        LocalDate currentDate = LocalDate.parse("2017-01-01");
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);

        when(birthdaysClient.findFamousNamesOfAge(16)).thenReturn(Arrays.asList("JoeFamous Sixteen", "Another Person"));
        when(birthdaysClient.findFamousNamesBornOn(1, 2)).thenReturn(Arrays.asList("Jan TwoKnown"));

        Set<String> thoseInCommon = person.getThoseInCommon();

        assertAll(
                setContains(thoseInCommon, "Another Person"), setContains(thoseInCommon, "Jan TwoKnown"), setContains(thoseInCommon, "JoeFamous Sixteen"), ()-> assertEquals(3, thoseInCommon.size())
        );
    }

    private <T> Executable setContains(Set<T> set, T expected) {
        return () -> assertTrue(set.contains(expected), "Should contain " + expected);
    }

    // ...
}

保持测试代码干净

尽管经常被忽略, 但保持测试代码不受重复复制的影响同样重要。干净的代码和”不要重复自己”的原则对于维持高质量的代码库, 生产和测试代码都非常重要。请注意, 由于我们有几种测试方法, 因此最新的PersonTest.java有所重复。

为了解决这个问题, 我们可以做一些事情:

  • 将IOException对象提取到私有的final字段中。

  • 由于大多数Person对象是使用相同的参数创建的, 因此将Person对象的创建提取到其自己的方法中(在本例中为createJoeSixteenJan2())。

  • 为验证抛出的PersonExceptions的各种测试创建一个assertCauseAndMessage()。

可以在PersonTest.java文件的此版本中看到干净的代码结果。

测试比幸福的道路更多

当Person对象的出生日期晚于当前日期时该怎么办?应用程序中的缺陷通常是由于意外输入或缺乏对角落, 边缘或边界案例的洞察力造成的。重要的是要尽我们所能预料这些情况, 而单元测试通常是这样做的合适场所。在构建Person和PersonTest时, 我们包括了一些针对预期异常的测试, 但这绝不是完整的。例如, 我们使用不代表或存储时区数据的LocalDate。但是, 我们对LocalDate.now()的调用会根据系统的默认时区返回LocalDate, 该默认时区可能比系统用户的时区早或晚。这些因素应通过适当的测试和实施的行为加以考虑。

边界也应测试。考虑使用getDaysUntilBirthday()方法的Person对象。测试应包括该人的生日在当年是否已经过去, 该人的生日是否在今天以及a年如何影响天数。通过检查某人的生日前一天, 某天的生日以及该天之后的第二天(第二年是a年), 可以涵盖这些情况。以下是相关的测试代码:

PersonTest.java
// ...
class PersonTest {
    private final Supplier<LocalDate> currentDateSupplier = ()-> LocalDate.parse("2015-05-02");
    private final LocalDate ageJustOver5 = LocalDate.parse("2010-05-01");
    private final LocalDate ageExactly5 = LocalDate.parse("2010-05-02");
    private final LocalDate ageAlmost5 = LocalDate.parse("2010-05-03");

    // ...

    @Test
    void testGetDaysUntilBirthday() {
        assertAll(
            createPersonAndAssertValue(ageAlmost5, 1, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageExactly5, 0, Person::getDaysUntilBirthday), createPersonAndAssertValue(ageJustOver5, 365, Person::getDaysUntilBirthday)
        );
    }

    private Executable createPersonAndAssertValue(LocalDate dateOfBirth, long expectedValue, Function<Person, Long> personLongFunction) {
        Person person = new Person("Given", "Sur", dateOfBirth, currentDateSupplier);
        long actualValue = personLongFunction.apply(person);
        return () -> assertEquals(expectedValue, actualValue);
    }
}

整合测试

我们主要关注单元测试, 但是JUnit也可以用于集成, 验收, 功能和系统测试。此类测试通常需要更多的设置代码, 例如, 启动服务器, 使用已知数据加载数据库等。尽管我们通常可以在几秒钟内运行数千个单元测试, 但大型集成测试套件可能需要几分钟甚至几小时才能运行。通常不应使用集成测试来尝试覆盖代码中的每个排列或路径。单元测试更适合于此。

通常使用Selenium WebDriver(Apache 2.0许可证)与”页面对象模式”结合使用来为驱动填写形式的Web浏览器的Web应用程序创建测试(请参阅SeleniumHQ github Wiki)和Martin Fowler在Page Objects上的文章)。

JUnit通过使用HTTP客户端(例如Apache HTTP客户端)或Spring Rest Template(HowToDoInJava.com提供了一个很好的示例)来有效测试RESTful API。

就我们的Person对象而言, 集成测试可能涉及使用真实的BirthdaysClient而不是模拟对象, 其配置指定了People Birthdays服务的基本URL。然后, 集成测试将使用该服务的测试实例, 验证生日已发布到该服务, 并在该服务中创建将返回的名人。

其他JUnit功能

JUnit还有许多其他功能, 我们尚未在示例中进行探讨。我们将描述一些内容, 并为其他内容提供参考。

测试治具

应该注意的是, JUnit为运行每个@Test方法创建了一个测试类的新实例。 JUnit还提供注释挂钩, 以在所有@Test方法之前或之后运行特定方法。这些挂钩通常用于设置或清理数据库或模拟对象, 并且在JUnit 4和5之间有所不同。

用于测试的JUnit前缀和后缀。

在我们的PersonTest示例中, 我们选择在@Test方法本身中配置BirthdaysClient模拟对象, 但有时需要构建涉及多个对象的更复杂的模拟结构。 @BeforeEach(在JUnit 5中)和@Before(在JUnit 4中)通常适用于此。

在集成测试中, @ After *注释比单元测试更常见, 因为JVM垃圾回收处理大多数为单元测试创​​建的对象。 @BeforeClass和@BeforeAll批注最​​常用于需要一次执行昂贵的设置和拆卸操作的集成测试, 而不是用于每种测试方法。

对于JUnit 4, 请参考测试装置指南(一般概念仍然适用于JUnit 5)。

测试套件

有时你想运行多个相关测试, 但不是所有测试。在这种情况下, 可以将测试分组组成测试套件。有关如何在JUnit 5中执行此操作的信息, 请参阅HowToProgram.xyz的JUnit 5文章, 以及JUnit团队的JUnit 4文档。

JUnit 5的@Nested和@DisplayName

JUnit 5添加了使用非静态嵌套内部类的功能, 以更好地显示测试之间的关系。对于在诸如Jasmine for JavaScript的测试框架中使用嵌套描述的人员来说, 这应该是非常熟悉的。内部类使用@Nested注释以使用它。

@DisplayName注释也是JUnit 5的新增功能, 它允许你以字符串格式描述用于报告的测试, 除了测试方法标识符之外, 还将显示该测试。

尽管@Nested和@DisplayName可以彼此独立使用, 但它们可以一起提供更清晰的测试结果, 以描述系统的行为。

Hamcrest Matchers

Hamcrest框架虽然本身不​​是JUnit代码库的一部分, 但它提供了一种在测试中使用传统的assert方法的替代方法, 从而可以实现更具表现力和可读性的测试代码。使用传统的assertEquals和Hamcrest assertThat查看以下验证:

//Traditional assert
assertEquals("Hayden, Josh", displayName);

//Hamcrest assert
assertThat(displayName, equalTo("Hayden, Josh"));

Hamcrest可以与JUnit 4和5一起使用。Vogella.com关于Hamcrest的教程非常全面。

其他资源

  • 单元测试, 如何编写可测试的代码及其重要性的文章涵盖了编写干净的, 可测试的代码的更多具体示例。

  • 充满信心地构建:《 JUnit测试指南》探讨了单元测试和集成测试的不同方法, 以及为什么最好选择并坚持使用它

</ div>

  • 《 JUnit 4 Wiki》和《 JUnit 5用户指南》始终是一个很好的参考点。

  • Mockito文档提供有关其他功能和示例的信息。

JUnit是通往自动化的道路

我们已经探索了使用JUnit在Java世界中进行测试的许多方面。我们研究了使用Java代码库的JUnit框架进行单元和集成测试, 将JUnit集成到开发和构建环境中, 如何与供应商和Mockito一起使用模拟和存根, 通用约定和最佳代码实践, 要测试的内容以及一些其他出色的JUnit功能。

现在该轮到读者了, 他们越来越熟练地使用JUnit框架来应用, 维护和利用自动化测试的好处。

赞(0)
未经允许不得转载:srcmini » JUnit健壮的单元和集成测试指南

评论 抢沙发

评论前必须登录!