java - 如何简化测试?

标签 java unit-testing junit mockito

我想测试我的方法IOConsoleWriterImpl.displayAllClientsInfo(List<ClientEntity> clients)它打印数据。

displayAllClientsInfo大致有以下结构:

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

因此,据我了解,为了模拟两个 foreach 循环(使用 Mockito),我需要模拟两个迭代器。

有测试:

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

我相信有一个很好的方法来避免代码重复(我指的是测试的第二段和第三段)。或者我不应该担心,一切都很好?

最佳答案

这一切看起来有点尴尬,所以我可以理解你对如何简化它的想法。

您可以只传入 ClientInfo 实例的实际列表,而不是模拟的列表。例如:

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);

但这似乎是显而易见的,所以也许有一些原因您还没有这样做(也许构建这些类有点尴尬或过于冗长)。

或者,您可以通过使代码对测试更加友好来避免“测试设置”的尴尬。例如,您可以将 IOConsoleWriterImpl 中的“写入”职责提取到注入(inject)到 IOConsoleWriterImpl 中的接口(interface)中。像这样的事情:

// 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);
    }
}

然后在您的测试用例中,您可以将模拟的 Writer 注入(inject)到 IOConsoleWriter 中,并验证它是否以您预期的状态调用。

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

io.displayAllClientsInfo(clients);

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

类似地,您可以提供 Writer 的 stub 实现,它记录所给的内容,然后对该 stub 的内容进行断言。例如:

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"))
    );

更新 1:基于您的评论以及您提供的实际类(class)的链接。

看起来IOConsoleWriterImpl.displayAllClientsInfo()有两个职责:

  • 询问 ClientInfo 集合并确定要打印的内容
  • 打印输出(包括标题和格式)

这让我认为提取 Writer 接口(interface)会带来一些好处:

  • 提升 IOConsoleWriterImpl 的 SRP
  • 简化了IOConsoleWriterImpl
  • 有利于对 IOConsoleWriterImpl 进行更简单的测试,因为 IOConsoleWriterTest 可以只关注客户端询问职责
  • 有利于更简单地测试“写入”行为;您可以编写一个SystemOutWriterTest,它只关注编写者的职责

但是,你已经完成的事情是可以的;它提供了对 IOConsoleWriterImpl.displayAllClientsInfo() 的良好覆盖,尽管它相当冗长,但仍然可读。

总之,我建议传入实际的列表是最简单的更改,它(a)在功能上与您当前拥有的相同,并且(b)涉及更少的设置/更易于阅读。除此之外,我关于提取新界面背后的“写入”行为的建议将简化 IOConsoleWriterImpl 并使您的测试更加细粒度(每个测试用例可能更小并且更容易推理)并且这个我认为,改变是很容易实现的。当然,您对改变的兴趣可能会有所不同;)并且这里的好处不足以令人信服地要求进行这种改变。

关于java - 如何简化测试?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46588933/

相关文章:

java - setter 方法和返回 getter 的问题

java - 同一 JUnit 测试中的 assertTrue 和 assertFalse

java - Eclipse 给出关于不返回整数的错误

java - 从迭代器中获取键值

java - Java 8 中的覆盖率报告

c# - 函数不会返回任何内容

django - 在没有数据库的情况下使用 django-discover-runner

java - (Spring MVC + Hibernate 4 + 测试 4) Autowiring DAO 返回 NULL

java - 获取 SpringJUnit4ClassRunner 中所有类型的 Bean

java - 当链接实体名称不是从父实体名称派生时,如何避免使用 OneToOne 映射出现 NullPointerException?