39

For example, I have this method in UserService:

  @Override
  @Transactional
  public UserDto create(UserDto userDto) {

    User dbUser = userRepository.findOne(userDto.getId());

    if (dbUser != null) {
      throw new AuthException(AuthException.ErrorCode.DUPLICATE_USER_EXCEPTION);
    }

    User oneByLogin = userRepository.findOneByLogin(userDto.getLogin());
    if (oneByLogin != null) {
      throw new AuthExceptionAuthException.ErrorCode.DUPLICATE_LOGIN_EXCEPTION);
    }

    User newUser = new User();
    newUser.setGuid(UUID.randomUUID().toString());
    newUser.setInsertDate(new Date());
    newUser.setFirstName(userDto.getFirstName());
    newUser.setLastName(userDto.getLastName());
    newUser.setLogin(userDto.getLogin());
    newUser.setPassword(userDto.getPassword());
    newUser.setAuthToken(TokenGenerator.nextToken());
    newUser.setAuthTokenCreatedDate(new Date());

    User savedUser = userRepository.save(newUser);

    userDto.setAuthToken(savedUser.getAuthToken());
    log.info("User {0} created", savedUser.getLogin());
    return userDto;
  }

How can I create unit test for this method? I tried next:

  @Test
  public void createUser() {

    UserDto userDtoRequest = new UserDto();
    userDtoRequest.setLogin("Alex");
    userDtoRequest.setPassword("123");

    UserDto found = userService.create(userDtoRequest);
    assertThat(found.getAuthToken()).isNotEmpty();
}

I have next logic:

  1. Test start
  2. User dbUser = userRepository.findOne(userDto.getId()); dbUser = NULL
  3. if (dbUser != null) and if (oneByLogin != null) skip
  4. create new user and set data
  5. User savedUser = userRepository.save(newUser); savedUser = NULL

On this step, I have a problem because I cannot mock userRepository.save(newUser).

newUser create inside the method. and test fail.

savedUser.getAuthToken() - savedUser == NULL

I can rewrite:

    userRepository.save(newUser);
    userDto.setAuthToken(newUser.getAuthToken());
    log.info("User {0} created", newUser.getLogin());
    return userDto;

but what if I want to use the returned object savedUser?

Little Helper
  • 1,870
  • 3
  • 12
  • 20
ip696
  • 6,574
  • 12
  • 65
  • 128
  • 1
    You are testing your service, so you should mock its dependencies like the repository. By mocking the method `save`, you can specify the object it returns. – grape_mao Jul 09 '18 at 14:40
  • 1
    @ grape_mao and how do I do it? how do I call the method userRepository.save(newUser) if newUser object create inside service method and I have not it in my test. when(userService.save(???)).thenReturn(???); – ip696 Jul 09 '18 at 14:57
  • If you can't write test for your method, it's a sign that you need to divide it into several parts, and tests them separately. – Alexander Polozov Jul 09 '18 at 14:58
  • @ip696 you have two options. 1. ignore the argument passed in, return a `User` with a token. 2. use something like `doAnswer` to mock the method, so that you can catch the argument. – grape_mao Jul 09 '18 at 15:03
  • Possible duplicate of [Unable to mock Spring-Data-JPA repositories with Mockito](https://stackoverflow.com/questions/37655915/unable-to-mock-spring-data-jpa-repositories-with-mockito) – K.Nicholas Jul 09 '18 at 19:39

4 Answers4

58

You need to do this.

when(userRepository.save(Mockito.any(User.class)))
                .thenAnswer(i -> i.getArguments()[0]);

And now you can obtain user which you pass as argument.

spajdo
  • 901
  • 9
  • 20
  • 4
    same procedure but with more readable function returnFirstArg() could be found here https://stackoverflow.com/questions/2684630/making-a-mocked-method-return-an-argument-that-was-passed-to-it/11840286#11840286 – schoener Jun 17 '20 at 21:02
  • is it possible to combine this method to make it return a mock? In my service method, after performing a save, I try to return the generated id. But this id appears to be a null value when training the repository mock like this. – Yannick Mussche Feb 21 '23 at 10:42
14

You can do the following:

@RunWith(MockitoJUnitRunner.class)
public class SimpleTest {

  @Mock
  private UserRepository mockedUserRepository;

  // .. your test setup

  @Test
  public void testYourMethod() {

     User userToReturnFromRepository = new User();
     userToReturnFromRepository.setAuthToken(YOUR_TOKEN);
     when(mockedUserRepository.save(any(User.class)).thenReturn(userToReturnFromRepository);

     UserDto found = userService.create(userDtoRequest);

     // ... your asserts

  }

}

with this approach you just have to make sure your mockedUserRepository gets injected into your class under test (e.g. in the constructor).

rieckpil
  • 10,470
  • 3
  • 32
  • 56
  • Is this considering Entity to DTO conversion inside service? repo's' save returns entity NOT DTO, service should translate it – Kris Swat Oct 16 '20 at 20:56
8

You need to write multiple test cases in order to test different scenarios.

Scenario 1: when findOne returns a not null object:

@Test(expected=AuthException.class)
public void testCreateUserWhenAvailable()    {
     //Create one sample userDto object with test data
     when(mockedUserRepository.findOne(userDto.getId())).thenReturn(new User());
     userService.create(userDto);
}

Scenario 2: when findOneByLogin returns a null object:

@Test(expected=AuthException.class)
public void testCreateUserWhenLoginAvailable()    {
     //Create one sample userDto object with test data
     when(mockedUserRepository.findOne(userDto.getId())).thenReturn(null);
     when(mockedUserRepository.findOneByLogin(userDto.getId())).thenReturn(new User());

     userService.create(userDto);
}

Scenario 2: when save is done:

@Test

public void testCreateUserWhenSaved()    {
     //Create one sample userDto object with test data
     when(mockedUserRepository.findOne(userDto.getId())).thenReturn(null);
     when(mockedUserRepository.findOneByLogin(userDto.getId())).thenReturn(null);

     //Create sample User object and set all the properties
     User newUser=new User();
     when(mockedUserRepository.save(Mockito.any(User.class)).thenReturn(newUser);
     User returnedUser=userService.create(userDto);
     //You have two ways to test, you can either verify that save method was invoked by 
     //verify method
     verify(mockedUserRepository, times(1)).save(Mockito.any(User.class));
     //or by assertion statements, match the authToken in the returned object to be equal 
     //to the one set by you in the mocked object
     Assert.assertEquals(returnedUser.getAuthToken(),newUser.getAuthToken());
}
Kayvan Tehrani
  • 3,070
  • 2
  • 32
  • 46
codeLover
  • 2,571
  • 1
  • 11
  • 27
3

Just a two cents from me on how to create JPA repository save method with generating random IDs for fields with @GeneratedValue.

/**
 * Mocks {@link JpaRepository#save(Object)} method to return the
 * saved entity as it was passed as parameter and add generated ID to it.
 * If ID could not be generated, it will be ignored.
 * If parameter already has and ID, it will be overridden.
 */
private <T, V> void mockSave(JpaRepository<T, V> repository) {
    when(repository.save(any())).thenAnswer(i -> {
        Object argument = i.getArgument(0);
        Arrays.stream(argument.getClass().getDeclaredFields())
                .filter(f -> f.getAnnotation(GeneratedValue.class) != null)
                .forEach(f -> enrichGeneratedValueField(argument, f));
        return argument;
    });
}

So here you pass the desired repository as a parameter and the methods calls enrichGeneratedValueField for all fields annotated with @GeneratedValue annotation. Here's implementation of this method:

private void enrichGeneratedValueField(Object argument, Field field) {
    try {
        if (field.getType().isAssignableFrom(Integer.class)) {
            FieldUtils.writeField(field, argument, Math.abs(random.nextInt()), true);
        } else {
            FieldUtils.writeField(field, argument, mock(field.getType()), true);
        }
    } catch (Exception ignored) {
    }
}

In this example I used IDs only which are the type of Integer, but fill free to add your desired type of IDs.

Praytic
  • 1,771
  • 4
  • 21
  • 41
  • This worked like a charm, thank you so much. I additionally replaced the any() with an ArgumentCaptor, which made it super easy to assert things with the returns. – Eki Apr 03 '23 at 02:33