Статьи

Как очистить ваши тесты

Тесты должны быть вашим основным средством документирования в системе. Мне нравится думать, что мы все перешли от идеи использования комментариев в коде (за исключением API), и большинство из нас знают, что стремятся к чистому, самодокументируемому коду. Но для меня тесты — это самый простой и мощный способ документирования части кода. Если я смотрю на код и у меня есть момент WTF, я должен быть в состоянии перейти к тестам и посмотреть, каково было предполагаемое поведение.

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

Недавно я обнаружил тест, проходящий через систему, которая выглядела совсем не так:

@Test
public void bankTransferWillIncreaseDestinationBankAccount() throws Exception {
    BankAccount bankAccount = new BankAccount(20.0);
    final BankTransfer mock = mockery.mock(ConcreteBankTransfer.class);

    mockery.checking(new Expectations(){{
         allowing(mock).accountFrom(); will(returnValue(“accFrom”));
         allowing(mock).accountTo(); will(returnValue(“accTo”));
         allowing(mock).name(); will(returnValue(“Jon Smith”));
         allowing(mock).transferId(); will(returnValue(“143NMd24”));
         allowing(mock).ccy(); will(returnValue(null));
         allowing(mock).overdraftLimit();will(returnValue(200.0));
         oneOf(mock).amount(); will(returnValue(20.0));
     }
     });
    bankAccount.apply(mock);
    assertThat(bankAccount.amount(), is(40.0));
}

Это драматическая реконструкция фактического кода. Ни один программист не пострадал при создании этого кода.

Дело в том, что в классе было три теста, у всех которых была рвота «разрешать». Этот код плохо пахнет. Насмешки должны использоваться, чтобы издеваться над поведением . В этом случае объект BankTransfer, по сути, является POJO. Плохое поведение.

Но большая проблема здесь в том, что невозможно увидеть, что происходит. Является ли факт, что ccy имеет значение null? А как насчет ID перевода? Имеет ли овердрафт какое-либо отношение к этому?

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

@Test
public void bankTransferWillIncreaseDestinationBankAccount() throws Exception {
    BankAccount bankAccount = new BankAccount(20.0);
    final BankTransfer mock = new ConcreteBankTransfer(“accFrom”, “accTo”, null, 20.0, 200.0, “Jon Smith”, “143NMd24”);

    bankAccount.apply(mock);
    assertThat(bankAccount.amount(), is(40.0));
}

Конечно, кода меньше, но я понятия не имею, что означает каждое поле в конструкторе. Во всяком случае, сейчас неясно, какие поля имеют значение.

Вот где приходит Maker. Maker позволяет вам создавать объект, говоря: «Меня не волнуют никакие ценности, кроме этих конкретных». Давайте посмотрим на окончательный код, прежде чем показывать реализацию.

private Maker aBankTransfer = a(BankTransferMaker.BankTransfer);

@Test
public void bankTransferWillIncreaseDestinationBankAccount() throws Exception {
    BankAccount bankAccount = new BankAccount(20.0);
    BankTransfer bankTransfer = make(aBankTransfer.but(with(BankTransferMaker.amount, 20.0)));
    bankAccount.apply(bankTransfer);
    assertThat(bankAccount.amount(), is(40.0));
}

Для меня это бесконечно понятнее. Код делает именно так, как он говорит; он создает BankTransfer (и нам все равно, как он выглядит), но мы указываем, что он должен иметь сумму 20,0, поскольку это значение, которое мы заботимся о нашем тесте. Очень кратко, ясно и многоразово. Везде, где нужен объект BankTransfer, можно использовать это повторно.

Чтобы использовать Maker, вам нужно импортировать Nat Pryce «make-it-easy» (подробности Maven на http://mvnrepository.com/artifact/com.natpryce/make-it-easy ). Тогда это просто вопрос создания вашего Создателя.

Существует довольно много стандартного кода, и создание Maker может быть довольно утомительным. В результате вы можете подумать, прежде чем начать использовать их повсюду.

public class BankTransferMaker {

    public static final Property<BankTransfer, String> accountFrom = newProperty();
    public static final Property<BankTransfer, String> accountTo = newProperty();
    public static final Property<BankTransfer, String> name = newProperty();
    public static final Property<BankTransfer, String> transferId = newProperty();
    public static final Property<BankTransfer, Double> overdraftLimit = newProperty();
    public static final Property<BankTransfer, Double> amount = newProperty();
    public static final Property<BankTransfer, ConcreteBankTransfer.Currency> ccy = newProperty();

    public static final Instantiator BankTransfer = new Instantiator() {

       @Override public BankTransfer instantiate(PropertyLookup lookup) {

            BankTransfer bankTransfer = new ConcreteBankTransfer(
                lookup.valueOf(accountFrom, random(5)),
                lookup.valueOf(accountTo, random(5)),
                lookup.valueOf(ccy, new ConcreteBankTransfer.Currency(random(3))),
                lookup.valueOf(amount, nextDouble(0,100000)),
                lookup.valueOf(overdraftLimit, nextDouble(0,100000)),
                lookup.valueOf(name, random(10)),
                lookup.valueOf(transferId, random(10)));
  
            return bankTransfer;
           }
       };
}

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

Затем мы создаем Instantiator; Вот как на самом деле создается наш объект. Вы можете установить значения по умолчанию на то, что вы хотите; Я использовал apache-commons для вставки случайных значений, потому что мне действительно все равно, что там.

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

make(aBankTransfer.but(
with(BankTransferMaker.amount, 20.0),
with(BankTransferMaker.transferId, “Octopus”)));

Это действительно хороший способ четко определить, какие значения имеют значение в вашем тесте.