Статьи

Создание объектов в модульных тестах Java


Большинство модульных тестов Java состоят из тестируемого класса (CUT) и, возможно, зависимостей или соавторов. CUT, зависимости и соавторы должны быть где-то созданы. Некоторые люди создают каждый объект, используя «новый» (ванильная конструкция), другие используют такие шаблоны, как
Test Data Builders или
Object Mother . Иногда зависимости и соавторы являются экземплярами конкретных классов, а иногда имеет смысл использовать
Test Doubles . Эта статья о шаблонах, которые я нашел наиболее полезными для создания объектов в моих тестах.

В январе 2006 года Мартин Фаулер написал
короткую запись в блогеэто включало определения для различных типов двойников теста. Я нашел работу с этими определениями очень полезной.


Test Doubles: Mocks и

Stubs Mockito дает вам возможность легко создавать макеты и заглушки. Точнее, я использую Mockito «mocks» для насмешек и окурков. Mockito не является строгим: они не выдают исключение при вызове «неожиданного» метода. Вместо этого Mockito записывает все вызовы методов и позволяет проверить в конце теста. Любые вызовы методов, которые не проверены, просто игнорируются. Возможность игнорировать вызовы методов, которые вас не интересуют, делает Mockito идеальным вариантом для работы с заглушками.


Test Doubles: чайники

Я не нашел много случаев, когда мне нужен манекен. Пустышка — это не более, чем традиционный макет (тот, который генерирует исключения при «неожиданных» вызовах методов) без ожидаемых вызовов методов. В тех случаях, когда я хотел убедиться, что на объекте ничего не вызывалось, я обычно использовал метод verifyZeroInteractions для макетов Mockito. Однако, есть исключительные случаи, когда я не хочу проверять, что никакие методы не были вызваны, но я хотел бы получать оповещения, если метод вызывается. Это скорее сценарий предупреждения, чем фактический сбой.

В моем текущем проекте мы закончили тем, что катали свои собственные макеты, где это было необходимо, и переопределили (или реализовали) каждый метод с кодом, который выдал бы новое исключение. Это было довольно легко сделать в IntelliJ; Тем не менее, я с нетерпением жду следующей версии Mockito, которая должна облегчить создание пустышки.

Test Doubles: Fakes

Мой текущий проект использует Fakes. Мы определяем подделку как экземпляр, который работает на и нацелен на одну проблему. Например, если ваш класс зависит от
волокна Jetlang, вы можете передать поддельное волокно Jetlang, которое синхронно выполняло команду, как только ей было дано. Поддельное волокно не позволит вам планировать задачи, но ничего страшного, оно разработано для обработки синхронных запросов и все.

У нас нет большого количества подделок, но они могут быть превосходной альтернативой по сравнению с созданием нескольких макетов, которые ведут себя одинаково. Если мне нужен CreditCardProcessingGateway, чтобы вернуть результат один раз, хорошо бы использовать макет. Тем не менее, если мне нужно, чтобы CreditCardProcessingGateway последовательно возвращал значение true, если ему присваивается
длинный действительный номер кредитной карты (или, в противном случае, false), подделка может быть лучшим вариантом.


Конкретные классы

Я большой поклонник тестовых сборщиков данных Nat Pryce
для создания конкретных классов. Я использую тестовые сборщики данных для создания большинства зависимостей и соавторов. Создатели тестовых данных также позволяют мне легко добавить тестовую двойку в случае необходимости. В следующем примере кода показано, как я буду использовать построитель тестовых данных для простого создания объекта автомобиля.

aNew().car().
with(mock(Engine.class)).
with(fake().radio().fm_only()).
with(aNew().powerWindows()).build();


The (contrived) example demonstrates 4 difference concepts:
  • Автомобиль может быть легко построен с помощью двигателя
  • Автомобиль может быть легко построен с поддельным радио FM
  • Автомобиль может быть легко построен с бетонной реализацией стеклоподъемников
  • Все остальные зависимости будут разумными значениями по умолчанию

Оригинальная запись Ната на эту тему гласит, что у каждого строителя должны быть «разумные значения по умолчанию». Это оставляло вещи немного открытыми для интерпретации, поэтому мы пробовали разные значения по умолчанию. В конце концов, было наиболее разумно, чтобы все разработчики тестовых данных использовали другие сборщики тестов в качестве значений по умолчанию или null. Мы никогда не используем тестовые значения в качестве значений по умолчанию. На практике это, в любом случае, не является болезненным, поскольку вы можете легко добавить свой собственный макет или подделку в конкретный тест, который требует этого.

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

Возможно, вы также заметили методы aNew () и fake () из предыдущего примера. Метод aNew () возвращает класс DomainObjectBuilder, а метод fake () возвращает класс Faker. Эти методы являются вспомогательными, которые могут быть импортированы статически. Реализации этих классов очень просты. Учитывая объект домена Radio, DomainObjectBuilder будет иметь метод, определенный как в примере ниже.

public RadioBuilder radio() {
RadioBuilder.create();
}

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

Следующий код показывает, как все эти вещи работают вместе.

// RadioTest.java
public class RadioTest {
    public void shouldBeOn() {
        Radio radio = aNew().radio().build();
        radio.turnOn();
        assertTrue(true, radio.isOn());
    }
    // .. other tests...
}

// DomainObjectBuilder.java
public class DomainObjectBuilder {
    public static DomainObjectBuilder aNew() {
        return new DomainObjectBuilder();
    }

    public RadioBuilder radio() {
        return RadioBuilder.create();
    }
}

// RadioBuilder.java
public class RadioBuilder {
    private int buttons;
    private CDPlayer cdPlayer;
    private MP3Player mp3Player;

    public static RadioBuilder create() {
        return new RadioBuilder(4,
            CDPlayerBuilder.create().build(),
            MP3PlayerBuilder.create().build());
    }

    public RadioBuilder(int buttons, CDPlayer cdPlayer, MP3Player mp3Player) {
        this.buttons = buttons;
        this.cdPlayer = cdPlayer;
        this.mp3Player = mp3Player;
    }

    public RadioBuilder withButtons(int buttons) {
        return new RadioBuilder(buttons, cdPlayer, mp3Player);
    }

    public RadioBuilder with(CDPlayer cdPlayer) {
        return new RadioBuilder(buttons, cdPlayer, mp3Player);
    }

    public RadioBuilder with(MP3Player mp3Player) {
        return new RadioBuilder(buttons, cdPlayer, mp3Player);
    }

    public Radio build() {
        return new Radio(buttons, cdPlayer, mp3Player);
    }
}

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

Поскольку Java дает мне перегрузку методов на основе типов, я всегда называю свои методы «с», когда могу (как показано в примере). Это не всегда работает, если, например, у вас есть два разных свойства одного типа. Обычно это происходит со встроенными типами, и в этих случаях я создаю такие методы, как withButtons, withUpperLimit или withLowerLimit.

Еще одна привычка, с которой я столкнулся, — это использование Builders для создания всех объектов в моих тестах, даже тестируемого класса. Это приводит к более ремонтопригодным тестам. Если вы явно используете конструктор Class Under Test в своем тесте и добавляете зависимость, вам в конечном итоге придется изменять каждую строку, которая создает экземпляр Class Under Test. Однако если вы используете компоновщик, вам может не потребоваться что-либо менять, и если вам придется что-то менять, это, вероятно, будет только для подмножества тестов.

Вывод

Я большой поклонник Test Data Builders и Test Doubles. Объединение этих двух концепций позволило быстрее писать тесты и легче их обслуживать. Эти идеи также могут быть добавлены постепенно, что является хорошим бонусом для тех, кто хочет добавить этот стиль в существующую кодовую базу.