unit test a interface implementation with mock in Spring Boot

Viewed 17843

I'm trying to write a simple unit test for a service in Spring Boot.

The service calls a method on a repository which returns an instance of User. I'm trying to mock the repository, because I want to test only the service.

So, the code for Repository:

public interface UserRepository extends MongoRepository<User, String> {
  User findByEmail(String email);
}

Service interface:

public interface UserService {
  @Async
  CompletableFuture<User> findByEmail(String email) throws InterruptedException;
}

Service implementation:

@Service
public class UserServiceImpl implements UserService {
  private UserRepository userRepository;

  // dependency injection
  // don't need Autowire here
  // https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-spring-beans-and-dependency-injection.html
  public UserServiceImpl(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Async
  public CompletableFuture<User> findByEmail(String email) throws InterruptedException {
    User user = userRepository.findByEmail(email);
    return CompletableFuture.completedFuture(user);
  }
}

Unit Test:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

  @InjectMocks
  UserService userService;

  @Mock
  UserRepository mockUserRepository;

  @Before
  public void setUp() {
    MockitoAnnotations.initMock(this);
  }

  @Test
  public void mustReturnUser() throws InterruptedException {
    String emailTest = "foo@bar.com";

    User fakeUser = new User();
    fakeUser.setEmail(emailTest);

    when(mockUserRepository.findByEmail(emailTest)).thenReturn(fakeUser);

    User user = userService.findByEmail(emailTest).join();
    assertThat(user).isEqualTo(fakeUser);

    verify(mockUserRepository).findByEmail(emailTest);
  }
}

When I run this test, I got a MockitoException:

org.mockito.exceptions.base.MockitoException: 
Cannot instantiate @InjectMocks field named 'userService'.
...
Caused by: org.mockito.exceptions.base.MockitoException: the type 'UserService' is an interface.

Instead of using the interface, I tried to use the real implementation; changing the test like this:

@InjectMocks
UserServiceImpl userService;

Now, the test passes with success, but this don't appear be right (at least for me). I like to test the UserService that Spring Boot is using (suppose that in a new version of my system, I implement a new UserServicePostgreSQLImpl - now I'm using MongoDB). (edit: see the bottom edit in the question)

I changed the Unit Test as follows:

@Autowired
@InjectMocks
UserService userService;

but now I got a test failure:

Expected :model.User@383caf89
Actual   :null

For some reason, when I use @Autowired, the UserRepository mock doesn't work. If I change the emailTest to use a real email in my database, the test passes. When I use @Autowired, the test is using the real UserRepository and not a Mock version of UserRepository.

Any help?

Edit: looking at the answers from @msfoster and @dunni, and thinking better, I believe that the correct way is to test every implementation (in my example, use UserServiceImpl userService).

5 Answers

If interface is implemented by more than one class, then use the qualifier name (example below) in Junit beans.xml file to run the respective Junit test case.

Example:

 @Autowired
    @Qualifier("animal")
    private Animal animals;

In Junit beans.xml

  <bean id="animal" class="com.example.abc.Lion"/>

where Lion is the implementation class for the Interface Animal.

You need to @InjectMocks for the implementation class. Not the interface class.

Example:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

  @Mock
  UserRepository mockUserRepository;

  @InjectMocks
  UserServiceImpl userServiceImpl; ------> This is important
}
Related