Статьи

(Unit) Тестирование швейцарского ножа: все инструменты, которые вы хотели знать

Я люблю тестировать. И мне нравится производительность. Есть много инструментов и библиотек, которые делают написание тестов проще, удобнее, веселее. Я хотел бы представить здесь те из них, которые мне показались наиболее полезными за эти годы: от избранных расширенных функций JUnit до библиотек утверждений, мощного внедрения поведения / ошибок, тестирования кода, связанного с базой данных, и, наконец, повышения производительности тестирования в сто раз с помощью Groovy. ,

Этот пост сопровождает мою молниеносную беседу о JavaZone 2012, содержит более подробные сведения и дополнительные инструменты и советы.

Содержание

  1. Advanced JUnit 4: полезно знать
    1. Удаление дубликатов и тесты Simplyfing с помощью @Rule
    2. @RunWith (Параметризовано): повторно запустить тесты с разными данными
    3. Всемогущий Таможенный Бегун
    4. Условно игнорировать тесты с предположением
    5. Теории (Экспериментальные!)
  2. Тестирование DAO с помощью DbUnit Express
  3. Читабельные тесты с помощью Matchers
  4. Достижение Вечного Блаженства с Groovy
  5. Инъекция неисправностей с помощью JBoss Byteman
  6. Другие вещи, чтобы знать
    1. Тестирование интеграции Java EE с JBoss Arquillian
    2. Непрерывная интеграция в вашей IDE с Infinitest
  7. Код

Advanced JUnit 4: полезно знать

Удаление дубликатов и тесты Simplyfing с помощью @Rule

Правила были введены в JUnit вокруг версии 4.8. Проще говоря, правила могут выполнять действия до и после каждого теста или всего тестового примера, аналогично вашим методам @Before [Class] и @After [Class], но даже до / после них. Чтобы использовать правило, вы должны назначить ему открытое поле вашего тестового класса. Пример:

public class ExampleTest {
   @Rule public ExpectedException thrown = ExpectedException.none(); // *must* be public

   @Test
   public void throwsNullPointerExceptionWithMessage() {
      thrown.expect(NullPointerException.class); // same as @Test(expected=...)
      thrown.expectMessage("cool failure");      // check message substring
      throw new NullPointerException("My cool failure message");
  }
}

Вы можете использовать @Rule с открытым полем экземпляра для правил, которые должны действовать до / после каждого метода тестирования, и @ClassRule с открытым статическим полем для правил, которые должны действовать до / после всего теста.

Вы можете использовать существующие реализации правил, чтобы упростить ваши тесты. Вот некоторые из наиболее полезных правил (нажмите на название JavaDoc с примерами):

Если вы повторяете один и тот же код установки и разрыва в нескольких тестовых примерах, вам следует рассмотреть возможность создания собственной реализации правила (обычно путем расширения ExternalResource ) и проталкивать туда код установки / разрыва. Давайте как введем правило упрощенного тестирования пользователей DbUnit Express.

Перед введением @Rule каждый тест DbUnit Express требовал объявления тестера БД и его инициализации в setUp:

public class SimpleNonExtendingEmbeddedDbJUnit4Test {

    private final EmbeddedDbTester testDb = new EmbeddedDbTester();

    @Before
    public void setUp() throws Exception {
       testDb.onSetup();
   }
// ...

Представление  EmbeddedDbTesterRule упростило это до одной аннотированной строки:

public class EmbeddedDbTesterRuleTest {

    @Rule
    public final EmbeddedDbTesterRule embeddedDb = new EmbeddedDbTesterRule();
// ...

(EmbeddedDbTesterRule является более сложным, чем обычно, потому что я хотел расширить исходный класс EmbeddedDbTester и, следовательно, не смог расширить ExternalResource.)

См. Также СУХОЙ: используйте JUnit @Rule вместо повторения установки / @ Before в каждом тесте .

@RunWith (Параметризовано): повторно запустить тесты с разными данными

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

@Test public void acceptAllValidEmails() {
   String emails = {"word@example.com", "under_score@a.b.cz", "d.ot@here.org"};
   for (String email: emails) {
      assertTrue("Should accept " + email, this.filter.accept(email));
   }
}

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

@RunWith(value = Parameterized.class)
public class FilterTest {

   private String email;
   private Filter filter = ...;

   public FilterTest(String email) { this.email = email; } // parametrized constructor

   @Parameters // the 1st element of each array will be assigned to the 1st constructor parameter etc.
   public static Collection<Object[]> data() {
     return Arrays.asList(new String[][]  { {"word@example.com"}, {"under_score@a.b.cz"}, {"d.ot@here.org"}});
   }

   @Test public void acceptAllValidEmails() {
      assertTrue("Should accept " + this.email, this.filter.accept(this.email));
   }
}

Недостатком является то, что он требует довольно много печатать и поэтому окупается только иногда. Ограничение состоит в том, что вы не можете применить набор значений к одному методу тестирования, что связано с дизайном JUnit, который предполагает создание разных TestCase для каждого отдельного прибора, то есть данных / контекста теста (что не является плохой идеей). но иногда это слишком дорого).

Альтернативы:

  • Используйте средство запуска TwiP (доступно отдельно), которое добавляет поддержку параметров для методов тестирования и предоставляет значения из предоставленного набора или случайным образом из всего пространства
  • Используйте JUnitParamsRunner (доступный отдельно), который позволяет более гибкую спецификацию данных, и делайте это для каждого метода отдельно
  • Используйте TestNG :-)
  • Используйте JUnit Theories, см. Ниже

Всемогущий Таможенный Бегун

Вы наверняка использовали альтернативный JUnit runner (активируемый аннотацией @RunWith (<runner class>)), такой как Parametrized, MockitoJUnitRunner или SpringJUnit4ClassRunner. Но вы когда-нибудь задумывались о написании своего?

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

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

Вы обычно расширяете бегун по умолчанию BlockJUnit4ClassRunner ( source ), как, например, Mockito и Spring.

Условно игнорировать тесты с предположением

Полезно иметь возможность игнорировать тесты, основанные на условии, например, отключить тесты интеграции базы данных в средах, где база данных недоступна. Для этого вы можете использовать класс org.junit.Assume — если вы вызываете какой-либо из его методов accept * в тестовом методе, и он оценивается как false, то тест будет обработан, если он был помечен @Ignore. Вы можете игнорировать все тесты, выполнив это в методе @Before.

Пример: игнорировать тесты БД, если задано определенное системное свойство:

@Before public void ignoreTestsIfDbTestsDisabled() {
   assumeThat(System.getProperties().containsKey("tests.db.disable"), is(false));
}

(Предположим также часто используется вместе с теориями, чтобы определить, какие ценности являются законными для конкретной теории.)

Теории (Экспериментальные!)

Теории — это параметризованные методы испытаний, которые выполняются для каждой возможной комбинации значений их параметров. Значения определяются как поля, аннотированные @DataPoint или предоставляемые методом, аннотированным @DataPoints. Вы используете @RunWith (Theories.class) и аннотируете каждый метод тестирования с помощью @Theory вместо @Test.

Йенс Шаудер написал пример теста, в котором каждый метод (теория) теста выполняется для всех возможных двухэлементных комбинаций трех строковых значений . Джейкоб Чилдресс использует теории как альтернативу параметризованному, используя только один параметр (таким образом, нет никаких комбинаций) и метод @DataPoints. Адам Хепнер написал краткие инструкции для двухпараметрического метода и предположил в StackOverflow.

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

Обратите внимание, что вы можете исключить некоторые комбинации / значения с помощью предположения (обсуждалось выше).

Тестирование DAO с помощью DbUnit Express

DbUnit Express — это тонкая оболочка для DbUnit (модульное тестирование кода, взаимодействующего с базой данных), предназначенная для того, чтобы сделать запуск с тестированием БД максимально быстрым и простым, введя соглашение о конфигурации, используя встроенную базу данных Derby из коробки (хотя Вы можете изменить это), предоставляя различные утилиты для инициализации, использования и проверки содержимого тестовой базы данных.

Главной особенностью DbUnit [Express] является возможность гарантировать чистое, определенное состояние базы данных перед каждым тестом, удаляя и вставляя данные на основе (необязательно тестируемых для класса) файлов набора данных (XML, CSV, Excel,…) , В дополнение к этому он обеспечивает доступ к базе данных через соединение или источник данных и утилиты для проверки данных. Цель DbUnit Express — помочь вам подготовить ваш первый полный тест БД за 10 минут в форме, которую можно легко распространить среди других разработчиков (БД в памяти автоматически инициализируется из предоставленного файла .ddl).

Пример:

public class ExampleJUnit4WithRuleTest {

	/**
	 * Initialize the test and instruct it to use a custom data set file instead of the default dbunit-test_data_set.xml.
	 * The set up of the test DB will be executed automaticaly thanks to the magic of @Rule.
	 */
    @Rule
    public EmbeddedDbTesterRule testDb = new EmbeddedDbTesterRule("EmbeddedDbTesterRuleTest-data.xml");

    @Test
    public void should_contain_data_supplied_by_dbunit() throws Exception {
    	// 1. TODO: Invoke the database-using class that you want to test, passing to it the test database
    	// via testDb.getDataSource() or testDb.getSqlConnection()
    	// ex.: new MyUserDao(testDb.getDataSource()).save(new User("Jakub", "Holy"));

    	// 2. Verify the results ...
    	// Here we use a checker to check the content of the my_test_table loaded from the EmbeddedDbTesterRuleTest-data.xml
        testDb.createCheckerForSelect("select some_text from my_test_schema.my_test_table")
                .withErrorMessage("No data found => onSetup wasn't executed as expected")
                .assertRowCount(1)
                .assertNext("EmbeddedDbTesterRuleTest data"); // the 1st row
    }
}

Подробнее о настройке, использовании и преимуществах читайте на странице документации DbUnit Express .

Связанные с:

Читабельные тесты с помощью Matchers

Тесты намного лучше, если они четко выражают то, что тестируют. Однако сам JUnit не позволяет (возможно, по замыслу?) Выражать более сложные условия, такие как проверка содержимого списка или определенных атрибутов объектов в списке. Вы можете сделать свои тесты намного более осмысленными и более простыми для понимания, используя библиотеки соответствия, такие как FEST-Assert (в настоящее время версия 2 вехи 7) или Hamcrest . В качестве бонуса вы получите гораздо более понятные и понятные сообщения об ошибках.

// JUnit
assertNotNull(list);
assertEquals(6, list.size());
assertTrue(list.contains(sam));
assertTrue(list.contains(frodo));
// FEST-Assert
assertThat(list)
 .hasSize(6)
 .contains(frodo, sam);
 
// Hamcrest assertThat(list.size(), is(6)); assertThat(list, hasItems(frodo, sam));

Лично я предпочитаю FEST-Assert, потому что он имеет хороший свободный API, который интуитивно понятен в использовании и легко обнаруживается с помощью автозаполнения в IDE. Hamcrest старше и страдает от проблем с генериками — раньше или позже вы сталкиваетесь со случаем, когда вам нужно добавить такие же странные приведения или преобразовать универсальный список в List, а затем привести его к List <SomethingElse>, чтобы заставить Hamcrest работать. Иногда даже случается, что код работает с Maven, но не работает с Eclipse или наоборот.

Hamcrest & generic hell от JF Smart и в StackOverflow (например, явное приведение к типу элемента коллекции ).

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

Достижение Вечного Блаженства с Groovy

Самый счастливый момент в моей тестовой жизни был, когда я понял, что могу использовать один язык — обычно Java — для производственного кода, а другой, гораздо более производительный для тестов — Groovy.

Зачем использовать Groovy?

  • Его синтаксис составляет 99,9% от Java + еще 1000% => вы можете копировать и вставлять в Groovy (и обратно), вы можете постепенно изучать продуктивные функции Groovy
  • Мощный, лаконичный, продуктивный
  • Имеет литералы для списков, карт, регулярных выражений и т. Д.
  • Groovy добавляет много чрезвычайно полезных и мощных методов в классы JDK, которые взлетят до вашей производительности:

  • Единый мощный assert («assert <expressions>»), обеспечивающий очень четкие сообщения об ошибках по всем частям задействованного выражения. С Groovy вам обычно не нужно изучать и использовать библиотеку соответствия
  • Закрытия сейчас и здесь :-)
  • Многострочные строки и строки с заменой переменных («text $ replaceWithVariableValue text»)
  • Полезные библиотеки, такие как Спок

Узнайте больше о том, как Groovy может сделать вас гораздо более продуктивным и счастливым, только на том, что Masochist будет писать модульные тесты на Java. Будьте умнее, используйте Groovy (или Scala …) .

Альтернативы : ScalaTest, если вы знаете Scala — он также очень хорошо интегрируется с JUnit и рабочим кодом на Java

Инъекция неисправностей с помощью JBoss Byteman

Байтмен — это чистая магия. Это может изменить поведение любого метода в стеке вызовов, выполняемых во время теста. И вам не нужно иметь доступ к объекту, которому принадлежит метод. Поведение вводится в целевом метод перед испытанием и удаляется , когда она заканчивает ( в отличие от большинства других АОПОВ инструментов , которые кодируют изменения во время загрузки и оставить его таким образом).

Существует два основных варианта использования Byteman:

  1. Тестирование обработки сбоев путем «внедрения» сбоев, обычно в форме выброса исключения, такого как SocketTimeoutException или FileNotFoundException, в методы где-то в стеке вызовов
  2. Тестирование устаревшего кода — Byteman позволяет вам сокращать / заменять зависимости, которые в противном случае не позволили бы вам тестировать класс (например, клиент веб-службы, созданный внутри класса в классе)

Напр .:

@RunWith(BMUnitRunner.class)
public class MyMainTest {

	@Test(expected = IllegalStateException.class)
	@BMRule(name="throw IllegalStateException from the helper class",
			targetClass = "MainsHelper",
			targetMethod = "sayHello",
			action = "throw new java.lang.IllegalStateException(\"Exception injected by Byteman\")")
	public void testSayHello() {
		new MyMain().sayHello();
		fail("sayHello should have failed due to Byteman injecting an exception into sayHello");
	}
}

Узнайте больше на  Cool Tools: Внедрение ошибок в модульные тесты с JBoss Byteman — Более легкое тестирование обработки ошибок и получите полный проект ExampleByteman от GitHub .

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

Альтернатива : JMockit (может быть проще в использовании, поскольку вы пишете код только как код Java, а не как текст, как в случае с Byteman)

Другие вещи, чтобы знать

Тестирование интеграции Java EE с JBoss Arquillian

Java EE в версии 5/6 стала простой, но мощной платформой, заслуживающей внимания. Если вы пойдете этим путем и передадите сквозные задачи в контейнер (в виде перехватчиков, внедрения зависимостей, событий и т. Д.), Вам также потребуется проверить, что эти части, управляемые контейнером, собраны правильно и работают должным образом.

JBoss Arquillian — лучший инструмент для этой работы — он позволяет вам определить, какие классы и ресурсы включить в тест, и запустить его на встроенном сервере (Glassfish, JBoss,…). Если вы используете Java EE, вам обязательно стоит взглянуть на Arquillian.

Непрерывная интеграция в вашей IDE с Infinitest

Infinitest ( Руководство пользователя ) — мой самый любимый плагин Eclipse / IntelliJ. Он обеспечивает немедленную обратную связь при каждом изменении производственного или тестового кода, выполняя тесты, затронутые этим изменением, и сообщая об их сбоях непосредственно в исходном коде в качестве маркеров.

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

Infinitest может быть легко сконфигурирован для пропуска некоторых тестов (обычно медленных / интеграционных тестов) и установки аргументов JVM для тестов и поддерживает как JUnit, так и TestNG. Это не совсем идеально, но в любом случае оно отлично работает. Настоятельно рекомендуется.

И еще более…

Смотрите мою вики-страницу по тестированию для еще большего количества инструментов и библиотек .

Код

Код доступен в моем репозитории UnitTestingSwissKnife GitHub .

PS: обратная связь приветствуется.