Search code examples
javamockitostringbuilderjunit5

StringBuilder corrupted (internal field `count` = 0)


I write test for method which prints some output via some writer. Writer is just interface which implementation ConsoleWriterImpl is just wrapper for System.out.

Goal of the test: check that all info that should be printed has been passed to the Writer.printLine(Object str).

Problem

I use ArgumentCaptor<Object> argument = ArgumentCaptor.forClass(Object.class); for capturing input to Writer.printLine(Object str). Then get all inputs: List outputList = argument.getAllValues();.

That list consists of 2 types objects: Strings and StringBuilders. Then I want to convert all these objects to a one string in testing purposes. But all StringBuilders in outputList corrupted — they have count = 0. So, when I try to convert these StringBuilders I got empty strings. See below code of the test — I left comment where is problem.

Questions:

  • Why StringBuilders got corrupted here? If the reason is "Instances of StringBuilder are not safe for use by multiple threads."(source) — please explain how it influences in that case, I don't create threads manually...
  • How to deal with it?

ConsoleWriterImpl

public class ConsoleWriterImpl implements Writer {
    private PrintStream stream = System.out;

    public PrintStream getStream() {
        return stream;
    }

    public void setStream(PrintStream stream) {
        this.stream = stream;
    }

    @Override
    public void printLine(Object str) {
        stream.println(str);
    }
}

Test

import com.dtos.AccountDTO;
import com.dtos.ClientDTO;
import com.services.ClientService;
import com.view.io.Reader;
import com.view.io.Writer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.notNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class ClientViewImplTest {
    private Writer writer;
    private Reader reader;
    private ClientService clientService;
    private ClientViewImpl clientView;

    @BeforeEach
    void setUp() {
        writer = mock(Writer.class);
        reader = mock(Reader.class);
        clientService = mock(ClientService.class);
        clientView = new ClientViewImpl(writer, reader, clientService);
    }

    @SuppressWarnings("unchecked")
    @Test
    void displayAllClientsInfo() throws ParseException {
        // Given
        DateFormat df = new SimpleDateFormat("HH:mm:ss dd.MM.yyyy");
        List<ClientDTO> clients = new ArrayList<>();
        clients.add(new ClientDTO(1L, "John Smith", "client@example.com", Arrays.asList(
                new AccountDTO(10L, "JSmith1", "zzwvp0d9", df.parse("10:15:30 20.10.2017")),
                new AccountDTO(20L, "JSmith2", "mhjnbgfv", df.parse("10:15:30 5.5.2017")),
                new AccountDTO(30L, "JSmith3", "ytersds1", df.parse("15:00:30 12.10.2017"))
        )));
        clients.add(new ClientDTO(2L, "Jack Black", "jack@example.com", new ArrayList<>()));
        when(clientService.getAllClients()).thenReturn(clients);
        ArgumentCaptor<Object> argument = ArgumentCaptor.forClass(Object.class);
        // When
        clientView.displayAllClientsInfo();
        // Then
        verify(writer, atLeast(1)).printLine(argument.capture());
        List outputList = argument.getAllValues();

        StringBuilder str = new StringBuilder(2000);
        for (Object sb : outputList) {
            str.append(sb); // here we got empty strings in case sb type's is StringBuilder
        }

        String output = str.toString();
        assertAll(
                // Client
                () -> assertTrue(output.contains(Long.toString(1))),
                () -> assertTrue(output.contains("client@example.com")),
                () -> assertTrue(output.contains("John Smith")),
                // Accounts
                () -> assertTrue(output.contains(Long.toString(10))),
                () -> assertTrue(output.contains("JSmith1")),
                () -> assertTrue(output.contains("zzwvp0d9")),
                () -> assertTrue(output.contains(df.parse("10:15:30 20.10.2017").toString())),
                () -> assertTrue(output.contains(Long.toString(20))),
                () -> assertTrue(output.contains("JSmith2")),
                () -> assertTrue(output.contains("mhjnbgfv")),
                () -> assertTrue(output.contains(df.parse("10:15:30 5.5.2017").toString())),
                () -> assertTrue(output.contains(Long.toString(30))),
                () -> assertTrue(output.contains("JSmith3")),
                () -> assertTrue(output.contains("ytersds1")),
                () -> assertTrue(output.contains(df.parse("15:00:30 12.10.2017").toString())),
                // Client
                () -> assertTrue(output.contains(Long.toString(2))),
                () -> assertTrue(output.contains("jack@example.com")),
                () -> assertTrue(output.contains("Jack Black"))
        );
    }
}

Method under test clientView.displayAllClientsInfo()

public void displayAllClientsInfo() {
    final Collection<ClientDTO> clients = clientService.getAllClients();
    if (clients != null && clients.size() > 0) {
        writer.printLine(StringUtils.center("Clients", 55) + StringUtils.center("Accounts", 85));
        writer.printLine(StringUtils.repeat("-", 140));
        String columnsNames = String.format("%1$5s%2$25s%3$27s%4$3s%5$30s%6$25s%7$25s", "id", "e-mail", "name |",
                "id", "created", "login", "password");
        writer.printLine(columnsNames);
        writer.printLine(StringUtils.repeat("=", 140));
        StringBuilder clientInfo = new StringBuilder();
        for (ClientDTO client : clients) {
            clientInfo.append(String.format("%1$5d%2$25s%3$25s |", client.getId(), client.getEmail(),
                    client.getName()));
            writer.printLine(clientInfo);
            clientInfo.delete(0, clientInfo.length());
            List<AccountDTO> accounts = client.getAccounts();
            if (accounts != null && accounts.size() > 0) {
                for (AccountDTO ac : accounts) {
                    clientInfo.append(String.format("%1$60d%2$30s%3$25s%4$25s", ac.getId(), ac.getCreated(), ac.getLogin(),
                            ac.getPassword()));
                    clientInfo.setCharAt(56, '|');
                    writer.printLine(clientInfo);
                    clientInfo.delete(0, clientInfo.length());
                }
            }
            clientInfo.delete(0, clientInfo.length());
            writer.printLine(StringUtils.repeat("-", 140));
        }
    } else {
        writer.printLine("No data to display.");
        log.info("No data to display.");
    }
}

problem


Solution

  • A StringBuilder sets count to 0 rather than re-allocating or clearing its internal char[] array. This is what happened somewhere in the process, it is not any corruption or inconsistency.

    You captured some StringBuilder objects by the ArgumentCaptor. What the captor does is, it takes the argument that is provided to System.out.println(Object) call. In that call, toString() method is called on the object implicitly, but the capture takes the StringBuilder itself, which is then emptied. As @Sormuras mentioned, the delete method called on the builder is what caused the zero count.

    Solution? Well, maybe call toString() explicitly in ClientView.displayAllClientsInfo(), making an actual String out of the StringBuilder. String builder is only there to build a String, the problem is that thanks to the captor, you are still using it after its lifecycle pretty much ended already.

    Also, the use of StringBuilder inside displayAllClientsInfo method is pretty much pointless, you barely use any of its features at all, I'd just stick to the String.format alone.