作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
约书亚·海登的头像

Joshua Hayden

作为一名结果驱动的软件工程师,Josh专注于实现高质量的代码. 他经常参与开发的所有阶段.

Expertise

Previously At

NetApp
Share

自动化软件测试对于长期的质量至关重要, maintainability, 以及软件项目的可扩展性, and for Java, JUnit是实现自动化的途径.

虽然本文的大部分内容将集中在写作上 健壮的单元测试 利用存根, mocking, 依赖注入, 我们还将讨论JUnit和集成测试.

The JUnit测试框架 是一个通用的、免费的、开源的测试基于java的项目的工具.

在撰写本文时, JUnit 4 当前是主版本吗, 十多年前被释放, 上一次更新是在两年多前.

JUnit 5 (使用木星编程和扩展模型)正在积极开发中. 中引入的语言特性得到了更好的支持 Java 8 并包括其他新的,有趣的功能. 一些团队可能发现JUnit 5已经可以使用了, 而其他人可能会继续使用JUnit 4,直到JUnit 5正式发布. 我们将看看这两个方面的例子.

Running JUnit

JUnit测试可以直接在IntelliJ中运行, 但它们也可以在Eclipse等其他ide中运行, NetBeans, 甚至是命令行.

测试应该总是在构建时运行,尤其是单元测试. 具有任何失败测试的构建都应被视为失败, 不管问题是在产品代码中还是在测试代码中——这都需要团队的纪律,以及对解决失败测试给予最高优先级的意愿, 但必须坚持自动化的精神.

JUnit测试也可以通过Jenkins这样的持续集成系统来运行和报告. 使用Gradle等工具的项目, Maven, 或者Ant还有一个额外的优势,那就是能够将测试作为构建过程的一部分来运行.

表示兼容性的齿轮组:JUnit 4与NetBeans在一起, 另一个是JUnit 5, Eclipse和Gradle, 最后一个是JUnit 5,有Maven和IntelliJ IDEA.

Gradle

作为JUnit 5的样例Gradle项目,请参见 JUnit用户指南的Gradle部分 and the junit5-samples.git repository. 注意,它还可以运行使用JUnit 4 API的测试(称为 “vintage”).

The project can be created in IntelliJ via the menu option File > Open… > navigate to the junit-gradle-consumer子目录 > OK > Open as Project > OK to import the project from Gradle.

对于Eclipse, Gradle插件 can be installed from Help > Eclipse Marketplace… The project can then be imported with File > Import… > Gradle > Gradle Project > Next > Next > Browse to the junit-gradle-consumer sub-directory > Next > Next > Finish.

在IntelliJ或Eclipse中设置Gradle项目后,运行Gradle build 任务将包括运行所有JUnit测试 test task. 的后续执行可能会跳过测试 build 如果没有对代码进行任何更改.

对于JUnit 4,请参见JUnit 's 使用Gradle wiki.

Maven

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

In IntelliJ, use File > Open… > navigate to junit-maven-consumer / pom.xml > OK > Open as Project. The tests can then be run from Maven Projects > junit5-maven-consumer > Lifecycle > Test.

In Eclipse, use File > Import… > Maven > Existing Maven Projects > Next > Browse to the junit-maven-consumer directory > With the pom.xml selected > Finish.

The tests can be executed by running the project as Maven build… > specify goal of test > Run.

For JUnit 4, see Maven存储库中的JUnit.

开发环境

除了通过Gradle或Maven等构建工具运行测试之外, 许多ide可以直接运行JUnit测试.

IntelliJ IDEA

IntelliJ IDEA 2016.JUnit 5测试需要2或更高版本,而JUnit 4测试应该在较旧的IntelliJ版本中工作.

为本文的目的, 你可能想在IntelliJ中创建一个新项目,从我的GitHub仓库之一( JUnit5IntelliJ.git or JUnit4IntelliJ.git),它包含了简单的 Person 类示例并使用内置的JUnit库. The test can be run with Run > Run ‘All Tests’. 也可以在IntelliJ中运行测试 PersonTest class.

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

See also the IntelliJ测试指南.

Eclipse

An empty Java Eclipse中的项目将没有测试根目录. This has be added from project Properties > Java Build Path > Add Folder… > Create New Folder… > specify the Folder name > Finish. 新目录将被选为源文件夹. 在剩下的两个对话框中单击OK.

JUnit 4 tests can be created with File > New > JUnit Test Case. 选择“New JUnit 4 test”和新创建的测试源文件夹. 指定一个“测试中的类”和一个“包”,确保包与测试中的类匹配. 然后,为测试类指定一个名称. 在完成向导之后,如果出现提示,选择“Add JUnit 4 library”到构建路径. 然后,项目或单个测试类可以作为JUnit测试运行. See also Eclipse编写和运行JUnit测试.

NetBeans

NetBeans只支持JUnit 4测试. Test classes can be created in a NetBeans Java project with File > New File… > Unit Tests > JUnit Test or Test for Existing Class. 缺省情况下,命名为测试根目录 test 在项目目录中.

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

让我们来看一个简单的生产代码示例,它对应的单元测试代码非常简单 Person class. 您可以从我的 github project 并通过IntelliJ打开它.

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

class Person {
   private final String givenName;
   私有最终字符串姓氏;

   Person(字符串givenName,字符串姓氏){
       this.给定名称=给定名称;
       this.姓=姓;
   }

   字符串getDisplayName () {
       返回姓氏+ "," + givenName;
   }
}

The immutable Person 类具有构造函数和 getDisplayName () method. 我们想测试一下 getDisplayName () 返回按预期格式格式化的名称. 下面是单个单元测试(junit5)的测试代码:

src /测试/ java / com/example/PersonTest.java
package com.example;

import org.junit.jupiter.api.Test;
导入静态org.junit.jupiter.api.Assertions.*;

类PersonTest {

   @Test
   无效testGetDisplayName () {
       Person = new Person(“Josh”,“Hayden”);
       字符串displayName = person.getDisplayName ();
       asserequals ("Hayden, Josh", displayName);
   }
}

PersonTest uses JUnit 5’s @Test and assertion. For JUnit 4, the PersonTest 类和方法需要是公共的,并且应该使用不同的导入. Here’s the JUnit 4示例Gist.

Upon running the PersonTest 类,则测试通过,UI指示灯为绿色.

通用JUnit约定

Naming

虽然不是必须的, we use common conventions in naming the test class; specifically, 我们从被测试类的名称开始(Person),并在其后面加上“Test”(PersonTest). 命名测试方法也是类似的,从被测试的方法开始(getDisplayName ()),并在其前面加上“test”(testGetDisplayName ()). 虽然有许多其他完全可以接受的命名测试方法的约定, 在整个团队和项目中保持一致是很重要的.

产品名称Name in Testing
PersonPerson Test
getDisplayName ()testDisplayName ()

Packages

我们还采用了创建测试代码的惯例 PersonTest 类(com.example)作为生产代码 Person class. 如果我们使用不同的包进行测试,我们将被要求使用公共包 access modifier 在生产代码类中, constructors, 以及由单元测试引用的方法, 即使在不合适的地方, 所以最好还是把它们放在同一个包装里. 但是,我们确实使用单独的源目录(src/main/java and src/test/java),因为我们通常不希望在发布的产品构建中包含测试代码.

结构和注释

The @Test 注释(JUnit 4/5)告诉JUnit执行 testGetDisplayName () 方法作为测试方法,并报告它是通过还是失败. 只要所有断言(如果有的话)都通过并且没有抛出异常,测试就被认为通过了.

我们的测试代码遵循结构模式 Arrange-Act-Assert (AAA). 其他常见模式包括Given-When-Then和Setup-Exercise-Verify-Teardown(单元测试通常不显式地需要Teardown)。, 但我们在本文中使用AAA.

让我们看看我们的测试示例是如何遵循AAA的. 第一行“arrange”创建了一个 Person 要测试的对象:

       Person = new Person(“Josh”,“Hayden”);

第二行,“行为”, exercises 生产代码是 Person.getDisplayName () method:

       字符串displayName = person.getDisplayName ();

第三行“assert”验证结果是否符合预期.

       asserequals ("Hayden, Josh", displayName);

Internally, the assertEquals() 呼叫使用了“海登”, 字符串对象的equals方法,用于验证从生产代码(displayName) matches. 如果不匹配,测试就会被标记为失败.

请注意,对于每个AAA阶段,测试通常有不止一行.

单元测试和生产代码

现在我们已经介绍了一些测试约定, 让我们把注意力转向使产品代码可测试.

我们回到我们的 Person 类,我在其中实现了一个方法,该方法根据一个人的出生日期返回他或她的年龄. 代码示例要求Java 8利用新的日期和功能api. 以下是最新消息 Person.java 类看起来像:

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

    人(String givenName, String姓,LocalDate, dateOfBirth) {
        // ...
        this.dateOfBirth = dateOfBirth;
    }

    // ...

    long getAge() {
        返回ChronoUnit.YEARS.(dateOfBirth, LocalDate之间.now());
    }

    public static void main(字符串... args) {
        Person Person = new Person(“Joey”,“Doe”,LocalDate).解析(" 2013-01-12 "));
        System.out.println(person.getDisplayName () + ": " + person.getAge() + " years");
        //乔伊:4年
    }
}

在写这篇文章的时候,乔伊已经4岁了. 让我们添加一个测试方法:

PersonTest.java
// ...
类PersonTest {
    // ...

    @Test
    testGetAge() {
        Person Person = new Person(“Joey”,“Doe”,LocalDate).解析(" 2013-01-12 "));
        长年龄=人.getAge();
        assertequal(4岁);
    }
}

它今天通过了,但一年后我们运行它的时候呢? 这个测试是不确定的和脆弱的,因为预期的结果取决于运行测试的系统的当前日期.

存根和注入一个值供应商

在生产环境中运行时,我们希望使用当前日期, LocalDate.now(), 用于计算人的年龄, 但要在一年后做一个决定性的测试, 测试需要自己提供 currentDate values.

这被称为依赖注入. 我们不想要 Person 对象以确定当前日期本身, 但是我们想把这个逻辑作为依赖项传入. 单元测试将使用已知的, stubbed value, 产品代码将允许系统在运行时提供实际值.

Let’s add a LocalDate supplier to Person.java:

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

    人(String givenName, String姓,LocalDate, dateOfBirth) {
        这个(给定的名字,姓氏,出生日期,本地日期::现在);
    }

    //用于测试
    人(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier) {
        // ...
        this.dateOfBirth = dateOfBirth;
        this.currentDateSupplier = currentDateSupplier;
    }

    // ...

    long getAge() {
        返回ChronoUnit.YEARS.(dateOfBirth, currentDateSupplier之间.get());
    }

    public static void main(字符串... args) {
        Person Person = new Person(“Joey”,“Doe”,LocalDate).解析(" 2013-01-12 "));
        System.out.println(person.getDisplayName () + ": " + person.getAge() + " years");
        //乔伊:4年
    }
}

为了便于测试 getAge() 方法,我们将其更改为使用 currentDateSupplier, a LocalDate 供应商,用于检索当前日期. 如果你不知道什么是供应商,我建议你阅读 Lambda内置功能接口.

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

PersonTest.java
// ...
类PersonTest {
    // ...

    @Test
    testGetAge() {
        dateOfBirth =本地日期.解析(“2013-01-02”);
        LocalDate currentDate = LocalDate.解析(“2017-01-17”);
        Person person = new Person("Joey", "Doe", dateOfBirth, ()->currentDate);
        长年龄=人.getAge();
        assertequal(4岁);
    }
}

测试现在注入了它自己的 currentDate 值,因此我们的测试在明年或任何年份运行时仍然会通过. 这通常被称为 stubbing,或提供要返回的已知值,但我们首先必须进行更改 Person 来允许此依赖项被注入.

Note the lambda syntax ( ()->currentDate),以建构 Person object. 这被视为a的供应商 LocalDate,按照新构造函数的要求.

嘲弄和存根Web服务

我们准备好了 Person 对象(其整个存在都在JVM内存中)与外部世界通信. 我们想添加两个方法: publishAge() 方法,该方法将发布该人的当前年龄 getThoseInCommon () method, 哪个会返回与我们生日相同或年龄相同的名人的名字 Person. 假设我们可以与一个名为“People birthday”的RESTful服务进行交互.“我们有一个Java客户端,它由单个类组成, BirthdaysClient.

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

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

公共类BirthdaysClient {

    public void publishRegularPersonAge (字符串名称,长年龄)抛出IOException {
        System.out.Println(“出版”+姓名+“年龄”+年龄);
        //带有姓名和年龄的HTTP POST,并可能抛出异常
    }

    public Collection findFamousNamesOfAge(long age) throws IOException {
        System.out.Println(“寻找年龄的名人”+年龄);
        return Arrays.asList(/*带年龄的HTTP GET并可能抛出异常*/);
    }

    public Collection findFamousNamesBornOn(int month, int dayOfMonth) throws IOException {
        System.out.println(“查找出生日期”+“dayOfMonth”+“of month”+月份);
        return Arrays.asList(/*带有月和日的HTTP GET,并可能抛出异常*/);
    }
}

让我们加强 Person class. 我们首先为期望的行为添加一个新的测试方法 publishAge(). 为什么从测试开始,而不是从功能开始? 我们遵循测试驱动开发(也称为TDD)的原则。, 其中我们首先编写测试, 然后是让它通过的代码.

PersonTest.java
// … 
类PersonTest {
    // … 

    @Test
    testPublishAge () {
        dateOfBirth =本地日期.解析(“2000-01-02”);
        LocalDate currentDate = LocalDate.解析(“2017-01-01”);
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate);
        person.publishAge();
    }
}

此时,测试代码无法编译,因为我们还没有创建 publishAge() 它调用的方法. 一旦我们创建一个空的 Person.publishAge() 方法,一切都通过. 我们现在已经准备好进行测试,以验证该人的年龄实际上已发布到 BirthdaysClient.

添加模拟对象

由于这是一个单元测试,它应该在内存中快速运行,因此该测试将构造我们的 Person 对象与模拟 BirthdaysClient 它实际上不会发出web请求. 然后,测试将使用这个模拟对象来验证是否按预期调用了它. 要做到这一点,我们将在 5框架 (MIT许可)创建模拟对象,然后创建一个模拟对象 BirthdaysClient object:

PersonTest.java
// ...
import com.example.birthdays.BirthdaysClient;
// ...
导入静态org.mockito.Mockito.mock;

类PersonTest {
    private BirthdaysClient BirthdaysClient = mock.class);

    // ...

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

我们进一步增加了……的签名 Person 构造函数 BirthdaysClient 对象,并将测试更改为注入mock BirthdaysClient object.

添加模拟期望

接下来,我们在我们的 testPublishAge 期望 BirthdaysClient is called. Person.publishAge() 应该叫它,如我们的新 PersonTest.java:

PersonTest.java
// ...
类PersonTest {
    // ...

    @Test
    testPublishAge ()抛出IOException {
        // ...
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);
        verifyZeroInteractions (birthdaysClient);
        person.publishAge();
        验证(birthdaysClient).publishRegularPersonAge (“Joe十六”,16);
    }
}

我们Mockito-enhanced BirthdaysClient 跟踪对其方法进行的所有调用, 这样我们就能确认没人打过电话给 BirthdaysClient with the verifyZeroInteractions () 方法。 publishAge(). 尽管可能不是必需的,但这样做可以确保构造函数没有进行任何非法调用. On the verify() 行,我们指定如何期望调用 BirthdaysClient to look.

Note, 那是因为publishRegularPersonAge的签名中有IOException, 我们将其添加到测试方法签名中, too.

此时,测试失败:

需要但未启用的:
birthdaysClient.publishRegularPersonAge (
    "Joe Sixteen",
    16L
);
-> at com.example.PersonTest.testPublishAge (PersonTest.java:40)

这是预料之中的,因为我们还没有实现所需的更改 Person.java,因为我们遵循测试驱动的开发. 现在,我们将通过进行必要的更改来使该测试通过:

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

    人(String givenName, String姓,LocalDate, dateOfBirth) {
        this(givenName,姓,dateOfBirth, LocalDate::now, new BirthdaysClient());
    }

    //用于测试
    人(String givenName, String surname, LocalDate dateOfBirth, Supplier currentDateSupplier, birthday(生日){
        // ...
        this.birthdaysClient = birthdaysClient;
    }

    // ...

    void publishAge() {
        字符串nametopubblish = givenName + " " +姓氏;
        long age = getAge();
        try {
            birthdaysClient.publishRegularPersonAge (nameToPublish、年龄);
        }
        catch (IOException) {
            // TODO处理!
            e.printStackTrace ();
        }
    }
}

异常测试

我们让生产代码构造函数实例化一个new BirthdaysClient, and publishAge() now calls the birthdaysClient. All tests pass; everything is green. Great! But notice that publishAge() 是否正在吞食IOException. 而不是让它冒出来, 我们想用我们自己的PersonException在一个名为 PersonException.java:

PersonException.java
package com.example;

公共类PersonException扩展Exception {
    public PersonException(字符串消息,可抛出原因){
        超级(消息、原因);
    }
}

我们将此场景作为一个新的测试方法实现 PersonTest.java:

PersonTest.java
// ...
类PersonTest {
    // ...

    @Test
    testPublishAge_IOException()抛出IOException {
        dateOfBirth =本地日期.解析(“2000-01-02”);
        LocalDate currentDate = LocalDate.解析(“2017-01-01”);

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

        IOException = new IOException();
        doThrow (ioException).当(birthdaysClient).publishRegularPersonAge (“Joe十六”,16);

        try {
            person.publishAge();
            失败("未抛出预期异常");
        }
        catch (PersonException) {
            assertSame (ioException e.getCause());
            assertEquals("未能发布Joe 16岁").getMessage());
        }
    }
}

The Mockito doThrow() call stubs birthdaysClient 时抛出异常 publishRegularPersonAge () 方法调用。. If the PersonException 没有被抛出,我们测试不及格. 否则,我们断言异常为 正确的链接 并验证异常消息是否如预期的那样. Right now, 因为我们没有在生产代码中实现任何处理, 我们的测试失败是因为没有抛出预期的异常. 这是我们需要改变的 Person.java 使测试通过:

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

    publishAge()抛出PersonException {
        // ...
        try {
            // ...
        }
        catch (IOException) {
            抛出新的PersonException("Failed to publish " + nametoppublish + " age " + age, e);
        }
    }
}

存根:何时和断言

现在我们实现 Person.getThoseInCommon () 方法,使我们 Person.Java class look like this.

Our testGetThoseInCommon (), unlike testPublishAge (),并不核实是否有特定的电话被打给 birthdaysClient methods. Instead it uses when 对存根的调用返回调用的值 findFamousNamesOfAge () and findFamousNamesBornOn () that getThoseInCommon () 将需要做出. 然后断言我们提供的所有三个存根名称都将返回.

类包装多个断言 assertAll() JUnit 5方法允许将所有断言作为一个整体进行检查, 而不是在第一次断言失败后停止. 我们还包含一个消息 assertTrue() 以识别未包含的特定名称. 下面是我们的“快乐路径”(一个理想的场景)测试方法, 从本质上来说,这并不是一组可靠的测试,但我们稍后会讨论原因.

PersonTest.java
// ...
类PersonTest {
    // ...

    @Test
    testGetThoseInCommon ()抛出IOException, PersonException {
        dateOfBirth =本地日期.解析(“2000-01-02”);
        LocalDate currentDate = LocalDate.解析(“2017-01-01”);
        Person person = new Person("Joe", "Sixteen", dateOfBirth, ()->currentDate, birthdaysClient);

        当(birthdaysClient.findFamousNamesOfAge (16)).thenReturn(数组.asList(“JoeFamous 16”,“另一个人”));
        当(birthdaysClient.findFamousNamesBornOn(1、2).thenReturn(数组.asList (Jan TwoKnown));

        Set thoseInCommon = person.getThoseInCommon ();

        assertAll(
                setContains(thoseInCommon,“另一个人”),
                setContains(那些常见的,“Jan TwoKnown”),
                setContains(thoseInCommon, "JoeFamous Sixteen"),
                ()-> assertEquals(3, thoseInCommon.size())
        );
    }

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

    // ...
}

保持测试代码整洁

尽管经常被忽视,但是保持测试代码不受重复的影响同样重要. 干净的代码和原则 “不要重复你说的话” 对于维护高质量的代码库、生产代码和测试代码都非常重要吗. 注意,最近的PersonTest.Java有一些重复,现在我们有几个测试方法.

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

  • 提取IOException对象到一个私有的final字段.

  • Extract the Person 对象创建到它自己的方法(createJoeSixteenJan2 ()因为大多数Person对象都是用相同的参数创建的.

  • Create an assertCauseAndMessage () 用于验证抛出的各种测试 PersonExceptions.

的这个版本中可以看到干净的代码结果 PersonTest.java file.

测试更多的快乐之路

我们该怎么办 Person 对象的出生日期晚于当前日期? 应用程序中的缺陷通常是由于意外输入或缺乏对角落的预见, edge, 或者边界情况. 重要的是要尽可能地预测这些情况, 单元测试通常是这样做的合适场所. In building our Person and PersonTest,我们包含了一些针对预期异常的测试,但这绝不是完整的. 例如,我们使用 LocalDate 哪个不表示或存储时区数据. Our calls to LocalDate.now(),但是,返回a LocalDate 根据系统默认时区设置, 哪个可能比系统用户的时间早一天或晚一天. 应该考虑这些因素,并实施适当的测试和行为.

边界也应该被测试. Consider a Person object with a getDaysUntilBirthday () method. 测试应包括当事人的生日是否已经在当年过了, 这个人的生日是不是今天, 以及闰年是如何影响天数的. 这些情况可以在过生日前一天检查一下, the day of, 在这个人生日的第二天,下一年是闰年. 下面是相关的测试代码:

PersonTest.java
// ...
类PersonTest {
    private final Supplier currentDateSupplier = ()-> LocalDate.解析(“2015-05-02”);
    ageJustOver5 = LocalDate.解析(“2010-05-01”);
    private final LocalDate ageExactly5 = LocalDate.解析(“2010-05-02”);
    ageAlmost5 = LocalDate.解析(“2010-05-03”);

    // ...

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

    createPersonAndAssertValue(本地日期, 长expectedValue, Function personLongFunction) {
        Person Person = new Person("Given", "Sur", dateOfBirth, currentdatessupplier);
        long actualValue = personLongFunction.apply(person);
        return () -> assertEquals(expectedValue, actualValue);
    }
}

集成测试

我们主要关注单元测试, 但是JUnit也可以用于集成, acceptance, functional, 系统测试. 这样的测试通常需要更多的设置代码,例如.g.,启动服务器,加载数据库与已知的数据等. 虽然我们经常可以在几秒钟内运行数千个单元测试, 大型集成测试套件可能需要几分钟甚至几个小时才能运行. Integration tests should generally not be used to try to cover every permutation or path through the code; unit tests are more appropriate for that.

为在填写表单时驱动web浏览器的web应用程序创建测试, 点击按钮, 等待内容加载, etc.,通常使用 硒WebDriver (Apache 2.0许可证)与“页面对象模式”(参见 SeleniumHQ github wiki and Martin Fowler在Page Objects上的文章).

JUnit对于使用HTTP客户端(如Apache HTTP客户端或Spring Rest模板)测试RESTful api是有效的。HowToDoInJava.Com就是一个很好的例子).

在我们的例子中 Person 对象,则集成测试可能涉及使用真实的 BirthdaysClient 而不是模拟的,使用指定People birthday服务的基本URL的配置. 然后,集成测试将使用此类服务的测试实例, 验证生日是否已发布给它,并在将返回的服务中创建名人.

JUnit的其他特性

JUnit还有许多我们在示例中尚未探讨的附加特性. 我们将描述一些,并为其他的提供参考.

Test Fixtures

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

JUnit 4JUnit 5对于静态方法?
@BeforeClass@BeforeAllYes
@AfterClass@AfterAllYes
@Before@BeforeEachNo
@After@AfterEachNo

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

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

对于JUnit 4,请参考 测试夹具指南 (一般概念仍然适用于JUnit 5).

Test Suites

有时您希望运行多个相关测试,但不是所有测试. 在这种情况下,测试分组可以组成测试套件. 关于如何在JUnit 5中做到这一点,请查看 HowToProgram.xyz的JUnit 5文章,以及JUnit团队的 JUnit 4的文档.

JUnit 5的@Nested和@DisplayName

JUnit 5增加了使用非静态嵌套内部类的功能,以更好地显示测试之间的关系. 对于那些在JavaScript的Jasmine等测试框架中使用嵌套描述的人来说,这应该是非常熟悉的. 内部类用 @Nested to use this.

The @DisplayName 注释也是JUnit 5的新特性, 允许您以字符串格式描述报告的测试, 在测试方法标识符之外显示.

Although @Nested and @DisplayName 可以彼此独立使用吗, 它们一起可以提供描述系统行为的更清晰的测试结果.

Hamcrest匹配器

The Hamcrest框架, 尽管它本身不是JUnit代码库的一部分, 提供在测试中使用传统断言方法的替代方法, 允许更具表现力和可读性的测试代码. 请看下面使用传统assertEquals和Hamcrest assertThat的验证:

/ /传统的断言
asserequals ("Hayden, Josh", displayName);

/ / Hamcrest断言
断言(displayName, equalTo(“Hayden, Josh”));

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

额外的资源

JUnit是通往自动化的道路

我们已经用JUnit探索了Java世界中测试的许多方面. 我们已经了解了使用JUnit框架进行Java代码库的单元和集成测试, 在开发和构建环境中集成JUnit, 如何使用模拟和存根与供应商和Mockito, 通用约定和最佳代码实践, what to test, 以及其他一些很棒的JUnit特性.

现在该轮到读者在熟练运用中成长了, maintaining, 并获得使用JUnit框架的自动化测试的好处.

就这一主题咨询作者或专家.
Schedule a call
约书亚·海登的头像
Joshua Hayden

Located in 威奇托,堪萨斯州,美国

Member since 2016年12月22日

About the author

作为一名结果驱动的软件工程师,Josh专注于实现高质量的代码. 他经常参与开发的所有阶段.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Previously At

NetApp

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal开发者

Join the Toptal® community.