1

Thanks in advance for the help - I am new to mockito but have spent the last day looking at examples and the documentation but haven't been able to find a solution to my problem, so hopefully this is not too dumb of a question.

I want to verify that deleteLogs() calls deleteLog(Path) NUM_LOGS_TO_DELETE number of times, per path marked for delete. I don't care what the path is in the mock (since I don't want to go to the file system, cluster, etc. for the test) so I verify that deleteLog was called NUM_LOGS_TO_DELETE times with any non-null Path as a parameter. When I step through the execution however, deleteLog gets passed a null argument - this results in a NullPointerException (based on the behavior of the code I inherited).

Maybe I am doing something wrong, but verify and the use of isNotNull seems pretty straight forward...here is my code:

MonitoringController mockController = mock(MonitoringController.class);

// Call the function whose behavior I want to verify
mockController.deleteLogs();

// Verify that mockController called deleteLog the appropriate number of times
verify(mockController, Mockito.times(NUM_LOGS_TO_DELETE)).deleteLog(isNotNull(Path.class));

Thanks again

sacamano
  • 11
  • 3

2 Answers2

1

I've never used isNotNull for arguments so I can't really say what's going wrong with you code - I always use an ArgumentCaptor. Basically you tell it what type of arguments to look for, it captures them, and then after the call you can assert the values you were looking for. Give the below code a try:

    ArgumentCaptor<Path> pathCaptor = ArgumentCaptor.forClass(Path.class);
    verify(mockController, Mockito.times(NUM_LOGS_TO_DELETE)).deleteLog(pathCaptor.capture());
    for (Path path : pathCaptor.getAllValues()) {
        assertNotNull(path);
    }
radar
  • 595
  • 2
  • 11
  • I'll give this a go and see how it works out, thank you! – sacamano Jul 18 '14 at 20:22
  • It seems my problem is a lack of understanding about mockito and mocking in general - this suggestion still calls the method with a null param but that is due to my error in writing the test I think. – sacamano Jul 18 '14 at 20:41
1

As it turns out, isNotNull is a method that returns null, and that's deliberate. Mockito matchers work via side effects, so it's more-or-less expected for all matchers to return dummy values like null or 0 and instead record their expectations on a stack within the Mockito framework.

The unexpected part of this is that your MonitoringController.deleteLog is actually calling your code, rather than calling Mockito's verification code. Typically this happens because deleteLog is final: Mockito works through subclasses (actually dynamic proxies), and because final prohibits subclassing, the compiler basically skips the virtual method lookup and inlines a call directly to the implementation instead of Mockito's mock. Double-check that methods you're trying to stub or verify are not final, because you're counting on them not behaving as final in your test.


It's almost never correct to call a method on a mock directly in your test; if this is a MonitoringControllerTest, you should be using a real MonitoringController and mocking its dependencies. I hope your mockController.deleteLogs() is just meant to stand in for your actual test code, where you exercise some other component that depends on and interacts with MonitoringController.

Most tests don't need mocking at all. Let's say you have this class:

class MonitoringController {
  private List<Log> logs = new ArrayList<>();

  public void deleteLogs() {
    logs.clear();
  }

  public int getLogCount() {
    return logs.size();
  }
}

Then this would be a valid test that doesn't use Mockito:

@Test public void deleteLogsShouldReturnZeroLogCount() {
  MonitoringController controllerUnderTest = new MonitoringController();
  controllerUnderTest.logSomeStuff(); // presumably you've tested elsewhere
                                      // that this works
  controllerUnderTest.deleteLogs();
  assertEquals(0, controllerUnderTest.getLogCount());
}

But your monitoring controller could also look like this:

class MonitoringController {
  private final LogRepository logRepository;

  public MonitoringController(LogRepository logRepository) {
    // By passing in your dependency, you have made the creator of your class
    // responsible. This is called "Inversion-of-Control" (IoC), and is a key
    // tenet of dependency injection.
    this.logRepository = logRepository;
  }

  public void deleteLogs() {
    logRepository.delete(RecordMatcher.ALL);
  }

  public int getLogCount() {
    return logRepository.count(RecordMatcher.ALL);
  }
}

Suddenly it may not be so easy to test your code, because it doesn't keep state of its own. To use the same test as the above one, you would need a working LogRepository. You could write a FakeLogRepository that keeps things in memory, which is a great strategy, or you could use Mockito to make a mock for you:

@Test public void deleteLogsShouldCallRepositoryDelete() {
  LogRepository mockLogRepository = Mockito.mock(LogRepository.class);
  MonitoringController controllerUnderTest =
      new MonitoringController(mockLogRepository);

  controllerUnderTest.deleteLogs();
  // Now you can check that your REAL MonitoringController calls
  // the right method on your MOCK dependency.
  Mockito.verify(mockLogRepository).delete(Mockito.eq(RecordMatcher.ALL));
}

This shows some of the benefits and limitations of Mockito:

  • You don't need the implementation to keep state any more. You don't even need getLogCount to exist.
  • You can also skip creating the logs, because you're testing the interaction, not the state.
  • You're more tightly-bound to the implementation of MonitoringController: You can't simply test that it's holding to its general contract.
  • Mockito can stub individual interactions, but getting them consistent is hard. If you want your LogRepository.count to return 2 until you call delete, then return 0, that would be difficult to express in Mockito. This is why it may make sense to write fake implementations to represent stateful objects and leave Mockito mocks for stateless service interfaces.
Community
  • 1
  • 1
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • Thank you very much for the response - I think you've highlighted a few areas where my understanding is lacking regarding mocking and the way mockito works. I think my main struggle is not knowing what I am asking mockito to do. The method is not final by the way, but I believe I am doing the mocking incorrectly. I'll have another look. – sacamano Jul 18 '14 at 20:20
  • You're welcome! Don't worry, using Mockito well takes a while to figure out. My short version is that sometimes, to test a component, you sometimes have to test how it interacts with other systems—so you use the real component and use Mockito to imitate the other systems. I highly recommend [Mocks Aren't Stubs](http://martinfowler.com/articles/mocksArentStubs.html), an article by Martin Fowler, which should give you an idea about how mock-based tests differ from state-based tests. – Jeff Bowman Jul 18 '14 at 20:25
  • Too late to edit last comment - mockController.deleteLogs() is code in the MonitoringController class (so I guess its no surprise that mockito is actually calling that method?). So in order to test the MonitoringController in my MonitoringControllerTest class I should create an actual MonitoringController object? What would I be mocking if this is the case - class members? I'll read that article as well, thanks! – sacamano Jul 18 '14 at 20:38
  • Edited the answer heavily to reflect how the design of your class may influence whether or not to use Mockito in your class's tests. Hope this helps, and good luck! – Jeff Bowman Jul 18 '14 at 20:58
  • Thanks so much - the Fowler article and your response has clarified a lot of my misunderstanding about mocking - it's not as straightforward as I thought. My team member failed to mention some of the details behind mocking when they said "use mockito for testing", and this controller is not the most intuitive piece of code to test anyway. Well thanks again - my noob status prevents me from upvoting but I'll be sure to do so when I can. – sacamano Jul 18 '14 at 21:13
  • Did some minor refactoring to allow for mocking the classes dependencies and mockito worked for the method I have been trying to test! No whether this is a smell for poor design or the need for a different testing methodology is another question :P – sacamano Jul 18 '14 at 21:29
  • You're welcome! When you're satisfied with your answers on a question, click the checkmark under the voting buttons of the answer most helpful to you. This ["accepts the answer"](http://stackoverflow.com/help/accepted-answer) and also marks the question as "answered" on the list page. Have a good weekend! – Jeff Bowman Jul 18 '14 at 21:39