如何在Java中对列表实现进行TDD

2023/05/09

1. 概述

在本教程中,我们将使用测试驱动开发(TDD)流程逐步完成自定义List实现。

这不是TDD的介绍,所以我们假设你已经对它的含义有一些基本的了解,并且有持续的兴趣去改进它。

简单地说,TDD是一种设计工具,使我们能够借助测试来驱动我们的实现

一个快速的免责声明-我们在这里并不专注于创建高效的实现,只是以此为借口来展示TDD的实践。

2. 开始

首先,让我们为我们的类定义骨架:

public class CustomList<E> implements List<E> {
    private Object[] internal = {};
    // empty implementation methods
}

CustomList类实现了List接口,因此它必须包含在该接口中声明的所有方法的实现。

首先,我们可以为这些方法提供空体。如果方法有返回类型,我们可以返回该类型的任意值,例如Object为null或boolean为false。

为了简洁起见,我们将省略可选方法,以及一些不经常使用的强制性方法。

3. TDD循环

使用TDD开发我们的实现意味着我们需要首先创建测试用例,从而定义我们的实现需求。只有这样我们才能创建或修复实现代码以使这些测试通过

以非常简化的方式,每个循环中的三个主要步骤是:

  1. 编写测试-以测试的形式定义需求
  2. 实现功能-让测试通过,而无需过分关注代码的优雅性
  3. 重构-改进代码,使其更易于阅读和维护,同时仍能通过测试

我们将从最简单的方法开始,针对List接口的某些方法完成这些TDD循环。

4. isEmpty方法

isEmpty方法可能是List接口中定义的最直接的方法。以下是我们的开始实现:

@Override
public boolean isEmpty() {
    return false;
}

这个初始方法定义足以进行编译。当添加越来越多的测试时,该方法的主体将“被迫”改进。

4.1 第一个周期

让我们编写第一个测试用例,它确保isEmpty方法在列表不包含任何元素时返回true:

@Test
public void givenEmptyList_whenIsEmpty_thenTrueIsReturned() {
    List<Object> list = new CustomList<>();

    assertTrue(list.isEmpty());
}

给定的测试失败,因为isEmpty方法总是返回false。我们可以通过翻转返回值来使其通过:

@Override
public boolean isEmpty() {
    return true;
}

4.2 第二个周期

要确认isEmpty方法在列表不为空时返回false,我们需要至少添加一个元素:

@Test
public void givenNonEmptyList_whenIsEmpty_thenFalseIsReturned() {
    List<Object> list = new CustomList<>();
    list.add(null);

    assertFalse(list.isEmpty());
}

现在需要实现add方法。这是我们开始的add方法:

@Override
public boolean add(E element) {
    return false;
}

由于未对列表的内部数据结构进行任何更改,因此此方法实现不起作用。让我们更新它以存储添加的元素:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return false;
}

我们的测试仍然失败,因为isEmpty方法没有得到增强。让我们这样做:

@Override
public boolean isEmpty() {
    if (internal.length != 0) {
        return false;
    } else {
        return true;
    }
}

此时非空测试通过。

4.3 重构

到目前为止我们看到的两个测试用例都通过了,但是isEmpty方法的代码可以更优雅。

让我们重构它:

@Override
public boolean isEmpty() {
    return internal.length == 0;
}

我们可以看到测试通过了,因此isEmpty方法的实现现在已经完成了。

5. size方法

这是我们启用CustomList类编译的size方法的开始实现:

@Override
public int size() {
    return 0;
}

5.1 第一个周期

使用现有的add方法,我们可以为size方法创建第一个测试,验证具有单个元素的列表的大小是否为1:

@Test
public void givenListWithAnElement_whenSize_thenOneIsReturned() {
    List<Object> list = new CustomList<>();
    list.add(null);

    assertEquals(1, list.size());
}

测试失败,因为size方法返回0。让我们通过一个新的实现让它通过:

@Override
public int size() {
    if (isEmpty()) {
        return 0;
    } else {
        return internal.length;
    }
}

5.2 重构

我们可以重构size方法使其更优雅:

@Override
public int size() {
    return internal.length;
}

此方法的实现现已完成。

6. get方法

这是get的开始实现:

@Override
public E get(int index) {
    return null;
}

6.1 第一个周期

让我们看一下此方法的第一个测试,它验证列表中单个元素的值:

@Test
public void givenListWithAnElement_whenGet_thenThatElementIsReturned() {
    List<Object> list = new CustomList<>();
    list.add("tuyucheng");
    Object element = list.get(0);

    assertEquals("tuyucheng", element);
}

测试将通过get方法的这个实现通过:

@Override
public E get(int index) {
    return (E) internal[0];
}

6.2 重构

通常,我们会在对get方法进行额外改进之前添加更多测试。这些测试需要List接口的其他方法来实现正确的断言。

但是,这些其他方法还不够成熟,因此我们打破了TDD循环并创建了get方法的完整实现,这实际上并不难。

很容易想象get必须使用index参数从指定位置的内部数组中提取一个元素:

@Override
public E get(int index) {
    return (E) internal[index];
}

7. add方法

这是我们在第4节中创建的add方法:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return false;
}

7.1 第一个周期

以下是验证add返回值的简单测试:

@Test
public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() {
    List<Object> list = new CustomList<>();
    boolean succeeded = list.add(null);

    assertTrue(succeeded);
}

我们必须修改add方法以返回true才能使测试通过:

@Override
public boolean add(E element) {
    internal = new Object[] { element };
    return true;
}

虽然测试通过了,但add方法还没有涵盖所有情况。如果我们向列表中添加第二个元素,现有元素将丢失。

7.2 第二个周期

这是另一个测试,增加了列表可以包含多个元素的要求:

@Test
public void givenListWithAnElement_whenAnotherIsAdded_thenGetReturnsBoth() {
    List<Object> list = new CustomList<>();
    list.add("tuyucheng");
    list.add(".com");
    Object element1 = list.get(0);
    Object element2 = list.get(1);

    assertEquals("tuyucheng", element1);
    assertEquals(".com", element2);
}

测试将失败,因为当前形式的add方法不允许添加多个元素。

让我们更改实现代码:

@Override
public boolean add(E element) {
    Object[] temp = Arrays.copyOf(internal, internal.length + 1);
    temp[internal.length] = element;
    internal = temp;
    return true;
}

实现足够优雅,因此我们不需要重构它。

8. 总结

本教程通过测试驱动的开发过程来创建自定义List实现的一部分。使用TDD,我们可以逐步实现需求,同时将测试覆盖率保持在很高的水平。此外,该实现保证是可测试的,因为它是为了让测试通过而创建的。

请注意,本文中创建的自定义类仅用于演示目的,不应在实际项目中采用。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章