乐趣区

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

原文地址:https://reflectoring.io/unit-…

编写好的单元测试能够被看成一个很难把握的艺术。但好消息是反对单元测试的机制很容易学习。

本文给你提供在 Spring Boot 应用程序中编写好的单元测试的机制,并且深刻技术细节。

咱们将带你学习如何以可测试的形式创立 Spring Bean 实例,而后探讨如何应用 MockitoAssertJ,这两个包在 Spring Boot 中都为了测试默认援用了。

本文只探讨单元测试。至于集成测试,测试 web 层和测试长久层将会在接下来的系列文章中进行探讨。

代码示例

本文附带的代码示例地址:spring-boot-testing

应用 Spring Boot 进行测试系列文章

这个教程是一个系列:

  1. 应用 Spring Boot 进行单元测试(本文)
  2. 应用 Spring Boot 和 @WebMvcTest 测试 SpringMVC controller 层
  3. 应用 Spring Boot 和 @DataJpaTest 测试 JPA 长久层查问
  4. 通过 @SpringBootTest 进行集成测试

如果你喜爱看视频教程,能够看看 Philip 的课程:测试 Spring Boot 应用程序课程

依赖项

本文中,为了进行单元测试,咱们会应用 JUnit Jupiter(Junit 5)MockitoAssertJ。此外,咱们会援用 Lombok 来缩小一些模板代码:

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

MockitoAssertJ 会在 spring-boot-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();}

}

这个测试类在我的电脑上须要大略 4.5 秒来执行一个空的 Spring 我的项目。

然而一个好的单元测试仅仅须要几毫秒。否则就会妨碍 TDD(测试驱动开发)流程,这个流程提倡“测试 / 开发 / 测试”。

然而就算咱们不应用 TDD,期待一个单元测试太久也会毁坏咱们的注意力。

执行上述的测试方法事实上仅须要几毫秒。剩下的 4.5 秒是因为 @SpringBootTest 通知了 Spring Boot 要启动整个 Spring Boot 应用程序上下文。

所以咱们启动整个应用程序仅仅是因为要把 RegisterUseCase 实例注入到咱们的测试类中。启动整个应用程序可能耗时更久,假如应用程序更大、Spring须要加载更多的实例到应用程序上下文中。

所以,这就是为什么不要在单元测试中应用Spring。坦白说,大部分编写单元测试的教程都没有应用Spring Boot

创立一个可测试的类实例

而后,为了让 Spring 实例有更好的测试性,有几件事是咱们能够做的。

属性注入是不好的

让咱们以一个反例开始。思考下述类:

@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 实例参数的构造函数来容许构造函数注入。在这个单元测试中,咱们当初能够创立这样一个实例(或者咱们之后要探讨的 Mock 实例)并通过构造函数注入了。

当创立生成利用上下文的时候,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();}

}

还有局部的确,就是如何模仿测试类所依赖的 UserReposity 实例,咱们不想依赖实在的类,因为这个类须要一个数据库连贯。

应用 Mockito 来模仿依赖项

当初事实上的规范模仿库是 Mockito。它提供至多两种形式来创立一个模仿 UserRepository 实例,来填补前述代码的空白。

应用一般 Mockito 来模仿依赖

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

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

这会从外界创立一个看起来像 UserRepository 的对象。默认状况下,办法被调用时不会做任何事件,如果办法有返回值,会返回null

因为 userRepository.save(user) 返回 null,当初咱们的测试代码 assertThat(savedUser.getRegistrationDate()).isNotNull() 会报空指针异样(NullPointerException)。

所以咱们须要通知 Mockito,当userRepository.save(user) 调用的时候返回一些货色。咱们能够用动态的 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 注入模仿对象。因为 JUnit 不会主动实现,MockitoExtension则通知 Mockito 来评估这些 @Mock 注解。

这个后果和调用 Mockito.mock() 办法一样,凭集体品尝抉择即可。然而请留神,通过应用 MockitoExtension,咱们的测试用例被绑定到测试框架。

咱们能够在 RegisterUseCase 属性上应用 @InjectMocks 注解来注入实例,而不是手动通过构造函数结构。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();

有很多测试用例,只须要像这样进行很小的改变就能大大提高可了解性。所以,让咱们在 test/sources 中创立咱们自定义的断言吧:

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;
  }
}

当初,如果咱们不是从 AssertJ 库间接导入,而是从咱们自定义断言类 UserAssert 引入 assertThat 办法的话,咱们就能够应用新的、更可读的断言。

创立一个这样自定义的断言类看起来很费时间,然而其实几分钟就实现了。我置信,将这些工夫投入到创立可读性强的测试代码中是值得的,即便之后它的可读性只有一点点进步。咱们编写测试代码就一次,然而之后,很多其他人(包含将来的我)在软件生命周期中,须要浏览、了解而后操作这些代码很屡次。

如果你还是感觉很麻烦,能够看看断言生成器

论断

只管在测试中启动 Spring 应用程序也有些理由,然而对于个别的单元测试,它不必要。有时甚至无害,因为更长的周转工夫。换言之,咱们应该应用更容易反对编写一般单元测试的形式构建 Spring 实例。

Spring Boot Test Starter附带 MockitoAssertJ作为测试库。让咱们利用这些测试库来创立富裕表现力的单元测试!

退出移动版