Статьи

Тестирование устаревшего кода с Golden Master


В качестве разогрева для
SCNA ,
Chicago Software Craftsmanship Community провела практический сеанс кодирования, где разработчики, работающие в парах, должны протестировать и реорганизовать некоторый унаследованный код. Для этого они использовали
позолоченную розу ката . Вы можете найти ссылки на версии в Java, C # и ruby
здесь и для clojure
здесь .

В начале этого года мы провели одну и ту же сессию для
London Software Craftsmanship Community (LSCC), и тогда я решил написать свои тесты в стиле BDD (для этого я использовал JBehave). Вы можете проверить мое решение
здесь .

На этот раз вместо написания юнит-тестов или BDD / Spec By Example для тестирования каждой ветви этого ужасного кода, я решил решить ее, используя стиль теста под названием Golden Master.

Подход Golden Master

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

  1. Создайте X случайных входов, всегда используя одно и то же случайное начальное число, чтобы вы могли генерировать всегда один и тот же набор снова и снова. Вы, вероятно, захотите несколько тысяч случайных входов.
  2. Бомбардировать класс или тестируемую систему этими случайными входами.
  3. Захват выходов для каждого отдельного случайного входа

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

Утверждение тестов

Самый простой способ выполнить тестирование Golden Master на Java (также доступно для C # и Ruby) — это использовать
Approval Tests . Он делает всю обработку файлов за вас, хранит и сравнивает его. Вот пример:

package org.craftedsw.gildedrose;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import org.approvaltests.Approvals;
import org.junit.Before;
import org.junit.Test;

public class GildedRoseTest {

private static final int FIXED_SEED = 100;
private static final int NUMBER_OF_RANDOM_ITEMS = 2000;
private static final int MINIMUM = -50;
private static final int MAXIMUN = 101;

private String[] itemNames = {"+5 Dexterity Vest",
"Aged Brie",
"Elixir of the Mongoose",
"Sulfuras, Hand of Ragnaros",
"Backstage passes to a TAFKAL80ETC concert",
"Conjured Mana Cake"};

private Random random = new Random(FIXED_SEED);
private GildedRose gildedRose;

@Before
public void initialise() {
gildedRose = new GildedRose();
}

@Test public void
should_generate_update_quality_output() throws Exception {
List<Item> items = generateRandomItems(NUMBER_OF_RANDOM_ITEMS);

gildedRose.updateQuality(items);

Approvals.verify(getStringRepresentationFor(items));
}

private List<Item> generateRandomItems(int totalNumberOfRandomItems) {
List<Item> items = new ArrayList<Item>();
for (int cnt = 0; cnt < totalNumberOfRandomItems; cnt++) {
items.add(new Item(itemName(), sellIn(), quality()));
}
return items;
}

private String itemName() {
return itemNames[0 + random.nextInt(itemNames.length)];
}

private int sellIn() {
return randomNumberBetween(MINIMUM, MAXIMUN);
}

private int quality() {
return randomNumberBetween(MINIMUM, MAXIMUN);
}

private int randomNumberBetween(int minimum, int maximum) {
return minimum + random.nextInt(maximum);
}

private String getStringRepresentationFor(List<Item> items) {
StringBuilder builder = new StringBuilder();
for (Item item : items) {
builder.append(item).append("\r");
}
return builder.toString();
}

}

Для тех, кто не знаком с ката, после передачи списка элементов в класс GildedRose он будет проходить через них и в соответствии со многими различными правилами изменяет их атрибуты «sellIn» и «quality».

Я сделал небольшое изменение в классе Item, добавив в него автоматически сгенерированный метод toString ():

public class Item {
private String name;
private int sellIn;
private int quality;

public Item(String name, int sellIn, int quality) {
this.setName(name);
this.setSellIn(sellIn);
this.setQuality(quality);
}

        // all getters and setters here

@Override
public String toString() {
return "Item [name=" + name +
                              ", sellIn=" + sellIn +
                              ", quality=" + quality + "]";
}
}

При первом выполнении метода теста строка:

 Approvals.verify(getStringRepresentationFor(items));

создаст текстовый файл в той же папке, где находится тестовый класс, с именем: GildedRoseTest.should_generate_update_quality_output.received.txt. Это означает, что ..received.txt

ApprovalTests отобразит в консоли следующее сообщение:

To approve run : mv /Users/sandromancuso/development/projects/
java/gildedrose_goldemaster/./src/test/java/org/craftedsw/
gildedrose/GildedRoseTest.should_generate_update_quality_output.received.txt 
/Users/sandromancuso/development/projects/java/gildedrose_goldemaster/./src/
test/java/org/craftedsw/gildedrose/GildedRoseTest.should_generate_update_quality_output.approved.txt

По сути, после проверки файла, если мы довольны, нам просто нужно изменить .recepted на .approved, чтобы утвердить вывод. После этого каждый раз, когда мы запускаем тест, ApprovalTests будет сравнивать вывод с утвержденным файлом.

Вот пример того, как выглядит файл:

Item [name=Aged Brie, sellIn=-23, quality=-44]
Item [name=Elixir of the Mongoose, sellIn=-9, quality=45]
Item [name=Conjured Mana Cake, sellIn=-28, quality=1]
Item [name=Aged Brie, sellIn=10, quality=-2]
Item [name=+5 Dexterity Vest, sellIn=31, quality=5] 

Теперь вы готовы разорвать ужасный код GildedRose. Просто убедитесь, что вы запускаете тесты каждый раз, когда вносите изменения. 🙂

Infinitest

Если вы используете Eclipse или IntelliJ, вы также можете использовать
Infinitest . Он автоматически запускает ваши тесты каждый раз, когда вы сохраняете производственный или тестовый класс. Он достаточно умен, чтобы запускать только соответствующие тесты, а не весь набор тестов. В Eclipse в нижнем левом углу отображается полоса красного, зеленого или желтого цвета (в случае ошибок компиляции и невозможности запуска тестов).

При таком подходе рефакторинг унаследованного кода становится очень простым делом. Вы вносите изменения, сохраняете их, смотрите на панель внизу экрана. Если он зеленый, продолжайте рефакторинг, если он красный, просто нажмите CTRL-Z, и вы вернетесь в зеленый цвет. Замечательный. 🙂

Спасибо

Спасибо
Роберту Тейлору и
Балинту Пато за то, что они впервые показали мне этот подход на одном из
заседаний
LSCC в начале этого года. Было весело наконец сделать это самому.