乐趣区

关于测试:使用-Spring-Boot-进行单元测试

【注】本文译自:Unit Testing with Spring Boot – Reflectoring

编写好的单元测试能够被认为是一门难以把握的艺术。但好消息是反对它的机制很容易学习。
本教程为您提供了这些机制,并具体介绍了编写良好的单元测试所必须的技术细节,重点是 Spring Boot 应用程序。
咱们将看看如何以可测试的形式创立 Spring bean,而后探讨 Mockito 和 AssertJ 的用法,这两个库默认蕴含在 Spring Boot 中用于测试。
请留神,本文仅探讨 单元测试。集成测试、Web 层测试和长久层测试将在本系列的后续文章中探讨。

 代码示例

本文附有 GitHub 上 的工作代码示例。

依赖关系

对于本教程中的单元测试,咱们将应用 JUnit Jupiter (JUnit 5)、Mockito 和 AssertJ。咱们还将包含 Lombok 以缩小一些样板代码:

dependencies {compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.junit.jupiter:junit-jupiter:5.4.0')
    testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}

Mockito 和 AssertJ 是应用 spring-boot-starter-test 依赖项主动导入的,但咱们必须本人蕴含 Lombok。

不要在单元测试中应用 Spring

如果你以前用 Spring 或 Spring Boot 写过测试,你可能会说咱们不须要 Spring 来写单元测试 。这是为什么?
思考以下测试 RegisterUseCase 类的单个办法的“单元”测试:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {

    @Autowired
    private RegisterUseCase registerUseCase;

    @Test
    void savedUserHasRegistrationDate() {User user = new User("zaphod", "zaphod@mail.com");
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();}

}

这个测试在我电脑上的一个空 Spring 我的项目上运行大概须要 4.5 秒。
然而一个好的单元测试只须要几毫秒 。否则它会妨碍由测试驱动开发(TDD)思维推动的“测试 / 代码 / 测试”流程。但即便咱们不采纳 TDD,期待太长时间的测试也会毁坏咱们的注意力。
执行下面的测试方法实际上只须要几毫秒。剩下的 4.5 秒是因为 @SpringBootRun 通知 Spring Boot 设置整个 Spring Boot 应用程序上下文。
所以咱们启动了整个应用程序只是为了将 RegisterUseCase 实例主动拆卸到咱们的测试中 。一旦应用程序变大并且 Spring 不得不将越来越多的 bean 加载到应用程序上下文中,它将破费更长的工夫。
那么,为什么咱们不应该在单元测试中应用 Spring Boot 呢?诚实说,本教程的大部分内容都是对于在没有 Spring Boot 的状况下编写单元测试。

创立可测试的 Spring Bean

然而,咱们能够做一些事件来进步 Spring bean 的可测试性。

字段注入是不可取的

让咱们从一个不好的例子开始。思考以下类:

@Service
public class RegisterUseCase {

    @Autowired
    private UserRepository userRepository;

    public User registerUser(User user) {return userRepository.save(user);
    }

}

这个类不能在没有 Spring 的状况下进行单元测试,因为它没有提供传递 UserRepository 实例的办法。那么,咱们须要依照上一节中探讨的形式编写测试,让 Spring 创立一个 UserRepository 实例并将其注入到用 @Autowired 注解的字段中。

这里的教训是不要应用字段注入

提供构造函数

实际上,咱们基本不要应用 @Autowired 注解:

@Service
public class RegisterUseCase {

    private final UserRepository userRepository;

    public RegisterUseCase(UserRepository userRepository) {this.userRepository = userRepository;}

    public User registerUser(User user) {return userRepository.save(user);
    }

}

这个版本通过提供容许传入 UserRepository 实例的构造函数来容许构造函数注入。在单元测试中,咱们当初能够创立这样一个实例(可能是咱们稍后探讨的模仿实例)并将其传递给构造函数。
在创立生产应用程序上下文时,Spring 将主动应用此构造函数来实例化 RegisterUseCase 对象。留神,在 Spring 5 之前,咱们须要在构造函数中增加 @Autowired 注解,以便 Spring 找到构造函数。
还要留神 UserRepository 字段当初是 final。这是有情理的,因为字段内容在应用程序的生命周期内永远不会扭转。它还有助于防止编程谬误,因为如果咱们遗记初始化字段,编译器会报错。

缩小样板代码

应用 Lombok 的 @RequiredArgsConstructor 注解,咱们能够让构造函数主动生成:

@Service
@RequiredArgsConstructor
public class RegisterUseCase {

    private final UserRepository userRepository;

    public User registerUser(User user) {user.setRegistrationDate(LocalDateTime.now());
        return userRepository.save(user);
    }

}

当初,咱们有一个十分简洁的类,没有样板代码,能够在一般的 java 测试用例中轻松实例化:

class RegisterUseCaseTest {

    private UserRepository userRepository = ...;

    private RegisterUseCase registerUseCase;

    @BeforeEach
    void initUseCase() {registerUseCase = new RegisterUseCase(userRepository);
    }

    @Test
    void savedUserHasRegistrationDate() {User user = new User("zaphod", "zaphod@mail.com");
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();}

}

然而,还短少一点,那就是如何模仿咱们被测类所依赖的 UserRepository 实例,因为咱们不想依赖实在的货色,它可能须要连贯到数据库。

应用 Mockito 来模仿依赖

当初事实上的规范模仿库是 Mockito。它至多提供了两种办法来创立模仿的 UserRepository 以填补后面代码示例中的空白。

应用一般 Mockito 模仿依赖项

第一种办法是以编程形式应用 Mockito:

private UserRepository userRepository = Mockito.mock(UserRepository.class);

这将创立一个从内部看起来像 UserRepository 的对象。默认状况下,当一个办法被调用时它什么都不做,如果该办法有返回值则返回 null
咱们的测试当初将在 assertThat(savedUser.getRegistrationDate()).isNotNull() 处以 NullPointerException 失败,因为 userRepository.save(user) 当初返回 null
所以,咱们必须通知 Mockito 在调用 userRepository.save() 时返回一些货色。咱们应用动态 when 办法来做到这一点:

    @Test
    void savedUserHasRegistrationDate() {User user = new User("zaphod", "zaphod@mail.com");
        when(userRepository.save(any(User.class))).then(returnsFirstArg());
        User savedUser = registerUseCase.registerUser(user);
        assertThat(savedUser.getRegistrationDate()).isNotNull();}

这将使 userRepository.save() 返回传递给办法的雷同用户对象。
Mockito 具备更多功能,能够进行模仿、匹配参数和验证办法调用。无关更多信息,请查看参考文档。

应用 Mockito 的 @Mock 注解模仿依赖项

创立模仿对象的另一种办法是 Mockito 的 @Mock 注解与 JUnit Jupiter 的 MockitoExtension 相结合:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    private RegisterUseCase registerUseCase;

    @BeforeEach
    void initUseCase() {registerUseCase = new RegisterUseCase(userRepository);
    }

    @Test
    void savedUserHasRegistrationDate() {// ...}

}

@Mock 注解指定了 Mockito 应该注入模仿对象的字段。@MockitoExtension 通知 Mockito 评估那些 @Mock 注解,因为 JUnit 不会主动执行此操作。
后果和手动调用 Mockito.mock() 一样,抉择应用哪种形式是品尝问题。然而请留神,通过应用 MockitoExtension 将咱们的测试绑定到测试框架。
请留神,咱们也能够在 registerUseCase 字段上应用 @InjectMocks 注解,而不是手动结构 RegisterUseCase 对象。而后 Mockito 会依照指定的算法为咱们创立一个实例:

@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private RegisterUseCase registerUseCase;

    @Test
    void savedUserHasRegistrationDate() {// ...}

}

应用 AssertJ 创立可读断言

Spring Boot 测试反对主动附带的另一个库是 AssertJ。咱们曾经在下面应用它来实现咱们的断言:

assertThat(savedUser.getRegistrationDate()).isNotNull();

然而,让断言更具可读性不是更好吗?例如:

assertThat(savedUser).hasRegistrationDate();

在很多状况下,像这样的小改变会使测试更容易了解。因而,让咱们在 测试源文件夹 中创立咱们本人的自定义断言:

class UserAssert extends AbstractAssert<UserAssert, User> {UserAssert(User user) {super(user, UserAssert.class);
    }

    static UserAssert assertThat(User actual) {return new UserAssert(actual);
    }

    UserAssert hasRegistrationDate() {isNotNull();
        if (actual.getRegistrationDate() == null) {
            failWithMessage("Expected user to have a registration date, but it was null");
        }
        return this;
    }
}

当初,如果咱们从新的 UserAssert 类而不是从 AssertJ 库导入 assertThat 办法,咱们就能够应用新的、更易于浏览的断言。
创立像这样的自定义断言仿佛须要很多工作,但实际上只需几分钟即可实现。我深信投入这些工夫来创立可读的测试代码是值得的,即便之后它的可读性只是略微好一点。毕竟,咱们只编写一次测试代码,其他人(包含“将来的我”)必须在软件的生命周期中屡次浏览、了解和操作代码
如果依然感觉工作量太大,请查看 AssertJ 的断言生成器。

论断

在测试中启动 Spring 应用程序是有起因的,但对于一般的单元测试来说,这是没有必要的。因为更长的周转工夫,它甚至是无害的。相同,咱们应该以一种易于反对为其编写简略单元测试的形式构建咱们的 Spring bean。
Spring Boot Test Starter 附带 Mockito 和 AssertJ 作为测试库。
让咱们利用这些测试库来创立富裕表现力的单元测试!
最终模式的代码示例可在 github 上 找到。

退出移动版