Перейти к основному содержимому

Как TDD реализовать список в Java

· 7 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим настраиваемую реализацию списка с использованием процесса разработки через тестирование (TDD).

Это не введение в TDD, поэтому мы предполагаем, что у вас уже есть некоторое базовое представление о том, что это значит, и постоянный интерес к тому, чтобы стать лучше.

Проще говоря, TDD — это инструмент проектирования, позволяющий нам управлять нашей реализацией с помощью тестов .

Небольшой отказ от ответственности — здесь мы не сосредотачиваемся на создании эффективной реализации — просто используем ее как предлог для демонстрации практики TDD.

2. Начало работы

Во-первых, давайте определим скелет для нашего класса:

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

Класс CustomList реализует интерфейс List , поэтому он должен содержать реализации для всех методов, объявленных в этом интерфейсе.

Для начала мы можем просто предоставить пустые тела для этих методов. Если метод имеет возвращаемый тип, мы можем вернуть произвольное значение этого типа, например, null для Object или false для boolean .

Для краткости мы будем опускать необязательные методы, а также некоторые обязательные методы, которые используются нечасто.

3. Циклы TDD

Разработка нашей реализации с помощью TDD означает, что нам нужно сначала создать тестовые примеры , тем самым определив требования для нашей реализации. Только после этого мы создадим или исправим код реализации , чтобы эти тесты прошли.

В очень упрощенном виде три основных шага в каждом цикле:

  1. Написание тестов – определите требования в виде тестов
  2. Внедрение функций — проведите тесты, не уделяя слишком много внимания элегантности кода.
  3. Рефакторинг — улучшите код, чтобы его было легче читать и поддерживать, продолжая проходить тесты.

Мы пройдемся по этим циклам TDD для некоторых методов интерфейса List , начиная с самых простых.

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 . Вот метод добавления , с которого мы начинаем:

@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 :

@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 :

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

6.1. Первый цикл

Давайте взглянем на первый тест для этого метода, который проверяет значение одного элемента в списке:

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

assertEquals("foreach", 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. Метод добавления

Это метод добавления , который мы создали в разделе 4:

@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("foreach");
list.add(".com");
Object element1 = list.get(0);
Object element2 = list.get(1);

assertEquals("foreach", 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 .