Java на habrahabr

Январь 20, 2012

 

JAVA / JMock и EasyMock: сравнение и howto в примерах и не только

Практически ни для кого не секрет, что при тестировании кода, использующего какие-то внешние компоненты, часто применяют подход mock-объектов. Для тех, кто всё же о нём не знает, кратко поясню: это такие объекты, которые имеют тот же интерфейс, что и используемые компоненты, но их поведение полностью задаётся в тесте, и их использование позволяет избежать поднятия полной инфраструктуры, необходимой приложению для запуска. Что ещё более важно, можно легко и непринуждённо проконтролировать, что код вызывал те или иные методы у mock-объекта с теми или иными аргументами.

В этой статье я проведу сравнительный анализ двух распространённых в Java библиотек для работы с mock'ами: EasyMock и JMock. Для осознания достаточно базового знания JUnit, а после прочтения этой статьи у вас будет весьма хорошее представление о том, как пользоваться обеими этими библиотеками.

Java на habrahabr

Август 23, 2011

 

JAVA / JUnit 4.9 зарелизило

Несколько часов назад на github-е популярного TDD-фреймворка JUnit появилась финальная версия JUnit 4.9. Что же вошло в долгожданный релиз?

Java на habrahabr

Июнь 14, 2011

 

JAVA / И еще раз о тестах. Подход к тестированию кода в реальной жизни

Думаю, почти каждый сталкивался с таким мнением: писать тесты сложно, все примеры написания тестов даны для простейших случаев, а в реальной жизни они не работают. У меня же за последние годы сложилось впечатление, что писать тесты — это очень просто, даже тривиально*. Автор упомянутого выше комментария далее говорит, что неплохо было бы сделать пример сложного приложения и показать, как его тестировать. Попробую именно этим и заняться.

*)Писать сами тесты — действительно элементарно. Создать инфраструктуру, позволяющую легко писать тесты — чуть сложнее.

Java на habrahabr

Май 27, 2011

 

JAVA / Тестирование в Java. JUnit


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

Итак, техника довольно понятна, но встает вопрос, что использовать для написания этих самых тестов? В этой и других статьях я хотел бы поделиться своим опытом в использовании различных инструментов и техник для тестирования кода в Java.

Ну и начну с, пожалуй, самого известного, а потому и самого используемого фреймворка для тестирования — JUnit. Используется он в двух вариантах JUnit 3 и JUnit 4. Рассмотрю обе версии, так как в старых проектах до сих пор используется 3-я, которая поддерживает Java 1.4.

Я не претендую на автора каких-либо оригинальных идей, и возможно многим все, о чем будет рассказано в статье, знакомо. Но если вам все еще интересно, то добро пожаловать под кат.

ru.java на livejournal

Ноябрь 16, 2010

 

Многопоточное интеграционное тестирование в Spring

Интеграционные тесты на JUnit-е в Spring-овом приложении пишут, конечно же, почти все. Удобно: описал в коде тестов бизнес-кейсы, указал, как проинъектить зависимости, поставил по вкусу defaultRollback=true (чтобы не приходилось мучиться с порчей данных в базе) - и смотри, работает или не работает твое приложение по всей или части цепочки вызова, прямо в IDE или CI. Экономится масса времени на ручном тестировании, окупается при рефакторингах, когда бизнес-кейсы и интерфейсы остаются неизменными. Лепота.

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

Увы, out of the box ничего не нашлось: нет (пока?) в Spring возможности, добавив, скажем, аннотацию на метод, прогнать его в нескольких потоках, каждый в своей транзакции. Переписывать каждый тест так, чтобы управление потоками и транзакциями происходило внутри тела теста, не хотелось - тестов таких по приложению нужно было прогнать не один десяток. JMeter не годился, т.к. требовал деплоя и не интегрировался с maven-ом, а хотелось гонять такие тесты прямо в процессе разработки.

Первое, что пришло в голову, написать некий метод в родительском для всех моих тестов классе, который бы запускал тесты в многопоточном режиме (далее везде "многопоточный/мультранзакционный" подразумевает параллельный запуск нескольких потоков, когда в каждом потоке стартуется своя Spring-транзакция). От этого подхода я сразу отказался: поскольку @Transactional у меня указана непосредственно на родительском классе всех моих тестов, то ручное управление транзакциями внутри теста весьма чревато боком, если вообще возможно. Пришлось бы переставлять эту аннотацию на конкретные классы, а местами - и только на конкретные методы в большом количестве мест - к черту. Хочу пометить нужные тест-кейсы аннотацией, указать в ней количество потоков - и все! Выглядеть она будет, например, так:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ConcurrentRun { int threads() default 10; }
Хорошо, что нам на этот счет предлагает Spring и JUnit? Запуск тестов JUnit-ом осуществляется при помощи BlockJUnit4ClassRunner, от которого наследуется Spring-овый SpringJUnit4ClassRunner, отвечающий, как раз, за запуск всех тестов с использованием IoCC. Лезем в код. Ага, вот это место, в котором запускается проверка теста:
@Override protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) { EachTestNotifier eachNotifier = springMakeNotifier(frameworkMethod, notifier); if (isTestMethodIgnored(frameworkMethod)) { eachNotifier.fireTestIgnored(); return; } eachNotifier.fireTestStarted(); try { methodBlock(frameworkMethod).evaluate(); } catch (AssumptionViolatedException e) { eachNotifier.addFailedAssumption(e); } catch (Throwable e) { eachNotifier.addFailure(e); } finally { eachNotifier.fireTestFinished(); } }
Где methodBlock(...) - это метод, который осуществляет создание инстанса теста, и вызывает, собственно, метод теста, обернутый во все эти JUnit-овые @Before/@After и так далее. Вот таким образом:
@Override protected Statement methodBlock(FrameworkMethod frameworkMethod) { Object testInstance; try { testInstance = new ReflectiveCallable() { @Override protected Object runReflectiveCall() throws Throwable { return createTest(); //#!!! } }.run(); } catch (Throwable e) { return new Fail(e); } Statement statement = methodInvoker(frameworkMethod, testInstance); statement = possiblyExpectingExceptions(frameworkMethod, testInstance, statement); statement = withRulesReflectively(frameworkMethod, testInstance, statement); statement = withBefores(frameworkMethod, testInstance, statement); statement = withAfters(frameworkMethod, testInstance, statement); statement = withPotentialRepeat(frameworkMethod, testInstance, statement); statement = withPotentialTimeout(frameworkMethod, testInstance, statement); return statement; }
(тут прямо-таки просится fp-style вызовов всех этих методов, но да не о том речь)

Ну что ж, нет проблем - перепишем этот кусок кода так, чтобы запуск тестов происходил в нескольких потоках:
@Override protected void runChild(FrameworkMethod frameworkMethod, RunNotifier notifier) { EachTestNotifier eachNotifier = innerMakeNotifier(frameworkMethod, notifier); if (isTestMethodIgnored(frameworkMethod)) { eachNotifier.fireTestIgnored(); return; } int threads = 1; if (frameworkMethod != null){ Method method = frameworkMethod.getMethod(); if (method.getAnnotation(ConcurrentRun.class) != null){ int threadsFromAnnotation = method.getAnnotation(ConcurrentRun.class).threads(); if (threadsFromAnnotation > 1){ threads = threadsFromAnnotation; } } } if (threads > 1) { evaluateConcurrently(frameworkMethod, eachNotifier, threads); } else { evaluateSameThread(frameworkMethod, eachNotifier); } } protected void evaluateSameThread(FrameworkMethod frameworkMethod, EachTestNotifier eachNotifier) { try { methodBlock(frameworkMethod).evaluate(); } catch (AssumptionViolatedException e) { eachNotifier.addFailedAssumption(e); } catch (Throwable e) { eachNotifier.addFailure(e); } finally { eachNotifier.fireTestFinished(); } } protected void evaluateConcurrently(FrameworkMethod frameworkMethod, EachTestNotifier eachNotifier, int threads) { try { CountDownLatch endLatch = new CountDownLatch(threads); for (int i = 0; i < threads; i++){ TestThreadEvaluationTask evaluationTask = new TestThreadEvaluationTask(eachNotifier, frameworkMethod, endLatch, i); evaluationTask.start(); } endLatch.await(); eachNotifier.fireTestFinished(); // #1 } catch (InterruptedException ie){ Thread.currentThread().interrupt(); } } private class TestThreadEvaluationTask extends Thread { final EachTestNotifier notifier; final FrameworkMethod fwMethod; final CountDownLatch endLatch; TestThreadEvaluationTask(EachTestNotifier notifier, FrameworkMethod fwMethod, CountDownLatch endLatch, int number) { this.notifier = notifier; this.fwMethod = fwMethod; this.endLatch = endLatch; setName("Test "+fwMethod.getName()+" thread "+number); } @Override public void run() { try { methodBlock(fwMethod).evaluate(); } catch (AssumptionViolatedException e) { notifier.addFailedAssumption(e); //#2 } catch (Throwable e) { notifier.addFailure(e); //#3 } finally { endLatch.countDown(); } } }
Что мы в этом коде делаем? Смотрим на наличие нашей аннотации - если нет, запускаем тест, как раньше в том же потоке. Если есть - запускаем несколько потоков по оценке теста, ждем, пока они все так или иначе отработают. Обратите внимание на строчки помеченные ##1-3: событие завершения теста будет одно, независимо от количества потоков, что даст нам правильное поведение, скажем, в CI - у нас не случится внезапного вырастания количества тестов в отчете во много раз только от того, что мы поставили аннотацию. В то же время, случись что, ошибок нам он напишет столько, в скольких потоках shit happened.

Все? Нет, черта с два. Проблема в том, что контекст менеджер TestContextManager, в рамках которого запускается тест, и который отвечает за значения инъецированных свойств, один на инстанс runner-а. Если взглянуть на строчку метода methodBlock, помеченную "!!!", становится ясно к чему это приведет: при запуске потоки начнут перезаписывать друг другу контекстные данные, приводя к некорректно инициализированным зависимостям или и вовсе NPE. Простое решение - перевести контекст менеджер в ThreadLocal.
// bla-bla-bla... public class ConcurrentTestExecutionRunner extends BlockJUnit4ClassRunner { // bla-bla-bla... public ConcurrentTestExecutionRunner(Class clazz) throws InitializationError { super(clazz); } private ThreadLocal<testcontextmanager> tlTestContextManager = new ThreadLocal<testcontextmanager>(){ @Override protected TestContextManager initialValue() { Class clazz = getTestClass().getJavaClass(); return new TestContextManager(clazz, getDefaultContextLoaderClassName(clazz)); } }; //... others runner's methods }
Здесь мне пришлось полностью отказаться от наследования SpringJUnit4ClassRunner и унаследоваться от BlockJUnit4ClassRunner, просто скопировав некоторые методы Spring-ового в свой runner, потому что в SpringJUnit4ClassRunner контекст менеджер объявлен как private final.

После этих изменений тесты успешно заработали в многопоточном варианте. Для удобства, я определил базовый класс для всех таких тестов, добавив туда статические методы для вызова разнообразных очищающих базу sql-скриптов в @AfterClass, подобные тем, которые есть в AbstractTransactionalJUnit4SpringContextTests, только там они нестатические и для @AfterClass непригодны.

В конечном итоге родительский класс для всех тестов выглядит примерно так:
@RunWith(ConcurrentTestExecutionRunner.class) @Transactional @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class}) public abstract class AbstractConcurrentTransactionalSpringTest implements ApplicationContextAware { //blah-blah-blah... }
Ну и вызов метода теста, соответственно,
@Test @ConcurrentRun(threads = 100) public void testSomeBusinessCaseInHundredOfTransactions() { // here go calls, asserts etc. }

Надеюсь, кому-то это окажется полезным.