Search code examples
javamockitojava-streamjooq

Unit test: Collection being processed with for-loop but not with streams


Issue with unit test is that the same collection is processed differently in a stream and in a for loop. The collection in both cases is empty (data.size() = 0), but in the Case 1 that collection is somehow processed, in other words it will step in the for-loop. In Case 2, that collection is just skipped (which is expected since it's empty).

Tests are using Mockito, and Result<Record> is comming for JOOQ.

The tests are old and unchanged, the only change is going from for-loop to stream.

Case 1

private SearchResult iterateData(
      Result<Record> data, ...) {

      for (Record record : data) {
           doSomething(record);
    }

Case 2

private SearchResult iterateData(
      Result<Record> data, ...) {
      data.stream().forEach(record -> doSomething(record)); 

Screenshot of Case 1 for loop example

Mocked Result object

private DefaultSearchRequestModel rowSpecificValuesTestSetup(
      parameters...) {
    

    DefaultSearchRequestModel searchRequest = new DefaultSearchRequestModel(
        Arrays.asList(....),
        Collections.singletonList(
            new SearchFilter(
                "test",
                Collections.singletonList(...)));

    List<Column> columns =
        this.searchService.filterUserAllowedColumns(...);

    Condition searchCondition =
        this.searchRepositoryMock.getSearchConditions(...);

    List<TableJoinMapping> joinMappings = ColumnHelper.getColumnTranslateDeviceJoinMappings(
        columns,
        searchRequest.getFilters());

    Result<Record> deviceDataResultMock = Mockito.mock(Result.class);
    Iterator<Record> resultIterator = Mockito.mock(Iterator.class);
    final Table fromTableMock = Mockito.mock(Table.class);
    when(resultIterator.hasNext()).thenReturn(true, false);
    Record recordMock = Mockito.mock(Record.class);
    when(resultIterator.next()).thenReturn(recordMock);
    when(deviceDataResultMock.iterator()).thenReturn(resultIterator);
    when(recordMock.get(CONTRACTID)).thenReturn(contractId);
   ...
when(this.userPermissions.getAccessPermissions()).thenReturn(searchRequest.getColumns().stream().map
        (name -> Column.findByName(name).getId()).collect(
        Collectors.toList()));
    when(this.searchRepositoryMock.getCurrentTable(companyId))
        .thenReturn(fromTableMock);
    when(recordMock.get(TYPEID)).thenReturn(financialTypeId);
    when(this.searchRepositoryMock.getDeviceData(
        ArgumentMatchers.anyList(),
        ArgumentMatchers.anyList(),
        any(),
        any(),
        eq(searchRequest.getPageSize()),
        eq(searchRequest.getPage()),
        eq(searchRequest.getSortCriterias()),
        eq(fromTableMock),
        ArgumentMatchers.anyList(),
        eq(Optional.empty()),
        eq(this.dslContextMock)))
        .thenReturn(deviceDataResultMock);

    return searchRequest;
  }```

Solution

  • Why it didn't work

    You're mocking Result.iterator():

    when(deviceDataResultMock.iterator()).thenReturn(resultIterator);
    

    But you didn't mock Result.spliterator(), or at least I didn't see it, which is what's being called by Result.stream(), which is just Collection.stream():

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
    

    So, you'll have to mock the spliterator() method as well, and probably a few others, too! Alternatively, tell Mockito to call default methods:

    Can you make mockito (1.10.17) work with default methods in interfaces?

    A comment on mocking in general

    I'm not convinced that mocking the jOOQ API is a very good idea. The jOOQ API is vast, and you'll likely forget to mock this or that method as this question here has aptly shown. With your current setup, you're planning on updating your mock every time you project a new column? E.g. you're doing this:

    when(recordMock.get(DEVICEID.getName()).thenReturn(deviceId);
    

    What if the column is renamed? Or another column is projected? You'll have to update this test. That feels like quite the chore, and it's very error prone.

    While jOOQ itself has JDBC mocking capabilities, please consider the bold disclaimer on that manual page:

    Disclaimer: The general idea of mocking a JDBC connection with this jOOQ API is to provide quick workarounds, injection points, etc. using a very simple JDBC abstraction. It is NOT RECOMMENDED to emulate an entire database (including complex state transitions, transactions, locking, etc.) using this mock API. Once you have this requirement, please consider using an actual database product instead for integration testing, rather than implementing your test database inside of a MockDataProvider

    When working with databases, it's usually best to resort to running integration tests, see the following resources for some details:

    Of course, you can write a few smoke tests to ensure jOOQ works correctly if you don't trust jOOQ (jOOQ being an external dependency). But the jOOQ unit and integration tests are vast, so in general, you should be able to trust core types like Result or Record to do the right thing for you. What you really want to test is your query correctness, and that, you can only integration test against an actual database instance.