Roberto Correia
Roberto Correia

Reputation: 1726

unit test a interface implementation with mock in Spring Boot

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 = "[email protected]";

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

Upvotes: 8

Views: 27286

Answers (5)

YourAboutMeIsBlank
YourAboutMeIsBlank

Reputation: 1907

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
}

Upvotes: 0

Christina
Christina

Reputation: 11

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.

Upvotes: 0

DwB
DwB

Reputation: 38300

This is just a variation on the @Yogesh Badke answer.

Although you are using spring at runtime, there is no need to use spring during the unit test. Instead, you can mock all the dependencies and set them to the mocks during test setup (using reflection or setters, if you have them).

Here is some example code:

import org.springframework.test.util.ReflectionTestUtils;

public class TestUserService
{
    private static final String VALUE_EMAIL = "test email value";

    private UserService classToTest;

    @Mock
    private User mockUser;

    @Mock
    private UserRepository mockUserRepository;

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

        classToTest = new UserService();

        doReturn(mockUser).when(mockUserRepository).findByEmail(VALUE_EMAIL);

        ReflectionTestUtils.setField(
            classToTest,
            "userRepository",
            mockUserRepository);
    }

    @Test
    public void findByEmail_goodEmailInput_returnsCorrectUser()
    {
        final User actualResult;


        actualResult = classToTest.findByEmail(VALUE_EMAIL);


        assertSame(
            mockUser,
            actualResult);
    }
}

Upvotes: 0

Yogesh Badke
Yogesh Badke

Reputation: 4587

You are running your tests with SpringRunner but for mocks you don't really need spring context. Try following code

// Using mockito runner
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

  @Mock
  UserRepository mockUserRepository;

  // Mockito will auto inject mockUserRepository mock to userService via constructor injection
  @InjectMocks
  UserService userService;

  @Test
  public void mustReturnUser() throws InterruptedException {
    String emailTest = "[email protected]";

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

Upvotes: 2

Plog
Plog

Reputation: 9622

In order for your UserServiceImpl to be autowired when annotating it with @InjectMocks then it needs to registered as a Spring bean itself. You can do this most simply by annotating your UserServiceImpl class with @Service.

This will ensure it is picked up by the component scan in your Spring boot configuration. (As long as the scan includes the package your service class is in!)

Upvotes: 3

Related Questions