Рубрики
Без рубрики

Корзина с фруктами: Написание тестов, Часть I

База данных создана, фреймворк и инструменты на месте, процесс TDD отработан, все волшебство готово – пора… Помечено как тестирование, java.

Тележка с Фруктами (Серия из 4 частей)

База данных создана, фреймворк и инструменты на месте, процесс TDD отработан, вся магия готова – время произносить заклинания!

Подразделение. . или около того Я думал

Пока мы будем добавлять наш код, нам все еще нужно было определиться с базовой архитектурой. Например, если мы хотим протестировать Фруктовую модель, что ж, тогда мы должны решить, что нам нужна фруктовая модель в первую очередь. Есть небольшая проблема с курицей или яйцом: мы действуем по принципу, что тесты будут стимулировать производство, но все же, чтобы даже написать тест, мы должны знать, что мы тестируем…

Итак, в примере с фруктовой моделью, что стоит на первом месте: модель или тесты? Курица или яйцо? Ответ: модель. Как бы. В нашем коллективном мозгу это стоит на первом месте. Тесты и производственный код зависят друг от друга. Способ написания тестов, безусловно, определяет способ написания кода, но для того, чтобы тесты вообще были написаны, мы должны определиться с базовой архитектурой, которую мы тестируем. Другими словами, мы должны решить, что мы тестируем.

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

Поэтому мы должны выяснить, что нам понадобится и, возможно, что более важно, что нам не понадобится. И вот тут мы сталкиваемся с ЯГНИ: Тебе Это Не Понадобится.

ЯГНИК исходит из методологии экстремального программирования и означает, что мы должны писать только тот код, который нам нужен сейчас, а не то, что мы думаем нам понадобится в будущем. Да, мы хотим, чтобы наш код был расширяемым, но мы также не хотим создавать будущую функциональность, которая не является тем, что мы хотим предоставить как часть нашего минимально жизнеспособного продукта. Хотя это может показаться не дальновидным, если мы будем слишком много предвосхищать, выходя за рамки функционирующего продукта, мы, скорее всего, в конечном итоге напишем код, который нам не нужен.

Например, скажем, что в конечном итоге учетные записи пользователей и логин – это то, что мы хотим, чтобы пользователи фруктовой корзины имели и делали. Я захожу и устанавливаю библиотеку OAuth с помощью стратегии Google. Затем я начинаю разрабатывать функциональность для создания фрукта. Внезапно я понимаю, что у меня не было причин для входа пользователей в систему. Зачем им нужно входить в систему, чтобы создавать фрукты? Если все, что требуется для создания fruit, – это учетная запись пользователя, и у любого может быть учетная запись пользователя, какой смысл заставлять их входить в систему? Хммм. . похоже, что для MVP нам понадобятся пользователи только для создания фруктов; им не нужна учетная запись.

В будущем нам может понадобиться эта функциональность, если, скажем, мы пойдем в направлении интернет-магазина. Но это не то, что мы строим в данный момент. Но, вы можете сказать, что нет ничего плохого в том, чтобы позволить библиотеке OAuth или любому другому коду входа, который мы написали, болтаться в нашей кодовой базе, так почему бы и нет? И ответ двоякий: да, вред есть, но также зачем тратить нашу энергию и ресурсы на функциональность, которая не является частью поставленной задачи?

Во-первых, вред: чем больше у вас зависимостей, тем больше у вас поверхностей для атак плохих актеров. Хотя OAuth – это библиотека, которой мы доверяем, в какой-то момент она может устареть и/или иметь уязвимости, о которых мы ничего не знаем. Кроме того, если вы им не пользуетесь, то почему он там?

Кроме того, любой код, который мы пишем, рискует привести к сбою в наших интеграционных и функциональных тестах. Могут возникнуть непредвиденные последствия для того, как части нашего кода взаимодействуют друг с другом, или незначительные изменения в поведении пользователя. Держите тесты зелеными!

Второе: стоимость сборки. Зачем тратить время на то, что мы специально не строим прямо сейчас? Создание новой функциональности требует затрат времени и человеческого труда, поэтому давайте сосредоточимся на том, чего мы хотим прямо сейчас. Мы можем обратиться к авторизации и войти в систему, когда нам понадобятся эти функции.

Для получения дополнительной информации о ЯГНИ ознакомьтесь с этим замечательным постом Мартина Фаулера . В качестве примера он приводит компанию по страхованию судоходства из Минас-Тирита. Надеюсь, там будет лучше, чем в Осгилиате.

Итак, давайте взглянем на структуру нашего API, чтобы мы могли даже знать, что в чем мы тестируем:

  1. действие: Вызов попадает в конечную точку, назовем ее “/fruits”, которая запускает вызов базы данных, в котором мы получаем все() фрукты. Действия и маршрутизация правильных вызовов будут выполняться контроллером, контроллером тележки с фруктами.

  2. ЛОГИКА: Поскольку все, что делает контроллер корзины с фруктами, – это маршрутизирует и возвращает ответы, нам нужно что-то для обработки реальной бизнес-логики. Для этого у нас есть Фруктовый сервис. Служба Fruit решает, как мы будем получать информацию из базы данных (а не какую информацию – это решает контроллер). Итак, у нас будет удобный метод под названием get All Fruits() , который будет делать именно это: получать все фрукты из базы данных.

  3. данные: Таким образом, служба Fruit будет вызывать базу данных, используя встроенный метод репозитория JPA, называемый find All(). Этот репозиторий корзины с фруктами является картографом и волшебным образом преобразует данные в выбранный нами объект Java, в данном случае Fruit.

Это оно.

Итак, давайте начнем с самого начала, с Марка Аврелия… или Контролер тележки с фруктами.

Поскольку я смотрел Молчание ягнят слишком много раз, цитата “Что это такое само по себе”, которую Ганнибал Лектор приписывает Марку Аврелию, постоянно всплывает у меня в голове при попытке проверить. Полный отрывок из Размышлений Книги 8 гласит: “Что это за вещь сама по себе, в ее собственном строении? Каковы его элементы субстанции, материала и причины? Какова его функция в мире? Какова его продолжительность?”

И в целом, это хорошие вещи, которые нужно задавать коду. Что мы хотим, чтобы этот фрагмент кода делал? Для чего он нам нужен и какие фрагменты кода нам нужны? Сколько времени должно потребоваться, чтобы сделать все это? Подобные ожидания будут руководить нашим тестированием

Вот что нам нужно от FruitCartController:

  • Чтобы иметь конечную точку, которая успешно возвращает HTTP-ответ со статусом OK.

  • Эта конечная точка также должна будет вернуть список всех фруктов в базе данных.

Крутой. Давайте напишем тест для первой пули.

Вопрос: Итак, в HTTP-запросе, как мы узнаем, что ответ был успешным? A: Код состояния 200 или OK.

Поэтому мы сделали тест для этого в классе под названием Тесты контроллера фруктовой тележки. Spring предоставляет нам MockMvc, сервлет, который имитирует HTTP-запросы CRUD. Если вы используете IntelliJ (который я настоятельно рекомендую для Java), вам будет предложено импортировать правильную библиотеку, как только вы начнете писать код. Наш заключительный тест гласит:

@Test
public void shouldReturnHttpStatusOk() throws Exception {
  this.mockMvc.perform(get("/api/fruits").accept(MediaType.APPLICATION_JSON))
    .andExpect(status().isOk());
}

И когда мы запускаем его, он терпит неудачу. Отлично: у нас есть наш красный. Время получить зеленый свет.

Итак, теперь, когда у нас есть тест, нам нужно написать код, чтобы он прошел.

Необходимо создать очевидный файл (контроллер корзины с фруктами) вместе с очевидным маршрутом (“/fruits” – часть “/api” – это пространство имен, поскольку у нас также будет маршрутизация с похожими именами для интерфейса и мы хотим быть максимально понятными при именовании). Тогда все, что мы делаем, это передаем это самым очевидным образом, возвращаясь к мистеру Аврелию – что это такое само по себе? Каково его поведение? Что ж, все, что нам требуется на данный момент, – это вернуть статус 200, так что давайте продолжим и создадим конечную точку, которая делает именно это.

@Controller
@RestController
@RequestMapping(value="/api")
public class FruitCartController {

    @RequestMapping(value="/fruits")
    public ResponseEntity getAll(){
        return new ResponseEntity(HttpStatus.OK);
    }
}

Проведите эти тесты, и они должны пройти! Ура! У нас есть наш зеленый.

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

Итак, давайте пройдем тест еще на один шаг вперед; в конце концов, мы не просто хотим статуса – мы хотим фруктов!

Мы изменим наш тест, чтобы потребовать возврата массива объектов JSON fruit. Это должно выглядеть так:

[{id: 1, name: "apple", description: "A fleshy red fruit"}, {id: 2, name: "banana", description: "A minion's favorite fruit"}]

Мы будем ожидать, что сможем получить доступ к первому элементу массива и получить имя “apple”. Вот код:

@WebMvcTest(FruitCartController.class)
public class FruitCartControllerTests {

  @Autowired
  MockMvc mockMvc;

  @Test
  public void shouldReturnAnArray() throws Exception {   
    this.mockMvc.perform(get("/api/fruits").accept
(MediaType.APPLICATION_JSON))
       .andExpect(status().isOk())
       .andExpect(MockMvcResultMatchers.jsonPath("$").isArray())
       .andExpect(MockMvcResultMatchers.jsonPath("$[0].id").value("1"))
  .andExpect(MockMvcResultMatchers.jsonPath("$[0].name").value("apple"));
    }

MockMvc выполняет этот запрос GET, но в дополнение к коду состояния OK мы также ожидаем некоторых фактических данных. Поэтому, конечно, это быстро терпит неудачу.

Вернемся к красному. Давайте сделаем так, чтобы это прошло.

Пройти тест на самом деле будет довольно просто: мы создаем модель Fruit с некоторыми стандартными установщиками, получателями и конструктором, а затем просто жестко кодируем нужный нам ответ непосредственно в возвращаемом результате в FruitCartController:

    @RequestMapping(value="/fruits")
    public ResponseEntity getAll(){
        Fruit fruit = new Fruit("1", "apple");
        return new ResponseEntity(Arrays.asList(fruit), HttpStatus.OK);
    }

Он зеленый. Время для рефакторинга.

Но действительно ли мы хотим, чтобы наша система работала именно так? База данных не запрашивается, и нам пришлось бы создавать множество различных объектов Fruit в контроллере, что не входит в его обязанности. Нет. Нам нужно сделать этот запрос к базе данных, чтобы получить все фрукты.

Теперь помните, что в нашем описании архитектуры системы контроллер обрабатывает только маршрутизацию, сообщая остальной части бэкэнда, какие данные он на самом деле хочет получить. Но он на самом деле не получает данные. Таким образом, его способность выполнять свои функции зависит от другого уровня кода: сервиса. В настоящее время сервис Fruit даже не существует; мы не написали для него код. Но, похоже, нам, возможно, придется начать, если мы собираемся приблизиться к тому, как мы на самом деле хотим, чтобы работал наш контроллер.

И теперь наш маленький тест вышел за рамки модульного тестирования. Это интеграционный тест: мы проверяем, что два уровня – контроллеры и службы – взаимодействуют надлежащим образом, чтобы вернуть желаемый результат.

Итак, давайте напишем несколько настоящих модульных тестов; давайте создадим наш фруктовый сервис.

АБСОЛЮТНАЯ ЕДИНИЦА ИЗМЕРЕНИЯ

Изначально мы думали, что сервис Fruit также будет интеграционным тестом. В конце концов, он собирается обратиться к хранилищу корзины с фруктами, и это тоже потребует тестирования, верно? Ну, не так уж и много.

Хранилище фруктовой корзины, хотя и является файлом, представляет собой интерфейс. Это контракт между файлами или слоями. И вся функциональность, которую мы используем, содержится в репозитории JPA. Итак, в итоге все, что у нас есть, – это файл, который выглядит так:

@Repository
public interface FruitCartRepository extends JpaRepository {

}

Внутри него ничего нет. Все, что нам нужно сделать, это сообщить Spring, что этот интерфейс является хранилищем ( @Repository обрабатывает это), и в основном создать его, указав, что он отображает данные фруктов из таблицы фруктов на объекты фруктов и что первичный ключ в базе данных является целочисленным типом данных. Все фактические вызовы данных предоставляются библиотекой.

Поэтому, если бы мы тестировали наш репозиторий корзин с фруктами, мы бы на самом деле не тестировали написанный нами код. Мы будем тестировать код репозитория JPA. И это, друзья, уже проверено. Нам просто нужно убедиться, что он правильно вызывается в вашем собственном пользовательском коде, что означает тестирование FruitService.

Вопрос: Каково основное поведение фруктового сервиса, в котором мы нуждаемся прямо сейчас? A: Нам это нужно, чтобы вернуть набор фруктов.

Что самое простое, что он может вернуть? Пустой массив! Часто нулевые или пустые возвращаемые значения – это самый простой способ начать тестирование. Детские шаги.

Поэтому мы создали еще один тестовый файл для нашего класса Fruit Service и провели тест, чтобы получить пустой массив:

public class FruitServiceTests {

    FruitService fruitService;

    @Before
    public void setUp() {
        this.fruitService = new FruitService();
    }

    @Test
    public void shouldReturnEmptyArray() {
        assertThat(fruitService.getAllFruits(), is(Arrays.asList()));
    }
}

Сначала мы инициализируем новый фруктовый сервис, который достаточно оригинально называется fruitService. Затем мы утверждаем, что он вызывает метод получить все плоды () и что мы ожидаем, что это значение будет пустым массивом.

Проведите эти тесты, и бум: КРАСНЫЙ СВЕТ.

Время для этого рефакторинга.

Мы заходим, создаем этот класс обслуживания фруктов и создаем метод с именем get All Fruits() для возврата Arrays.asList() . Это оно.

Проведите эти тесты, и бум: ЗЕЛЕНЫЙ СВЕТ.

Но на самом деле это не то, чего мы хотим, верно? Мы хотим эти фрукты!

Поэтому мы переписываем тест, чтобы найти плод в этом массиве:

@Test
public void shouldReturnArrayOfFruit() {
  assertThat(fruitservice.getAllFruits().get(0).getId(), is("1"));
}

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

Проведите этот тест, и мы вернемся к красному.

Теперь давайте исправим наш Фруктовый сервис. Мы заменяем возвращаемое значение Arrays.asList() следующим:

        Fruit fruit = new Fruit("1", "apple");
        return Arrays.asList(fruit);

И все: запустите тест, и загорится зеленый.

Но действительно ли мы хотим, чтобы это было так?

Мы столкнулись с той же проблемой с нашим фруктовым сервисом, что и с контроллером корзины фруктов. Он не запрашивает базу данных, а создает объекты прямо в своей собственной логике, что не то, что он должен делать. Он должен запрашивать картограф (хранилище корзин с фруктами), который должен запрашивать базу данных, чтобы получить то, что ему нужно. Затем он должен вернуть массив этих объектов. Итак, что нам нужно проверить, так это то, вызывает ли он хранилище корзины с фруктами. Мы хотим протестировать его, чтобы убедиться, что он возвращает не только то, что нам нужно, но и делает это так, как нам нужно. И для этого нам нужно улучшить нашу тестовую игру.

Нам нужна пара новых супергероев: Насмешки и заглушки.

Тележка с Фруктами (Серия из 4 частей)

Оригинал: “https://dev.to/sleepycecy/fruit-cart-writing-tests-part-i-ld9”