如何只测试方法的一部分,该部分的测试是用 spock 在测试 class 中编写的

How to test only one part of method which part's test is written in test class with spock

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

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

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)

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

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

我该如何解决这个问题?

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

我看到两个紧迫的问题:

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

  2. 正如我在评论中所说,您只需确保 DTO 具有通过 userDto.setPassword("pw") 或相似的。您的测试将如下所示:

package de.scrum_master.Whosebug.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.Whosebug.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.: 我仍然认为您的被测方法可以通过将其重构为更小的方法而获益,然后您可以轻松地对 and/or 单独进行测试。 BCryptPasswordEncoder 的依赖注入或将密码检查分解为一个单独的方法也可能会有所帮助,如果你想 mock/stub 编码器或其某些测试的结果。