Please see my example below. I've got a class (MyService
) in which I'm trying to unit test a particular method (handleStudentActivity
). MyService
has dependencies on InputValidator
and StudentRepository
(as well as some others - which are out of scope of my test).
So in my test class, MyServiceTest
I created @Mock
objects for these and used @InjectMocks
on my test subject.
Inside the unit test method, any calls to the dependent objects I mocked the invocations as follows...
doReturn(student).when(repository).save(student);
doThrow(InputValidatonException.class).when(validator).validateStudentData(student);
And my test case passes as expected (It throws an exception as the student name is blank).
LaterI modified the source code as follows, inside InputValidator
removed the body of validateStudentData
method
public void validateStudentData(Student student) {}
I ran the test again and it is passed this time too. I expected the test case to fail as the current code is not throwing an exception.
Is it the right way to write unit tests? Because the modifications to the source code do not break existing tests.
Or is it okay since the change is outside the test target ( which is MyService
) and changes in dependent objects are taken care of by their corresponding tests?
Here are my classes (removed non-relevant lines)
1. MyService
@Service
public class MyService {
@Autowired
private InputValidator validator;
@Autowired
private StudentRepository repository;
//More autowirings here
public void handleStudentActivity(Student student) {
//Some logic here...
Student savedEntry = repository.save(student);
validator.validateStudentData(savedEntry);
//Some logic here for handleStudentActivity with savedEntry
}
//Other stuff...
}
2. InputValidator
@Component
public class InputValidator {
@Autowired
private ManagementProfilerClient profilerClient;
@Autowired
private MonitorClient monitorClient;
public void validateStudentData(Student student) {
if (StringUtils.isBlank(student.getName()))
throw new InputValidatonException("Student name can't be blank.");
// Other validations follows...
}
//Other validator methods here
}
3. MyServiceTest
public class MyServiceTest {
@Mock
private InputValidator validator;
@Mock
private StudentRepository repository;
@InjectMocks
private MyService subject;
@BeforeClass
public void setup() {
MockitoAnnotations.initMocks(this);
}
@Test(expectedExceptions = InputValidatonException.class)
public void testHandleStudentActivityTrowsExceptionForInvalidStudentInput() {
// Arrange
Student student = new Student();
student.setName(""));
doReturn(student).when(repository).save(student); //Mock repository method call
doThrow(InputValidatonException.class).when(validator).validateStudentData(student); // Mock validator call
//Act
subject.handleStudentActivity(student);
}
}
CodePudding user response:
Your unit test tests a single unit of code. For MyServiceTest, the unit is MyService.
Your test does not include InputValidator, which has positive and negative effects: It's positive because a bug in InputValidator wouldn't cause your test to fail, and because if InputValidator is slow or has heavy dependencies, you won't need to worry about those aspects when writing a focused test of MyService. However, it's negative because your unit test alone won't tell you that the whole system is working, and might mean that you write improper assumptions into your unit test setup.
Your Mockito mocks are an automated type of test double that encodes assumptions made about the world outside your system-under-test (SUT): Your test doubles will likely run much faster and with fewer dependencies, but they are also prone to going stale as they diverge from your real implementation (as you described when clearing out your validateStudentData
method). This isn't inherently a problem with Mockito, as any fake implementation would suffer from the same assumptions, but in any case it does mean that your test might leave your code less safe than you thought it was.
This doesn't mean that your unit test is wrong, or that it should necessarily use a real InputValidator: Instead, you should not rely only on unit tests, but rather add system tests or integration tests which use fewer test doubles and more real systems. You'll likely want fewer of these system tests than unit tests, as they might be heavier to run and less specific in identifying a problem if they fail, but they are still necessary to ensure that your systems work together when assembled as in your real application.
For what it's worth, I wrote another answer on a similar Software Engineering StackExchange question here: Are (database) integration tests bad?. Other contributors also answered the question, so you can compare our observations there.