Reputation: 1726
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
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
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
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
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
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