1. Обзор

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

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

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

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

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

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

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

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

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

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

3. Циклы TDD

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

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

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

  1. Writing tests – define requirements in the form of tests
  2. Implementing features – make the tests pass without focusing too much on the elegance of the code
  3. Refactoring – improve the code to make it easier to read and maintain while still passing the tests

4. Метод isEmpty

Метод isEmpty, вероятно, самый простой метод, определенный в интерфейсе List. Вот наша начальная реализация:

Этого начального определения метода достаточно для компиляции. Тело этого метода будет «вынуждено» улучшаться, когда будет добавляться все больше и больше тестов.

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

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

Давайте напишем первый тестовый пример, который гарантирует, что метод isEmpty возвращает true, когда список не содержит ни одного элемента:

Данный тест завершается неудачей, так как метод isEmpty всегда возвращает false. Мы можем заставить его пройти, просто перевернув возвращаемое значение:

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

    assertTrue(list.isEmpty());
}

4.2. Второй цикл

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

Чтобы подтвердить, что метод isEmpty возвращает false, когда список не пуст, нам нужно добавить хотя бы один элемент:

Теперь требуется реализация метода add. Вот метод добавления, с которого мы начинаем:

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

    assertFalse(list.isEmpty());
}

Реализация этого метода не работает, так как во внутреннюю структуру данных списка не вносятся никакие изменения. Давайте обновим его, чтобы сохранить добавленный элемент:

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

Наш тест по-прежнему не работает, так как метод isEmpty не был улучшен. Сделаем так:

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

В этот момент непустой тест проходит.

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

4.3. Рефакторинг

Оба тестовых случая, которые мы видели до сих пор, проходят успешно, но код метода isEmpty мог бы быть более элегантным.

Проведем рефакторинг:

Мы видим, что тесты проходят успешно, поэтому реализация метода isEmpty завершена.

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

5. Метод size

Это наша начальная реализация метода size, позволяющая компилировать класс CustomList:

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

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

Используя существующий метод добавления, мы можем создать первый тест для метода размера, проверяя, что размер списка с одним элементом равен 1:

Тест не проходит как метод размера возвращает 0. Давайте сделаем это с новой реализацией:

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

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

5.2. Рефакторинг

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

Мы можем реорганизовать метод size, чтобы сделать его более элегантным:

Реализация этого метода завершена.

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

6. Метод get

Вот начальная реализация get:

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

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

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

Тест пройдет с этой реализацией метода get: ~~ ~

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

    assertEquals("baeldung", element);
}

6.2. Улучшение

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

Обычно мы добавляем больше тестов, прежде чем вносить дополнительные улучшения в метод get. Этим тестам потребуются другие методы интерфейса List для реализации правильных утверждений.

«Однако эти другие методы еще недостаточно развиты, поэтому мы прерываем цикл TDD и создаем полную реализацию метода get, что на самом деле не очень сложно.

Легко представить, что get должен извлечь элемент из внутреннего массива в указанном месте, используя параметр index:

7. Метод add

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

Это метод add, который мы создали в разделе 4. :

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

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

Ниже приведен простой тест, который проверяет возвращаемое значение add:

Мы должны изменить метод add, чтобы он возвращал true, чтобы тест прошел успешно: проходит, метод add еще не охватывает все случаи. Если мы добавим в список второй элемент, существующий элемент будет потерян.

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

    assertTrue(succeeded);
}

7.2. Второй цикл

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

Вот еще один тест, добавляющий требование, чтобы список мог содержать более одного элемента:

Тест завершится ошибкой, поскольку метод add в его текущей форме не позволяет добавить более одного элемента. добавлен.

Давайте изменим код реализации:

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

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

Реализация достаточно элегантна, поэтому нам не нужно ее рефакторить.

8. Заключение

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

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

Обратите внимание, что пользовательский класс, созданный в этой статье, используется только в демонстрационных целях и не должен использоваться в реальном проекте.

Полный исходный код этого руководства, включая методы тестирования и реализации, опущенные для краткости, можно найти на GitHub.

«

The complete source code for this tutorial, including the test and implementation methods left out for the sake of brevity, can be found over on GitHub.