Переходим на JUnit 4 (исходники)

Эндрю Гловер

Версия JUnit 4 отошла от прежних строгих соглашений о присваивании имён и иерархий наследования в пользу рациональности и гибкости аннотаций Java™ 5. В этом учебном пособии, которое дополняет популярную серию материалов по повышению качества программного кода, специалист по тестированию Эндрю Гловер (Andrew Glover) демонстрирует, как использовать обеспечиваемые аннотациями новые возможности - параметризованные тесты, тесты исключений и тесты с ограничением по времени. Кроме того, это учебное пособие рассказывает о гибких фикстурах JUnit 4 и показывает, как использовать аннотации вместо наборов (suite) для логического группирования тестов до начала их исполнения. В учебное пособие включены несколько примеров тестов, исполняемых в среде Eclipse, а также инструкции по исполнению тестов JUnit 4 с помощью старых, несовместимых версий инструмента Ant.

Цели документа

Это учебное пособие шаг за шагом демонстрирует фундаментальные концепции JUnit 4, при этом основное внимание уделяется аннотациям Java 5. Освоив это учебное пособие, рассчитанное на один час занятий, вы поймете основные отличия версии JUnit 4, а также познакомитесь с такими возможностями JUnit 4, как тесты исключений, параметризованные тесты и новая гибкая фикстурная модель. Из этого пособия вы узнаете о том, как объявлять тест, как использовать аннотации (вместо наборов тестов) для логического группирования тестов перед исполнением и как запускать тесты в средах Eclipse 3.2, Ant, а также из командной строки.

Предварительные условия

Чтобы получить максимальную отдачу от предлагаемого учебного пособия, вы должны иметь общее представление о разработке в среде Java. Кроме того, материал учебного пособия написан в предположении, что читатель осознает значение тестирования на этапе разработки и знаком с основами сопоставления с шаблонами. Для успешного освоения раздела, посвященного исполнению тестов JUnit 4, вам нужно обладать навыками работы с интегрированной средой разработки Eclipse 3.2 и с инструментом Ant версии 1.6 или выше. Знакомство с предшествующими версиями среды JUnit для работы с данным учебным пособием не обязательно.

Требования к системе

Для практической работы с представленным в учебном пособии программным кодом необходима работающая среда Sun JDK версии 1.5.0_09 (или выше) или IBM developer kit for Java technology версии 1.5.0 SR3. Для изучения разделов пособия, касающихся прогона тестов JUnit 4 в среде Eclipse, необходима работающая среда Eclipse версии 3.2 или выше. Для разделов пособия, касающихся прогона тестов с помощью Ant, необходим, соответственно, инструмент Ant версии 1.6 или выше.

Рекомендованная конфигурация системы для работы с данным учебным пособием:

  • Работающая среда Sun JDK версии 1.5.0_09 (или выше) или IBM developer kit for Java technology версии 1.5.0 SR3.
  • Не менее 500 МБ оперативной памяти.
  • Не менее 20 МБ свободного пространства на жестком диске для установки компонентов программного обеспечения и рассматриваемых в пособии примеров.

Указания данного учебного пособия изложены применительно к операционной системе Microsoft Windows. Все инструменты, рассмотренные в учебном пособии, способны также работать в операционных системах Linux и UNIX.

Новые возможности JUnit 4

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

  • Параметризованные тесты
  • Тесты исключений
  • Тесты с ограничением по времени
  • Гибкие фикстуры
  • Простые средства игнорирования тестов
  • Новый способ логического группирования тестов

В первом разделе учебного пособия объясняются важнейшие фундаментальные изменения, реализованные в версии JUnit 4. Затем мы подробно обсудим эти и другие новые функции в последующих разделах.

Прощание с прошлым

До появления в версии JUnit 4 аннотаций Java 5 эта инфраструктура тестирования опиралась на два соглашения, имеющих важнейшее значение для ее функционирования. Первое соглашение заключалось в том, что в JUnit неявным образом требовалось, чтобы имя любого метода, предназначенного для функционирования в качестве логического теста, начиналось с префикса test . Любой метод, имя которого начиналось с этого префикса, например, testUserCreate, исполнялся в соответствии с хорошо описанным процессом тестирования, который гарантировал исполнение соответствующей фикстуры (fixture) как до, так и после этого тестового метода. Второе соглашение, позволяющее среде JUnit распознавать нужный объект - класс, содержащий тесты - заключалось в том, что сам этот класс должен был являться расширением класса TestCase среды JUnit (или некоторым производным от него). Тест, нарушавший любое из указанных соглашений, не мог быть исполнен .

В листинге 2 показан тест JUnit, написанный до появления версии JUnit 4.

Листинг 1. Неужели все должно быть настолько сложно?

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {
	
 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCode() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

Новые возможности

В JUnit 4 за счет использования аннотаций Java 5 удалось полностью отказаться обоих вышеуказанных соглашений. Отпадает необходимость в иерархии классов, а методы, предназначенные для функционирования в качестве тестов, достаточно промаркировать новой аннотацией @Test.

В листинге 2 показан тот же тест, что и в листинге 1, но переписанный с использованием аннотаций

Листинг 2. Тестирование с применением аннотаций

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyGoodZipCode() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

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

Упрощение процесса документирования

Один из полезных побочных эффектов аннотаций заключается в том, что аннотации наглядно документируют все то, что должен делать каждый метод - без необходимости глубокого понимания внутренней модели инфраструктуры тестирования. Что может быть нагляднее, чем маркировка тестового метода аннотацией @Test? Это существенное усовершенствование по сравнению с предшествующими версиями JUnit, для которых требовалось хорошее знание соглашений JUnit даже если вы просто хотели понять, какой вклад вносит каждый метод в общий тестовый сценарий (test case).

Аннотации существенно помогают при анализе ранее написанного теста, однако они оказываются еще полезнее в процессе написания новых тестов.

Тестирование с применением аннотаций

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

Объявление теста

Объявление теста в JUnit 4 сводится к маркировке тестового метода аннотацией @Test. Обратите внимание (листинг 3), что нет необходимости в наследовании от какого-либо специального класса.

Листинг 3. Объявление теста в JUnit 4

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertFalse;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;
  
 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }
}

Замечание относительно статического импорта

В приведенном выше примере (листинг 3) я использовал поддерживаемую в Java 5 функцию статического импорта для импортирования метода assertFalse() класса Assert. Это объясняется тем, что в версии JUnit 4 тестовые классы не наследуют от класса TestCase, как это было в предшествующих версиях JUnit.

Тестирование исключений

Как и в предшествующих версиях JUnit, настоятельно рекомендуется указать тесту на необходимость выдачи определенных исключений (Exception). Единственная ситуация, когда это правило неприменимо - если вы собираетесь протестировать само конкретное исключение. Если тест выдает исключение, инфраструктура (framework) тестирования сообщает об отрицательном результате тестирования.

При необходимости тестирования конкретного исключения следует использовать предлагаемую в версии JUnit 4 аннотацию @Test с параметром expected. Этот параметр предназначен для представления типа исключения, которое данный тест должен выдавать в процессе исполнения.

Простое сравнение наглядно показывает, что дает этот новый параметр.

Тестирование исключений в версии JUnit 3.8

В листинге 4 показан тест testZipCodeGroupException() для среды JUnit 3.8, который проверяет, не приведет ли попытка получить третью группу объявленного мной регулярного выражения к исключению IndexOutOfBoundsException:

Листинг 4. Тестирование исключений в версии JUnit 3.8

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {

 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCodeGroupException() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  try{
   mtcher.group(2);
   fail("No exception was thrown");
  }catch(IndexOutOfBoundsException e){
  }
 }
}

В этой старой версии JUnit для такого простого теста мне пришлось написать изрядное количество кода - а именно конструкцию try/catch и условия неудачи теста на случай, если исключение не будет зафиксировано.

Тестирование исключений в версии JUnit 4

Показанный в листинге 5 тест исключения отличается от теста в листинге 4 только использованием параметра expected. (Обратите внимание, что для модификации теста из листинга 4 мне достаточно было поместить исключение IndexOutOfBoundsException в аннотацию @Test.)

Листинг 5. Тестирование исключений с параметром expected

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

Тестирование с ограничением по времени

В версии JUnit 4 в качестве параметра тестового сценария (test case) может быть использовано значение лимита времени (timeout). Как можно увидеть из листинга 6, значение timeout представляет максимальное количество времени, отводимого на исполнение данного теста: при превышении этого лимита времени тест завершается неудачей.

Листинг 6. Тестирование с ограничением по времени

                    
@Test(timeout=1)
public void verifyFastZipCodeMatch() throws Exception{		
 Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})?$"); 
 Matcher mtcher = pattern.matcher("22011");
 boolean isValid = mtcher.matches();		
 assertTrue("Pattern did not validate zip code", isValid);
}

Организовать тестирование с ограничением по времени несложно - для создания автоматизированного теста с ограничением по времени достаточно после аннотации @Test указать значение параметра timeout.

Игнорирование тестов

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

В версии JUnit 4 предусмотрена аннотация @Ignore, которая заставляет инфраструктуру тестирования проигнорировать данный тестовый метод. Можно также вставить комментарий к вашему решению об игнорировании теста - для разработчиков, которые могут впоследствии случайно столкнуться с этим тестом.

Аннотация @Ignore

В листинге 7 показано, насколько просто проигнорировать тест, регулярное выражение которого пока не работает:

Листинг 7. Игнорирование теста

                    
@Ignore("this regular expression isn't working yet")
@Test
public void verifyZipCodeMatch() throws Exception{		
 Pattern pattern = Pattern.compile("^\\d{5}([\\-]\\d{4})"); 
 Matcher mtcher = pattern.matcher("22011");
 boolean isValid = mtcher.matches();		
 assertTrue("Pattern did not validate zip code", isValid);
}

Все записано!

Как показано на рис. 1, попытка прогона этого теста в среде Eclipse (к примеру) приведет к появлению сообщения о проигнорированном тесте.

Рис. 1. Представление проигнорированного теста в среде Eclipse
Представление проигнорированного теста в среде Eclipse

Тестовые фикстуры (Test fixture)

Тестовые фикстуры существовали и до версии JUnit 4, однако в этой версии фикстурная модель существенно обновлена и усовершенствована. В этом разделе объясняется, зачем и в каких случаях целесообразно применять фикстуры, и демонстрируются различия между негибкими фикстурами предшествующих версий и новой фикстурной моделью в версии JUnit 4.

Для чего нужны фикстуры

Фикстуры помогают многократно использовать программный код за счет правила, которое гарантирует исполнение определенной логики до или после исполнения теста. В предшествующих версиях JUnit это правило неявно подразумевалось вне зависимости от реализации фикстур разработчиком. В версии JUnit 4 фикстуры указываются в явном виде посредством аннотаций, другими словами, правило реализуется только в том случае, если вы действительно решили использовать соответствующую фикстуру.

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

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

Негибкие фикстуры

В предшествующих версиях JUnit применялась негибкая фикстурная модель, при которой каждый тестовый метод необходимо было обрамлять методами setUp() и tearDown(). Возможные негативные стороны этой модели демонстрируются в листинге 8, где метод setUp() реализуется и затем исполняется дважды - для каждого заданного теста.

Листинг 8. Негибкие фикстуры

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.TestCase;

public class RegularExpressionTest extends TestCase {

 private String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private Pattern pattern;

 protected void setUp() throws Exception {
  this.pattern = Pattern.compile(this.zipRegEx);
 }

 public void testZipCodeGroup() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1));
 }

 public void testZipCodeGroupException() throws Exception{		 
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  try{
   mtcher.group(2);
   fail("No exception was thrown");
  }catch(IndexOutOfBoundsException e){
  }
 }
}

Обходные пути

Как показано в листинге 9, в предшествующих версиях JUnit можно было указать, чтобы фикстура исполнялась только один раз, с помощью «декоратора» TestSetup. Однако этот способ был достаточно обременительным (обратите внимание, что в данном случае необходим метод suite()).

Листинг 9. Применение TestSetup в предшествующих версиях JUnit

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.extensions.TestSetup;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import junit.textui.TestRunner;

public class OneTimeRegularExpressionTest extends TestCase {

 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 public static Test suite() {
  TestSetup setup = new TestSetup(
    new TestSuite(OneTimeRegularExpressionTest.class)) {
     protected void setUp() throws Exception {
      pattern = Pattern.compile(zipRegEx);
    }
   };
  return setup;
 }

 public void testZipCodeGroup() throws Exception {
  Matcher mtcher = pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();
  assertEquals("group(1) didn't equal -5051", "-5051", mtcher.group(1));
 }

 public void testZipCodeGroupException() throws Exception { 
  Matcher mtcher = pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();
  try {
   mtcher.group(2);
   fail("No exception was thrown");
  } catch (IndexOutOfBoundsException e) {
  }
 }
}

Достаточно сказать, что в предшествующих версиях JUnit сложность использования фикстур зачастую перевешивала преимущества от их применения.

Гибкие возможности версии JUnit 4.0

Благодаря использованию аннотаций в версии JUnit 4 накладные расходы, связанные с фикстурами, существенно сокращены. Аннотации позволяют исполнять одну и ту же фикстуру для каждого теста или всего один раз для всего класса, или не исполнять ее совсем. Предусмотрено четыре аннотации фикстур - две для фикстур уровня класса и две для фикстур уровня метода. На уровне класса используются фикстуры @BeforeClass и @AfterClass, а на уровне метода (или теста) используются фикстуры @Before и @After.

В тестовый сценарий в листинге 10 включена фикстура, которая с помощью аннотации @Before исполняется для обоих тестов.

Листинг 10. Гибкие фикстуры на основе аннотаций

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @Before
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

Однократные фикстуры

Предположим, вам необходимо исполнить фикстуру всего один раз. Вместо того, чтобы применять декоратор, как это делалось в предшествующих версиях (см. листинг 9), вы можете использовать аннотацию @BeforeClass, как показано в листинге 11:

Листинг 11. Однократная фикстура в JUnit 4

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class RegularExpressionJUnit4Test {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyZipCodeNoMatch() throws Exception{		
  Matcher mtcher = this.pattern.matcher("2211");
  boolean notValid = mtcher.matches();		
  assertFalse("Pattern did validate zip code", notValid);
 }

 @Test(expected=IndexOutOfBoundsException.class)
 public void verifyZipCodeGroupException() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101-5051");
  boolean isValid = mtcher.matches();			
  mtcher.group(2);		
 }
}

Функциональность TearDown в версии JUnit 4

Отметим, что метод tearDown() из предшествующих версий JUnit не исчез и в новой фикстурной модели. Чтобы воспроизвести функциональность метода tearDown(), достаточно создать какой-либо новый метод и применить аннотацию @After или @AfterClass.

Гибкость в квадрате

Версия JUnit 4 позволяет указать для тестового сценария более одной фикстуры. Новые фикстуры на основе аннотаций не препятствуют созданию нескольких фикстурных методов @BeforeClass. Однако следует учитывать, что в версии JUnit 4 нельзя указать, какой фикстурный метод должен исполняться в первую очередь. Это может привести к затруднениям в случае, если вы собираетесь использовать более одного фикстурного метода.

Исполнение тестов JUnit 4

К числу замечательных особенностей обновленной и усовершенствованной версии JUnit 4 относится отсутствие т.н. наборов тестов (Test suite) - механизма, использовавшегося в предшествующих версиях для логического группирования тестов для последующего совместного исполнения. В этом разделе я представлю новые оптимизированные аннотации, пришедшие на смену наборам, и покажу, как прогонять тесты JUnit 4 в среде Eclipse и с помощью инструмента Ant.

Набор тестов из предшествующих версий JUnit

Чтобы понять разницу, посмотрите на набор тестов из предшествующих версий JUnit, показанный в листинге 12 (этот набор группирует два логических тестовых класса и исполняет их как единое целое).

Листинг 12. Набор тестов из предшествующих версий JUnit

                    
import junit.framework.Test;
import junit.framework.TestSuite;

public class JUnit3Suite {

 public static Test suite() {
  TestSuite suite = new TestSuite();
  suite.addTest(OneTimeRegularExpressionTest.suite());
  suite.addTestSuite(RegularExpressionTest.class);		
  return suite;
 }
}

Две новые полезные аннотации

В версии JUnit 4 на смену семантике набора тестов пришли две новые аннотации. Первая из них - @RunWith - позволяет для прогона конкретного тестового класса применять других «исполнителей» (runner) вместо исполнителей, встроенных в инфраструктуру тестирования. В комплект JUnit 4 входит исполнитель наборов тестов - класс под названием Suite - который необходимо указывать в аннотации @RunWith. Кроме того, необходима еще одна аннотация под названием @SuiteClasses, параметром которой является список классов, представляющих набор тестов.

Листинг 13. Очень полезные аннотации

                    
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({ParametricRegularExpressionTest.class,
      RegularExpressionTest.class,
      TimedRegularExpressionTest.class})
public class JUnit4Suite {

}

 
Не беспокойтесь, если новые аннотации JUnit 4 кажутся вам непонятными. По всей видимости, создатели JUnit 4 сами испытывают подобные чувства. В Javadocs они объясняют, что планируют дорабатывать API-интерфейс исполнителя (runner) «по мере получения информации о том, как люди используют его на практике». По крайней мере честно!
 

Прогон тестов JUnit 4 в среде Eclipse

Тестовые классы JUnit 4 можно исполнять как с помощью интегрированной среды разработки, например, Eclipse, так и с помощью интерфейса командной строки. Для запуска тестов JUnit в среде Eclipse версии 3.2 и выше необходимо выбрать вариант тестирования Run As JUnit. Для прогона теста с помощью командной строки необходимо исполнить класс org.junit.runner.JUnitCore, передав ему в качестве аргумента полное имя теста.

Например, если при работе в Eclipse вы не хотите использовать встроенный исполнитель JUnit, вы можете задать новую конфигурацию прогона, предварительно задав класс JUnitCore (см. рис. 2).

Рис. 2. Первый шаг для прогона теста JUnit 4 в Eclipse с помощью командной строки
 

Указание теста

Затем необходимо указать тест для прогона, для чего нужно добавить полное имя соответствующего теста в окне Program arguments на вкладке Arguments (см. рис. 3).

Рис. 3. Второй шаг для прогона теста JUnit 4 в Eclipse с помощью командной строки

Инструмент Ant и JUnit 4

Ant и JUnit уже давно являются прекрасной парой, и многие разработчики ожидали, что с появлением версии JUnit 4 эти отношения только укрепятся. Однако возникли неожиданные сложности. В версиях Ant ниже 1.7 исполнять тесты JUnit 4 в неизменном виде невозможно. Это не значит, что их нельзя исполнить в принципе - их просто необходимо модифицировать.

Неподходящее сочетание

Прогон теста JUnit 4 (листинг 14) в Ant (версии ниже 1.7) дает интересные результаты:

Листинг 14. Простой тестовый класс JUnit 4

                    
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class RegularExpressionTest {
 private static String zipRegEx = "^\\d{5}([\\-]\\d{4})?$";
 private static Pattern pattern;

 @BeforeClass
 public static void setUpBeforeClass() throws Exception {
  pattern = Pattern.compile(zipRegEx);
 }

 @Test
 public void verifyGoodZipCode() throws Exception{		
  Matcher mtcher = this.pattern.matcher("22101");
  boolean isValid = mtcher.matches();		
  assertTrue("Pattern did not validate zip code", isValid);
 }
}

Провал по всем показателям

Запуск привычного задания junit в Ant дает ошибки, показанные в листинге 15.

Листинг 15. Множество ошибок

                    
[junit] Running test.com.acme.RegularExpressionTest
[junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec
[junit] Testsuite: test.com.acme.RegularExpressionTest
[junit] Tests run: 1, Failures: 1, Errors: 0, Time elapsed: 0.047 sec

[junit] Testcase: warning took 0.016 sec
[junit]     FAILED
[junit] No tests found in test.com.acme.RegularExpressionTest
[junit] junit.framework.AssertionFailedError: No tests found in
  test.com.acme.RegularExpressionTest
[junit] Test test.com.acme.RegularExpressionTest FAILED

Обходной маневр

Если вы хотите исполнять тесты JUnit 4 в Ant версии ниже 1.7, необходимо дополнить ваш набор тестов методом suite() , который будет возвращать экземпляр JUnit4TestAdapter (см. листинг 16).

Листинг 16. Новое применение старого метода

                    
public static junit.framework.Test suite(){
 return new JUnit4TestAdapter(RegularExpressionTest.class);
}

В этом экземпляре необходимо полностью определить возвращаемый тип Test, поскольку имеется аннотация со сходным именем @Test. При наличии метода suite() любая версия Ant исполнит все ваши тесты JUnit 4 без проблем!

 

Параметризованное тестирование

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

В версии JUnit 4 реализована новая прекрасная возможность, позволяющая создавать общие тесты, в которые можно направлять различные значения параметров. В результате вы можете создать один тестовый сценарий и прогнать его несколько раз - по одному разу для каждого параметра.

Простота параметризованного тестирования

Создание параметрического теста в JUnit 4 производится в пять простых шагов:

  1. Создание типового теста без конкретных значений параметров.
  2. Создание метода static, который возвращает тип Collection и маркирует его аннотацией @Parameter.
  3. Создание членов класса для типов параметров, которые требуются для типового метода, описанного на шаге 1.
  4. Создание конструктора, которые связывает эти типы параметров с соответствующими членами класса, описанными на шаге 3.
  5. Указание тестового сценария, который необходимо исполнять вместе с классом Parameterized (достигается использованием аннотации @RunWith).

Рассмотрим указанные шаги поочередно.

Шаг 1. Создание типового теста

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

Листинг 17. Типовой тест
                    
@Test
public void verifyGoodZipCode() throws Exception{		
 Matcher mtcher = this.pattern.matcher(phrase);
 boolean isValid = mtcher.matches();		
 assertEquals("Pattern did not validate zip code", isValid, match);
}

Шаг 2. Создание метода ввода параметров

На данном шаге создается метод ввода параметров, объявляемый как static и возвращающий тип Collection. Этот метод необходимо снабдить аннотацией @Parameters. Внутри этого метода вам достаточно создать многомерный массив Object и преобразовать его в список List, как показано в листинге 18:

Листинг 18. Метод ввода параметров с аннотацией @Parameters
                    
@Parameters
public static Collection regExValues() {
 return Arrays.asList(new Object[][] {
  {"22101", true },
  {"221x1", false },
  {"22101-5150", true },
  {"221015150", false }});
}

Шаг 3. Создание двух членов класса

Поскольку параметры имеют типы String и boolean, на следующем шаге вы создаете два члена класса.

Листинг 19. Объявление двух членов класса
                    
private String phrase;
private boolean match;

Шаг 4. Создание конструктора

Конструктор, который вы создадите на этом шаге, свяжет члены класса с вашими значениями параметров (см. листинг 20):

Листинг 20. Конструктор для связывания значений
                    
public ParametricRegularExpressionTest(String phrase, boolean match) {
 this.phrase = phrase;
 this.match = match;
}

Шаг 5. Задание класса Parameterized

И, наконец, на уровне класса необходимо указать, что данный тест должен исполняться вместе с классом Parameterized, как показано в листинге 21:

Листинг 21. Специфицирование класса Parameterized и аннотации @RunWith
                    
@RunWith(Parameterized.class)
public class ParametricRegularExpressionTest {
 //...
}

Прогон теста

При исполнении этого тестового класса типовой тестовый метод verifyGoodZipCode() исполняется четыре раза - по одному разу для каждой пары значений, определенных в методе ввода данных regExValues(), см. листинг 18.

Если, например, этот тест прогоняется в среде Eclipse, то она сообщит о четырех прогонах (см. рис. 4).

Рис. 4. Прогон параметризованных тестов в среде Eclipse

Другие новинки

Помимо важных изменений, описанных выше, в версии JUnit 4 реализован и ряд менее значительных, в частности, введен новый метод assert и исключено терминальное состояние.

Новый метод assert

В версии JUnit 4 добавлен новый метод assert для сравнения содержимого массивов. Это изменение не является значительным; тем не менее, вам больше не придется проходить по всему содержимому массива и проверять каждый отдельный элемент.

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

Листинг 22. В версии JUnit 4 метод assertEquals поддерживает работу с массивами

                    
@Test
public void verifyArrayContents() throws Exception{
 String[] actual = new String[] {"JUnit 3.8.x", "JUnit 4", "TestNG"};
 String[] var = new String[] {"JUnit 3.8.x", "JUnit 4.1", "TestNG 5.5"};
 assertEquals("the two arrays should not be equal", actual, var);		
}

Теперь - без ошибок!

Небольшое, но приятное изменение в версии JUnit 4 заключается в отказе от понятия «ошибка». В то время как предшествующие версии JUnit сообщали и о количестве неудач, и о количестве ошибок, в версии JUnit 4 тест или проходит успешно, или завершается неудачей.

Что интересно, одновременно с исключением одного состояния появилось и одно новое - вследствие возможности игнорирования тестов. При прогоне серии тестов среда JUnit 4 сообщает о количестве неудач и о количестве проигнорированных тестов.

Заключение

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

В этом пособии мы шаг за шагом познакомились с версией JUnit 4 - от объявления теста до параметризованного тестирования. Вы открыли для себя такие новые возможности, как тестирование с ограничением по времени и тестирование исключений, и узнали об изменениях в таких знакомых вам понятиях, как фикстуры и логическое группирование. Кроме того, вы увидели, как выглядит прогон теста в среде Eclipse, и ознакомились с простым приёмом, который позволит вам исполнять тесты в инструменте Ant любой версии, даже ранее версии 1.7.

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=6561