Статьи

Тестирование фреймворков для Java с примерами кода

Некоторое время назад я готовил презентацию, посвященную разработке и тестированию фреймворков для Java. Поскольку целью было продемонстрировать некоторый реальный, работающий код, я потратил довольно много времени на копирование, вставку, расширение и исправление различных примеров, почерпнутых из readmes, Javadoc, вики-страниц и постов блога. С тех пор эта кодовая база была расширена различными новыми функциями, с которыми я сталкивался, и я часто упоминал ее для экспериментов, как полезную ссылку и тому подобное.

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

Mock Braindump

В качестве введения, здесь сначала мозговая пыль различных точек интереса, связанных с рамками в образце 1 . Не ожидайте подробный обзор или сравнение — есть много из там — и на конкретные вопросы о рабочих из рамок пожалуйста , обратитесь к их документации 2 .

Мокито или EasyMock?

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

Лично я должен сказать, что нахожу синтаксис Mockito чуть более приятным и интуитивно понятным. Тот факт, что вам не нужно явно переключаться в режим воспроизведения, удобен, а для таких вещей, как шпион (для частичных насмешек) или InOrder (для упорядоченных ожиданий), синтаксис просто более элегантный.
Более фундаментально то, что акцент на окурках, а не на кропотливой проверке призывов к издевательствам, был для меня очень полезным и оправданным отличием.

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

Статика, местные жители и финалы: последний рубеж TDD?

«Смело идти… туда, где раньше не было никакой насмешливой основы», похоже, является задачей Jmockit и JEasyTest .

public final class ServiceA {

public void doBusinessOperationXyz(EntityX data)
throws InvalidItemStatus {
List<?> items = Database.find("select item from EntityY item where item.someProperty=?",
data.getSomeProperty());
BigDecimal total = new ServiceB().computeTotal(items);
data.setTotal(total);
Database.save(data);
}

}

public static final class ServiceB { ...

Задача: иметь возможность модульного тестирования этого класса, в частности вызовов поиска и сохранения в базе данных и вызова computeTotal в новом экземпляре ServiceB. Используя обычные насмешки, это почти невозможно:

  • найти и сохранить статические
  • ServiceB является последним классом и, следовательно, не может быть высмеян
  • даже если это возможно, вызываемый экземпляр ServiceB создается в тестируемом коде

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

Инъекция зависимости, возможно, стала чем-то вроде религии, но она не так широко распространена даром. Для кода, который выполняет DI, стандартных фреймворков почти всегда достаточно.

Но даже если, возможно, не стоит пытаться провести юнит-тестирование примера, не так ли? Что ж, опыт показывает, что есть несколько проблем, которые не могут быть решены с помощью достаточно большого количества манипуляций с байт-кодом.
JEasyTest использует свою магию, используя предварительное тестирование, которое может быть сделано с помощью
плагина Eclipse или путем добавления плагина к вашей сборке Maven 3 . Jmockit использует немного более современный подход к инструментам и поставляется с агентом Java, что означает, что для запуска в вашей IDE требуется только дополнительный аргумент VM.

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

@JEasyTest
public void testBusinessOperation() throws InvalidItemStatus {
on(Database.class).expectStaticNonVoidMethod("find").with(
arg("select item from EntityY item where item.someProperty=?"),
arg("abc")).andReturn(Collections.EMPTY_LIST);
on(ServiceB.class).expectEmptyConstructor().andReturn(serviceB);
on(Database.class).expectStaticVoidMethod("save").with(arg(entity));
expect(serviceB.computeTotal(Collections.EMPTY_LIST)).andReturn(total);
replay(serviceB);
serviceA.doBusinessOperationXyz(entity);
verify(serviceB);
assertEquals(total, entity.getTotal());
}

 

expectStaticNonVoidMethod? ARGH! По ощущениям больше похож на ASM, чем на юнит-тестирование.

Режим «ожиданий» в Jmockit наиболее близок к тому, что пользователи Mockito / EasyMock, вероятно, знакомы с 4 :

@MockField
private final Database unused = null;
@MockField
private ServiceB serviceB;

@Test
public void doBusinessOperationXyz() throws Exception {
EntityX data = new EntityX();
BigDecimal total = new BigDecimal("125.40");

List<?> items = new ArrayList<Object>();
Database.find(withSubstring("select"), withAny(""));
returns(items);
new ServiceB().computeTotal(items);
returns(total);
Database.save(data);
endRecording();

new ServiceA().doBusinessOperationXyz(data);
assertEquals(total, data.getTotal());
}

 

Концептуально это напоминает тест EasyMock с этапами записи и воспроизведения . Поля @MockField заменяют создание фактических фиктивных объектов: объявления полей указывают Jmockit только на то, что имитация заданных типов необходима при запуске теста, загромождая класс теста неиспользуемыми свойствами.
Кроме того, методы «управления имитацией» (withAny, return и т. Д.) Не являются статичными, то есть они визуально не идентифицируются, например, отображаются курсивом. Я был удивлен, насколько это незначительное несоответствие оттолкнуло меня — это просто не похоже на юнит-тест.

JMock

Из того, что я вижу, вокруг jMock больше ничего не происходит: последний релиз был в августе 2008 года, а последние новости были опубликованы более полугода назад. Синтаксис, который пытается имитировать псевдо-«естественный язык» DSL, просто слишком громоздок. Поддержка многопоточности в jMock побудила меня взглянуть поближе, но на самом деле это просто механизм, гарантирующий, что ошибки подтверждения, выданные в других потоках, действительно регистрируются тестовым потоком; нет поддержки для тестирования одновременного поведения.

Тестирование параллельного кода

Мне очень нравится MultithreadedTC , небольшой фреймворк 5, цель которого — упростить запуск и координацию нескольких тестовых потоков. Это осуществляется с помощью глобальных «часов», которые перемещаются вперед всякий раз, когда все потоки заблокированы — либо «естественно» (например, во время вызова, такого как blockingQueue.take ()
), либо намеренно используя команду waitForTick ( n ).
Таким образом, MultithreadedTC не предлагает намного больше, чем можно достичь с помощью «ручных» защелок, как описано в недавнем сообщении в блоге Иуэна Фульда , но метафора часов , кажется, облегчает понимание потока тестов, особенно для более длинных тестов.

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

public void thread1() throws InterruptedException {
...
waitForTick(1);
service.someMethod();
waitForTick(2);
...
}

public void thread2() throws InterruptedException {
...
waitForTick(1);
service.otherMethod();
waitForTick(2);
...
}

 

Этот код каким-то образом гарантирует, что service.someMethod () и service.otherMethod () запускаются практически одновременно, и гарантирует, что ни один поток не продолжит работу, пока оба метода не завершатся. Но что, если вы хотите убедиться, что половина someMethod завершается до вызова otherMethod?

Для этого вам нужно будет получить доступ к реализациям someMethod и otherMethod, например, путем создания подклассов реализаций службы или использования чего-то вроде Byteman .

В конечном счете, тем не менее, я думаю, что модульные тесты — это не правильный способ тестирования параллельного кода. «Хореография» тщательно выбранных действий небольшого числа тестовых потоков является плохой заменой для реального одновременного использования, и ошибки, которые вы обнаружите, если таковые имеются, не являются проблемами параллелизма, которые в конечном итоге вызывают кошмары.

Для правильной проверки параллельности, там не так далеко , как представляется, является хорошей заменой для начала целый пучок нитей — на столько же ядер , как это возможно — и запускать их на благо в то время как (см, например, интеграционные тесты на Мультивселенной , Java STM). Если есть возможность ввести определенное количество случайности в хронометраж (используя, например, Byteman), тем лучше!

Правила JUnit

Rocks
!

Версия 4.7 JUnit введены правила , сортировки по всем аспектам, которые вызываются до и после выполнения теста. Некоторые из стандартных примеров демонстрируют функциональные возможности «ведения домашнего хозяйства», такие как открытие и закрытие ресурса или создание и очистка временной папки. Правила также могут влиять на результат теста, например, вызывая его сбой, даже если все утверждения теста были успешными.

Хотя можно увидеть, как концепция правил может быть полезна, они все еще имеют некоторую грубость «v1». Правила «домашнего хозяйства» являются по существу удобной заменой логики @ Before / @ After, а синтаксис «влияющих на тест» правил кажется беспорядочным:

public class UsesErrorCollectorTwice {
@Rule
public ErrorCollector collector = new ErrorCollector();

@Test
public void example() {
collector.addError(new Throwable("first thing went wrong"));
collector.addError(new Throwable("second thing went wrong"));
collector.checkThat("ERROR", not("ERROR"));
collector.checkThat("OK", not("ERROR"));
System.out.println("Got here!");
}
}

Wouldn’t that be nicer if the assertions were a little more, um, assert-like? Or take:

public class HasExpectedException {
@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void throwsNullPointerExceptionWithMessage() {
thrown.expect(NullPointerException.class);
thrown.expectMessage("happened?");
thrown.expectMessage(startsWith("What"));
throw new NullPointerException("What happened?");
}
}

I mean, it’s certainly useful to be able to make more detailed assertions about exceptions, but could this not be integrated into either of the current exception-checking patterns6?

The potential power of rules also raises an question: is it wise to get into the habit of doing full-scale resource management (e.g. starting servers or DB connections) in a unit test?

Footnotes

  1. Sample source code here. Check out the project using svn checkout http://aphillips.googlecode.com/svn/mock-poc/trunk target-folder.
  2. See the sample code’s POM.
  3. Which is not to say it’s the best approach, of course!
  4. The original code is not longer being actively developed, but there has been some recent work aimed at better JUnit 4 integration.
  5. @Test
    public void tryCatchTestForException() {
    try {
    throw new NullPointerException();
    fail();
    } catch (NullPointerException exception) {
    // expected
    }
    }

    @Test(expected = NullPointerException.class)
    public void annotationTestForException() {
    throw new NullPointerException();
    }
  • Share / Bookmark