简体   繁体   中英

How can I simplify the test?

I want to test my method IOConsoleWriterImpl.displayAllClientsInfo(List<ClientEntity> clients) that it prints data.

displayAllClientsInfo has approximately the following structure:

for (ClientEntity client : clients) {
    System.out.println(client.getName());
    List<AccountEntity> accounts = client.getAccountEntities();
    for (AccountEntity account : accounts){
        System.out.println(account.getLogin());
    }
}

So, as I understand it, in order to mock two foreach loops (with Mockito), I need to mock two iterators.

There is the test:

    @Test
    void isDisplayAllClientsInfoPrintData() {
        //Given
        List<ClientEntity> clients = mock(List.class);
        List<AccountEntity> accounts = mock(List.class);
        Iterator<ClientEntity> clientIterator = mock(Iterator.class);
        Iterator<AccountEntity> accountIterator = mock(Iterator.class);
        ClientEntity client = mock(ClientEntity.class);
        AccountEntity account = mock(AccountEntity.class);

        when(clientIterator.hasNext()).thenReturn(true, false);
        when(clientIterator.next()).thenReturn(client);
        when(clients.iterator()).thenReturn(clientIterator);

        when(accountIterator.hasNext()).thenReturn(true, false);
        when(accountIterator.next()).thenReturn(account);
        when(accounts.iterator()).thenReturn(accountIterator);

        when(clients.size()).thenReturn(1);
        when(client.getAccountEntities()).thenReturn(accounts);
        when(client.getId()).thenReturn(1L);
        when(client.getEmail()).thenReturn("client@example.com");
        when(client.getName()).thenReturn("John Smith");
        when(account.getId()).thenReturn(2L);
        when(account.getCreated()).thenReturn(LocalDateTime.of(2017,5,25,12,59));
        when(account.getLogin()).thenReturn("JSmith");
        when(account.getPassword()).thenReturn("zzwvp0d9");

        //When
        IOConsoleWriter io = new IOConsoleWriterImpl();
        io.displayAllClientsInfo(clients);

        //Then
        String output = outputStream.toString();
        assertAll(
                () -> assertTrue(output.contains(Long.toString(1))),
                () -> assertTrue(output.contains("client@example.com")),
                () -> assertTrue(output.contains("John Smith")),
                () -> assertTrue(output.contains(Long.toString(2))),
                () -> assertTrue(output.contains(LocalDateTime.of(2017,5,25,12,59).toString())),
                () -> assertTrue(output.contains("JSmith")),
                () -> assertTrue(output.contains("zzwvp0d9"))
        );
    }

I believe that there is a nice way to avoid code duplication (I mean the second and third paragraphs of the test). Or I should not worry and everything is fine?

It all looks a little bit awkward so I can understand your thinking about how to simplify it.

You could just pass in an actual list of ClientInfo instances rather than a mocked one. For example:

List<ClientInfo> clientInfos = new ArrayList<>();

clients.add(new ClientInfo(1L, "client@example.com", "John Smith", 
    Arrays.asList(
        new Account(2L, LocalDateTime.of(2017,5,25,12,59), "JSmith", "zzwvp0d9"))
    )
);

io.displayAllClientsInfo(clients);

But that seems obvious so perhaps there is some reason why you are not already doing that (maybe constructing these classes is a bit awkward or overly verbose).

Alternatively, you could side step the 'test setup' awkwardness by making your code more test friendly. For example, you could extract the 'write' responsibility out of the IOConsoleWriterImpl into an interface which you inject into IOConsoleWriterImpl . Something like this:

// extract from IOConsoleWriterImpl

public IOConsoleWriterImpl(Writer writer) {
    this.writer = writer;
}

public void displayAllClientsInfo(ClientEntity clients) {
    for (ClientEntity client : clients) {
        System.out.println(client.getName());
        List<AccountEntity> accounts = client.getAccountEntities();
        for (AccountEntity account : accounts){
            writer.write(account.getLogin());
        }
    }
}

// a new interface to extract the 'writing' behaviour out of IOConsoleWriterImpl
public interface Writer {
    void write(String output);
}

// a sysout implementation of the Writer interface
public class SystemOutWriter implements Writer {
    @Override
    public void write(String output) {
        System.out.println(output);
    }
}

Then in your test case you could inject a mocked Writer into the IOConsoleWriter and verify that it is called with your expected state.

Writer writer = Mockito.mock(Writer.class);
IOConsoleWriter io = new IOConsoleWriterImpl(writer);

io.displayAllClientsInfo(clients);

Mockito.verify(writer).write(...);

Similarly, you could provide a stubbed implementation of Writer which records what it is given and then assert on the contents of this stub. For Example:

public class RecordingWriter implements Writer {
    private List<String> recordings = new ArrayList<>();

    @Override
    public void write(String output) {
        recordings.add(output);
    }

    public boolean contains(String incoming) {
        return recordings.contains(incoming);
    }
}

RecordingWriter writer = new RecordingWriter();
IOConsoleWriter io = new IOConsoleWriterImpl(writer);

io.displayAllClientsInfo(clients);

assertAll(
            () -> assertTrue(writer.contains(Long.toString(1))),
            () -> assertTrue(writer.contains("client@example.com")),
            () -> assertTrue(writer.contains("John Smith")),
            () -> assertTrue(writer.contains(Long.toString(2))),
            () -> assertTrue(writer.contains(LocalDateTime.of(2017,5,25,12,59).toString())),
            () -> assertTrue(writer.contains("JSmith")),
            () -> assertTrue(writer.contains("zzwvp0d9"))
    );

Update 1 : based on your comment and the links you provided to the actual classes at play here.

It looks like IOConsoleWriterImpl.displayAllClientsInfo() has two responsibilities:

  • Interrogating a collection of ClientInfo and working out what to print
  • Printing output (including headers and formatting)

This makes me think that extracting a Writer interface would offer a few benefits:

  • Promotes SRP for IOConsoleWriterImpl
  • Simplifies IOConsoleWriterImpl
  • Facilitates simpler testing for IOConsoleWriterImpl since IOConsoleWriterTest could focus on just the client interrogation responsibilties
  • Facilitates simpler testing of the 'writing' behaviour; you could write a SystemOutWriterTest which focuses on just the writer responsibilties

However, what you've already done is ok; it provides good coverage of IOConsoleWriterImpl.displayAllClientsInfo() and although it is quite verbose it is still readable.

In summary, I'd suggest that passing in an actual List is the simplest change that is (a) functionally equivalent to what you currently have and (b) involves less setup / is easier to read. Beyond that my suggestion about extracting the 'writing' behaviour behind a new interface will simplify IOConsoleWriterImpl and make your testing more fine grained (with each test case being, perhaps, smaller and easier to reason about) and this change would - I think - be quite simple to make. Of course, your appetite for change might be different ;) and the benefits here aren't sufficiently compelling to demand this change.

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