简体   繁体   中英

Is it a proper way to write unit tests by mocking all the dependencies of an object under test?

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

}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM