Статьи

Альтернативный подход к написанию тестов JUnit (Жасминовый путь)

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

Jasmine  — это популярная среда тестирования, основанная на поведенческих разработках для JavaScript, которая основана на  RSpec  (среда тестирования Ruby BDD).

Простой тест Жасмин выглядит так:

describe('AudioPlayer tests', function() {
var player;
beforeEach(function() {
player = new AudioPlayer();
});
it('should not play any track after initialization', function() {
expect(player.isPlaying()).toBeFalsy();
});
...
});

Вызов функции description () в первой строке создает новый набор тестов с использованием описания  тестов AudioPlayer . Внутри набора тестов мы можем использовать его () для создания тестов (называемых спецификациями в Jasmine). Здесь мы проверяем, возвращает ли метод isPlaying () AudioPlayer значение false после создания нового экземпляра AudioPlayer.
Тот же тест, написанный на JUnit, будет выглядеть так:

public class AudioPlayerTest {
private AudioPlayer audioPlayer;
@Before 
public void before() {
audioPlayer = new AudioPlayer();
}
@Test
void notPlayingAfterInitialization() {
assertFalse(audioPlayer.isPlaying());
}
...
}

Лично я считаю, что тест Жасмин гораздо более читабелен по сравнению с версией JUnit. В Jasmine единственный шум, который ничего не вносит в тест, это фигурные скобки и ключевое слово function. Все остальное содержит некоторую полезную информацию.
При чтении теста JUnit мы можем игнорировать ключевые слова, такие как void, модификаторы доступа (private, public, ..), аннотации и нерелевантные имена методов (например, имя метода с аннотацией @Before). В дополнение к этому описания тестов, закодированные в именах методов верблюжьего дела, не так уж хороши для чтения.

Помимо повышенной читабельности, мне очень нравится способность Jasmine вложения тестовых наборов.
Давайте посмотрим на пример, который немного длиннее:

describe('AudioPlayers tests', function() {
var player;
beforeEach(function() {
player = new AudioPlayer();
});
describe('when a track is played', function() {
var track;
beforeEach(function() {
track = new Track('foo/bar.mp3')
player.play(track);
});
it('is playing a track', function() {
expect(player.isPlaying()).toBeTruthy();
});
it('returns the track that is currently played', function() {
expect(player.getCurrentTrack()).toEqual(track);
});
});
...
});

Здесь мы создаем набор вспомогательных тестов, который отвечает за тестирование поведения при воспроизведении дорожки с помощью AudioPlayer. Внутренний вызов beforeEach () используется для установки общего предварительного условия для всех тестов в наборе субтестов.

Напротив, совместное использование общих предварительных условий для нескольких (но не всех) тестов в JUnit может иногда становиться громоздким. Конечно, дублирование кода установки в тестах — это плохо, поэтому мы создадим дополнительные методы для этого. Чтобы обмениваться данными между методами настройки и тестирования (например, переменной track в приведенном выше примере), мы должны использовать переменные-члены (с гораздо большей областью действия).
 Кроме того, мы должны объединить тесты с одинаковыми предварительными условиями, чтобы избежать необходимости читать весь класс тестов, чтобы найти все соответствующие тесты для определенной ситуации. Или мы можем разделить вещи на несколько небольших классов. Но тогда нам, возможно, придется разделить код установки между этими классами … 

Если мы посмотрим на тесты Jasmine, мы увидим, что структура определяется вызовом глобальных функций (таких как description (), it (), …) и передачей описательных строк и анонимные функции.

С Java 8 мы получили Lambdas, так что мы можем сделать то же самое правильно?
Да, мы можем написать что-то подобное в Java 8:

public class AudioPlayerTest {
private AudioPlayer player;
public AudioPlayerTest() {
describe("AudioPlayer tests", () -> {
beforeEach(() -> {
player = new AudioPlayer();
});
it("should not play any track after initialization", () -> {
expect(player.isPlaying()).toBeFalsy();
});
});
}
}

Если мы предположим на мгновение, что description (), beforeEach (), it () и wait () являются статически импортированными методами, которые принимают соответствующие параметры, это по крайней мере компилируется. Но как мы должны запустить этот вид теста?

Для интереса я попытался интегрировать это с JUnit, и оказалось, что это на самом деле очень легко (я напишу об этом в будущем). Результатом является небольшая библиотека под названием  Oleaster .

Тест, написанный на Oleaster, выглядит так:

import static com.mscharhag.oleaster.runner.StaticRunnerSupport.*;
...
@RunWith(OleasterRunner.class)
public class AudioPlayerTest {
private AudioPlayer player;
{
describe("AudioPlayer tests", () -> {
beforeEach(() -> {
player = new AudioPlayer();
});
it("should not play any track after initialization", () -> {
assertFalse(player.isPlaying());
});
});
}
}

Только несколько вещей изменились по сравнению с предыдущим примером. Здесь тестовый класс помечается аннотацией JUnit @ RunWith. Это говорит JUnit использовать Oleaster при запуске этого тестового класса. Статический импорт ofStaticRunnerSupport. * Предоставляет прямой доступ к статическим методам Oleaster, таким как description () или it (). Также обратите внимание, что конструктор был заменен  инициализатором экземпляра,  а Jasmine-подобный сопоставитель заменен стандартным утверждением JUnit.

Есть одна вещь, которая не так хороша по сравнению с оригинальными тестами Жасмин. Дело в том, что в Java переменная должна быть эффективно конечной, чтобы использовать ее внутри лямбда-выражения. Это означает, что следующий фрагмент кода не компилируется:

describe("AudioPlayer tests", () -> {
AudioPlayer player;
beforeEach(() -> {
player = new AudioPlayer();
});
...
});

Присвоение игроку внутри лямбда-выражения beforeEach () не будет компилироваться (потому что игрок не является окончательным). В Java мы должны использовать поля экземпляра в подобных ситуациях (как показано в примере выше).

Если вы беспокоитесь об отчетности: Oleaster отвечает только за сбор тестовых примеров и их запуск. Вся отчетность по-прежнему выполняется JUnit. Поэтому Oleaster не должен вызывать проблем с инструментами и библиотеками, которые используют отчеты JUnit.

Например, на следующем снимке экрана показан результат неудачного теста Oleaster в IntelliJ IDEA:

Если вам интересно, как на практике выглядят тесты Oleaster, вы можете взглянуть на тесты для Oleaster (написанные на самом Oleaster). Вы можете найти тестовый каталог GitHub  здесь

Не стесняйтесь добавлять любые отзывы, комментируя этот пост или создавая  проблему GitHub .