Is there a way to fix a Test which should return a string made of ResultSet?

I have a function which creates a string from a result set. And the one that sends messages. You can see them below

public String createStringReport(ResultSet resultSet) throws SQLException{
    StringBuilder reportData = new StringBuilder("");
    int columns = 0;
    if(resultSet.getMetaData() != null) {
        columns = resultSet.getMetaData().getColumnCount();
        while (resultSet.next()) {
            for (int i = 1; i <= columns; i++) {
                reportData.append(resultSet.getString(i)).append("t");
            }
            reportData.append("n");
        }
    }
    return !reportData.toString().isEmpty() ? reportData.toString() : "No data";
}

private void sendStringReport(String reportData) {
    for (User u: users) {
        sender.send(u, reportData);
        log.info("send report - " + report.getName() + " to" + u.getName());
    }
}

I need to write a test which calls sender with the parameter string which was generated before. That’s what I have for now:

@Test
public void shouldGenerateStringReportWhenThreadRunHasData() throws SQLException {
    String result = "somestring";
    Report report = getReport(ReportType.STRING);
    ReportCreator reportCreator = new
            StringReportCreator(report, users, sender, dataSource, unexpectedErrorCounter);

    when(resultSet.getString(anyInt())).thenReturn(result);

    when(resultSet.next()).thenReturn(true).thenReturn(false);
    int columns = 2;
    final ResultSetMetaData rsmd = mock(ResultSetMetaData.class);
    when(resultSet.getMetaData()).thenReturn(rsmd);
    doReturn(rsmd).when(resultSet).getMetaData();

    StringBuilder reportData = new StringBuilder("");
    if(resultSet.getMetaData() != null) {
        while (resultSet.next()) {
            for (int i = 1; i <= columns; i++) {
                reportData.append(result).append("t");
            }
            reportData.append("n");
        }
    }

    reportCreator.run();
    verify(sender, Mockito.times(users.size())).send(any(User.class), eq(reportData.toString));
}

Unfortunately, I am told:

Argument(s) are different! Wanted:
telegramSender.send(
    <any reportbot.entity.User>,
    "somestring somestring  
somestring  somestring  
"
);
-> at ru.phoenixdnr.reportbot.ReportCreatorTest$Run.shouldGenerateStringReportWhenThreadRunHasData(ReportCreatorTest.java:233)
Actual invocations have different arguments:
telegramSender.send(
    User(id=1, idTelegram=2, name=892e0f69-c10e-4961-b22e-11de333fbb55, email=73507828-9c7a-4026-8599-5e6c8c62e3c7),
    "No data"
);

Why is the reportData empty? How can I fix this? I’ve been trying for a week now, but nothing helps.

Answer

The problem appears to be that you:

  • set up the mock result set,
  • read the results out of it while constructing the expected output,
  • call the method you are trying to test, with the same result set.

When the method under test runs, it finds that the result set has already been exhausted (or at least .next() returns false), so there is no more data to read from it. Hence you get no data.

You wouldn’t have this problem if the code that generates the expected text used a separate result set.

One way to fix this is to move all of your ResultSet mock setup code into a separate method, which creates and returns the mock ResultSet. That way you can call this method twice to get two different mock ResultSets, one to create a result set to use to create the expected result, and once to pass to the report creator.

Another fix is to generate the expected output without using the mock ResultSet. You know that ResultSet.getMetaData() won’t return null, so the null-check is unnecessary, and you also know how many rows are in the mock ResultSet, so you can replace the while loop with a for loop.