Search code examples
javaunit-testinggroovyspock

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


I have sign up method in service class and I try to write unit test for success case for prevent dublicate email

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");

    }

And this is my test 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)
    }


}

test is failed , because it gives me NPE for ByCryptPasswordEncoder: but I don't write integration test and I have to test only dublicate email success and fail cases

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)

But, I comment these in service 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);

It gives me

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

How can I solve this problem?


Solution

  • Okay, I looked at your code some more and created lots of dummy classes locally in order to make it compile and run, trying to figure out what your use case is. Actually you should have shown in your MCVE, but I think I have an idea now. (Thanks to COVID-19 I was bored because there was no place to go and meet friends today.)

    I see two immediate problems:

    1. You cannot define an interaction 1 * entity.isPresent() >> false because your entity is not a mock or spy. Furthermore, you use a private constructor for Optional in order to initialise entity, which is also ugly and would not work outside of Groovy. Furthermore, it is not necessary to check that isPresent() was called on the optional object, only made sure that the method call returns false. This can be achieved much easier by just writing def entity = Optional.empty() and deleting the interaction instead.

    2. As I said in a comment, you can get rid of the NullPointerException for the encryptor call by just making sure that the DTO has a password set via userDto.setPassword("pw") or similar. Your test would then look like this:

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

    I also think it is not necessary to check that userRepository.findByEmail(..) is actually called and that it is called with a specific parameter. I think this is over-specification for a unit test. You would have to adjust it when changing the internal implementation of the method under test. I think here it should be enough to just specify the stub result. If you also reorganise the code a bit, the test looks like this:

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

    Please note that I changed Mock() to Stub() because we do not check any interactions (number of calls) anymore. If you feel you need this for userRepository, you can revert it to be a mock with an 1 * ... interaction again.

    P.S.: I still think your method under test would profit from refactoring it into smaller methods which then you can easily stub and/or test separately. Dependency injection for the BCryptPasswordEncoder or factoring out the password check into a separate method might also be helpful if you like to mock/stub the encoder or its result for some tests.