sacamano
sacamano

Reputation: 11

Mockito isNotNull passes null

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

Upvotes: 1

Views: 2237

Answers (2)

Jeff Bowman
Jeff Bowman

Reputation: 95634

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.

Upvotes: 1

radar
radar

Reputation: 605

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

Upvotes: 1

Related Questions