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:
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.");
}
}
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.