Автор оригинала: Nguyen Nam Thai.
1. Обзор
В этом уроке мы рассмотрим пользовательскую реализацию List с использованием процесса разработки на основе тестирования (TDD).
Это не введение в TDD, поэтому мы предполагаем, что у вас уже есть некоторое базовое представление о том, что это значит, и устойчивый интерес к тому, чтобы стать лучше в этом.
Проще говоря, TDD-это инструмент проектирования, позволяющий нам управлять нашей реализацией с помощью тестов .
Краткое заявление об отказе от ответственности – мы не фокусируемся на создании эффективной реализации здесь – просто используем его в качестве предлога для демонстрации практики TDD.
2. Начало работы
Во-первых, давайте определим скелет для нашего класса:
public class CustomListimplements List { private Object[] internal = {}; // empty implementation methods }
Класс Custom List реализует интерфейс List , следовательно, он должен содержать реализации для всех методов, объявленных в этом интерфейсе.
Для начала мы можем просто предоставить пустые тела для этих методов. Если метод имеет тип возвращаемого значения, мы можем вернуть произвольное значение этого типа, например null для объекта или false для boolean .
Для краткости мы опустим необязательные методы, а также некоторые обязательные методы, которые не часто используются.
3. Циклы TDD
Разработка нашей реализации с помощью TDD означает , что сначала нам нужно создать тестовые случаи , тем самым определив требования к нашей реализации. Только затем мы создадим или исправим код реализации , чтобы эти тесты прошли.
В очень упрощенном виде три основных этапа в каждом цикле::
- Написание тестов – определение требований в виде тестов
- Реализация функций – сделайте так, чтобы тесты проходили, не слишком фокусируясь на элегантности кода
- Рефакторинг – улучшите код, чтобы его было легче читать и поддерживать, все еще проходя тесты
Мы рассмотрим эти циклы TDD для некоторых методов интерфейса List , начиная с самых простых.
4. Метод isEmpty
Метод isEmpty , вероятно, является самым простым методом, определенным в интерфейсе List . Вот наша начальная реализация:
@Override public boolean isEmpty() { return false; }
Этого начального определения метода достаточно для компиляции. Тело этого метода будет “вынуждено” улучшаться, когда будут добавлены все новые и новые тесты.
4.1. Первый Цикл
Давайте напишем первый тестовый случай, который гарантирует, что метод isEmpty возвращает true , когда список не содержит ни одного элемента:
@Test public void givenEmptyList_whenIsEmpty_thenTrueIsReturned() { List
Данный тест не выполняется, так как метод isEmpty всегда возвращает false . Мы можем заставить его пройти, просто перевернув возвращаемое значение:
@Override public boolean isEmpty() { return true; }
4.2. Второй Цикл
Чтобы подтвердить, что метод isEmpty возвращает false , когда список не пуст, нам нужно добавить хотя бы один элемент:
@Test public void givenNonEmptyList_whenIsEmpty_thenFalseIsReturned() { List
Теперь требуется реализация метода 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 , позволяющая компилировать Пользовательский список класс:
@Override public int size() { return 0; }
5.1. Первый Цикл
Используя существующий метод add , мы можем создать первый тест для метода size , проверяя, что размер списка с одним элементом равен 1 :
@Test public void givenListWithAnElement_whenSize_thenOneIsReturned() { List
Тест завершается неудачно, так как возвращается метод 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
Тест пройдет с этой реализацией метода 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:
@Override public boolean add(E element) { internal = new Object[] { element }; return false; }
7.1. Первый Цикл
Ниже приведен простой тест, который проверяет возвращаемое значение add :
@Test public void givenEmptyList_whenElementIsAdded_thenGetReturnsThatElement() { List
Мы должны изменить метод add , чтобы вернуть true для прохождения теста:
@Override public boolean add(E element) { internal = new Object[] { element }; return true; }
Хотя тест проходит, метод add еще не охватывает все случаи. Если мы добавим второй элемент в список, существующий элемент будет потерян.
7.2. Второй Цикл
Вот еще один тест, добавляющий требование, чтобы список мог содержать более одного элемента:
@Test public void givenListWithAnElement_whenAnotherIsAdded_thenGetReturnsBoth() { List
Тест завершится неудачей, так как метод 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 .