【问题标题】:How to test only one part of method which part's test is written in test class with spock如何仅测试方法的一部分,该部分的测试是用spock在测试类中编写的
【发布时间】:2020-03-27 11:16:43
【问题描述】:

我在服务类中有注册方法,我尝试为成功案例编写单元测试以防止重复电子邮件

public void signUp(UserDTO userDTO) {
        logger.info("ActionLog.Sign up user.Start");
        Optional<UserEntity> checkedEmail = userRepository.findByEmail(userDTO.getEmail());
        System.out.println(checkedEmail);
        if (checkedEmail.isPresent()) {
            System.out.println("check email: "+checkedEmail);
            logger.error("ActionLog.WrongDataException.Thrown");
            throw new WrongDataException("This email already exists");
        }

        String password = new BCryptPasswordEncoder().encode(userDTO.getPassword());
        UserEntity customerEntity = UserEntity
                .builder()
                .name(userDTO.getName())
                .surname(userDTO.getSurname())
                .username(userDTO.getEmail())
                .email(userDTO.getEmail())
                .password(password)
                .role(Role.ROLE_USER)
                .build();

        userRepository.save(customerEntity);
        logger.info("ActionLog.Sign up user.Stop.Success");

    }

这是我的测试课

class UserServiceImplTest extends Specification {

    UserRepository userRepository
    AuthenticationServiceImpl authenticationService
    UserServiceImpl userService

    def setup() {
        userRepository = Mock()
        authenticationService = Mock()
        userService = new UserServiceImpl(userRepository, authenticationService)
    }

    def "doesn't throw exception if email doesn't exist in database"() {

        given:
        def userDto = new UserDTO()
        def entity = new Optional<UserEntity>()
        userDto.setEmail("example@mail.ru")
        1 * userRepository.findByEmail(userDto.getEmail()) >> entity
        1 * entity.isPresent() >> false

        when: "send dto object to service "
        userService.signUp(userDto)


        then: ""
        notThrown(WrongDataException)
    }


}

测试失败,因为它为我提供了 ByCryptPasswordEncoder 的 NPE: 但我不编写集成测试,我只需要测试重复的电子邮件成功和失败案例

java.lang.NullPointerException
    at org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder.encode(BCryptPasswordEncoder.java:108)
    at az.gdg.msauth.service.impl.UserServiceImpl.signUp(UserServiceImpl.java:41)
    at az.gdg.msauth.service.UserServiceImplTest.doesn't throw exception if email doesn't exist in database(UserServiceImplTest.groovy:35)

但是,我在服务类中评论这些

String password = new BCryptPasswordEncoder().encode(userDTO.getPassword());
        UserEntity customerEntity = UserEntity
                .builder()
                .name(userDTO.getName())
                .surname(userDTO.getSurname())
                .username(userDTO.getEmail())
                .email(userDTO.getEmail())
                .password(password)
                .role(Role.ROLE_USER)
                .build();

        userRepository.save(customerEntity);

它给了我

Too few invocations for:

1 * entity.isPresent() >> false   (0 invocations)

Unmatched invocations (ordered by similarity):

None


Too few invocations for:

1 * entity.isPresent() >> false   (0 invocations)

Unmatched invocations (ordered by similarity):

None

我该如何解决这个问题?

【问题讨论】:

  • 您需要引入一个抽象级别,使您能够控制新的 BCryptPasswordEncoder().encode。然后使用依赖注入能够在单元测试期间提供模拟,以及生产代码的真实实现。 BCryptPasswordEncoder 是否实现了接口?
  • 当然,如果你要做1 * entity.isPresent() &gt;&gt; false,你需要给实体一个密码让Encoder编码?
  • 我是 spock 框架的新手。我只想测试块的成功和失败情况,而不是密码编码器。因为它属于spring。不幸的是,当测试开始时,它会从上到下读取所有代码在服务中,当到达编码器时会抛出异常。我的问题是,我如何才能测试只有阻塞
  • 欢迎来到 SO。请阅读MCVE 文章,了解如何提出好的问题,以及如果您想获得好的答案,为什么 MCVE 如此有价值。谢谢。
  • 至于你的NullPointerException,没有MCVE我只能推测userDTO.getPassword()null,因为在你的测试中你忘了初始化密码,正如蒂姆已经说过的那样。

标签: java unit-testing groovy spock


【解决方案1】:

好的,我又查看了您的代码,并在本地创建了许多虚拟类以使其编译和运行,试图弄清楚您的用例是什么。实际上你应该在你的MCVE 中显示,但我想我现在有一个想法。 (感谢 COVID-19,我很无聊,因为今天没有地方可以去见朋友。)

我看到了两个直接的问题:

  1. 您无法定义交互 1 * entity.isPresent() &gt;&gt; false,因为您的 entity 不是模拟或间谍。此外,您使用 Optional 的私有构造函数来初始化 entity,这也很丑陋,并且在 Groovy 之外无法工作。此外,不必检查是否在可选对象上调用了isPresent(),只需确保方法调用返回false。只需编写 def entity = Optional.empty() 并删除交互即可更轻松地实现这一点。

  2. 正如我在评论中所说,您只需确保 DTO 具有通过 userDto.setPassword("pw") 或类似设置的密码,即可摆脱用于加密器调用的 NullPointerException。您的测试将如下所示:

package de.scrum_master.stackoverflow.q60884910

import spock.lang.Specification

class UserServiceImplTest extends Specification {
  UserRepository userRepository
  AuthenticationServiceImpl authenticationService
  UserServiceImpl userService

  def setup() {
    userRepository = Mock()
    authenticationService = Mock()
    userService = new UserServiceImpl(userRepository, authenticationService)
  }

  def "doesn't throw exception if email doesn't exist in database"() {
    given:
    def userDto = new UserDTO()
    def entity = Optional.empty()
    userDto.setEmail("example@mail.ru")
    userDto.setPassword("pw")
    1 * userRepository.findByEmail(userDto.getEmail()) >> entity
//    1 * entity.isPresent() >> false

    when: "send dto object to service "
    userService.signUp(userDto)

    then: ""
    notThrown(WrongDataException)
  }

}

我也认为没有必要检查userRepository.findByEmail(..) 是否被实际调用并且它是用特定参数调用的。我认为这是对单元测试的过度规范。在更改被测方法的内部实现时,您必须对其进行调整。我认为这里只指定存根结果就足够了。如果你也重新组织一下代码,测试看起来像这样:

package de.scrum_master.stackoverflow.q60884910

import spock.lang.Specification

class UserServiceImplTest extends Specification {
   def userRepository = Stub(UserRepository)
   def authenticationService = Stub(AuthenticationServiceImpl)
   def userService = new UserServiceImpl(userRepository, authenticationService)

  def "doesn't throw exception if email doesn't exist in database"() {
    given: "a user DTO"
    def userDto = new UserDTO()
    userDto.email = "example@mail.ru"
    userDto.password = "pw"

    and: "a user repository not finding any user by e-mail"
    userRepository.findByEmail(_) >> Optional.empty()

    when: "signing up a new user"
    userService.signUp(userDto)

    then: "no duplicate e-mail address exception is thrown"
    notThrown WrongDataException
  }
}

请注意,我将Mock() 更改为Stub(),因为我们不再检查任何交互(调用次数)。如果您觉得 userRepository 需要此功能,您可以再次将其还原为具有 1 * ... 交互的模拟。

P.S.:我仍然认为您正在测试的方法将从将其重构为更小的方法中受益,然后您可以轻松地单独存根和/或测试。 BCryptPasswordEncoder 的依赖注入或将密码检查分解到单独的方法中,如果您想模拟/存根编码器或其某些测试的结果,也可能会有所帮助。

【讨论】:

  • 先生,非常感谢您的明确解释。它解决了我的问题,我明白我应该做什么
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-07
  • 1970-01-01
  • 1970-01-01
  • 2017-12-30
  • 2016-11-17
  • 1970-01-01
相关资源
最近更新 更多