Статьи

Поддержка встроенного ядра Hamcrest JUnit

В посте « Улучшение assertEquals с JUnit» и «Hamcrest» я кратко обсудил « основные » совпадения «Hamcrest» с современными версиями JUnit . В этом посте я уделил особое внимание использованию статического метода JUnit assertThat (T, Matcher) в сочетании с ядром Hamcrest is() matcher, который автоматически включается в более поздние версии JUnit. В этой статье я расскажу о дополнительных «основных» средствах сравнения Hamcrest, которые поставляются в комплекте с последними версиями JUnit.

Двумя преимуществами JUnit, включая встроенные в ядро средства сравнения Hamcrest, является то, что нет необходимости специально загружать Hamcrest и нет необходимости явно включать его в пути классов модульных тестов. Прежде чем взглянуть на более удобные «основные» средства сравнения Hamcrest, важно отметить, что я намеренно и неоднократно имею в виду «основные» средства сравнения Hamcrest, потому что последние версии JUnit предоставляют только «основные» (и не все ) средства сравнения Hamcrest. автоматически. Любые сопоставители Hamcrest вне основных сопоставителей по-прежнему необходимо загружать отдельно и явно указывать в пути к классам модульных тестов. Один из способов получить представление о том, что является «ядром» Hamcrest (и, следовательно, какие средства сопоставления доступны по умолчанию в последних версиях JUnit), состоит в том, чтобы взглянуть на документацию API этого Javadoc-пакета :

Из этой документации, предоставленной JUnit для пакета org.hamcrest.core , мы видим, что доступны следующие средства сопоставления (с их описаниями):

Учебный класс Описание класса Javadoc Здесь?
AllOf <Т> Вычисляет логическое соединение двух совпадений. да
AnyOf <Т> Вычисляет логическую дизъюнкцию двух сопоставителей. да
DescribedAs <Т> Предоставляет пользовательское описание другому сопоставителю. да
Является ли <T> Украшает другой Matcher, сохраняя поведение, но позволяя тестам быть немного более выразительными. Еще раз
IsAnything <Т> Совпадение, которое всегда возвращает истину. нет
IsEqual <Т> Это значение равно другому значению, как проверено Object.equals (java.lang.Object) invokedMethod? да
IsInstanceOf Проверяет, является ли значение экземпляром класса. да
IsNot <Т> Вычисляет логическое отрицание совпадения. да
IsNull <Т> Значение равно нулю? да
IsSame <Т> Является ли значение тем же объектом, что и другое значение? да

В моем предыдущем посте, демонстрирующем совпадение Hamcrest is() используемое вместе с assertThat() JUnit, я использовал реализацию IntegerArithmetic качестве тестового корма. Я буду использовать это снова здесь для демонстрации некоторых других основных совпадений Hamcrest. Для удобства этот класс воспроизводится ниже.

IntegerArithmetic.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package dustin.examples;
 
/**
 * Simple class supporting integer arithmetic.
 *
 * @author Dustin
 */
public class IntegerArithmetic
{
   /**
    * Provide the product of the provided integers.
    *
    * @param firstInteger First integer to be multiplied.
    * @param secondInteger Second integer to be multiplied.
    * @param integers Integers to be multiplied together for a product.
    * @return Product of the provided integers.
    * @throws ArithmeticException Thrown in my product is too small or too large
    *     to be properly represented by a Java integer.
    */
   public int multiplyIntegers(
      final int firstInteger, final int secondInteger, final int ... integers)
   {
      int returnInt = firstInteger * secondInteger;
      for (final int integer : integers)
      {
         returnInt *= integer;
      }
      return returnInt;
   }
}

В публикации « Улучшение assertEquals с JUnit» и «Hamcrest» я в основном опирался на is() чтобы сравнить ожидаемые результаты с реальными результатами для тестируемого целочисленного умножения. Другой вариант — использовать equalTo сравнения equalTo как показано в следующем листинге кода.

Использование Hamcrest equalTo ()

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * Test of multiplyIntegers method, of class IntegerArithmetic, using core
 * Hamcrest matcher equalTo.
 */
@Test
public void testWithJUnitHamcrestEqualTo()
{
   final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
   final int result = this.instance.multiplyIntegers(2, 3, integers);
   assertThat(result, equalTo(expectedResult));
}

Хотя это и не обязательно, некоторым разработчикам нравится использовать is и equalTo вместе, потому что он чувствует себя более свободно. Это и есть причина существования компании: использовать другие подходы более свободно. Я часто использую is() сама по себе (подразумевая equalTo() ), как обсуждалось в разделе Улучшение assertEquals с JUnit и Hamcrest . В следующем примере демонстрируется использование is() matcher в сочетании с equalTo .

Использование Hamcrest equalTo () с is ()

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * Test of multiplyIntegers method, of class IntegerArithmetic, using core
 * Hamcrest matcher equalTo with "is" Matcher..
 */
@Test
public void testWithJUnitHamcrestEqualToAndIsMatchers()
{
   final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
   final int result = this.instance.multiplyIntegers(2, 3, integers);
   assertThat(result, is(equalTo(expectedResult)));
}

Устройство equalTo equam equalTo Hamcrest выполняет сравнение, аналогичное вызову Object.equals (Object). Действительно, его функциональность сравнения опирается на использование базовой реализации объекта equals(Object) . Это означает, что последние два примера пройдут, потому что сравниваемые числа логически эквивалентны. Когда кто-то хочет обеспечить еще большее равенство идентификаторов (на самом деле те же объекты, а не просто один и тот же логический контент), можно использовать средство sameInstance sameInstance sameInstance как показано в следующем листинге кода. Несоответствие также применяется, потому что утверждение будет истинным, и тест будет проходить только с «не», потому что ожидаемые и фактические результаты НЕ совпадают!

Использование Hamcrest sameInstance () с not ()

01
02
03
04
05
06
07
08
09
10
11
12
/**
 * Test of multiplyIntegers method, of class IntegerArithmetic, using core
 * Hamcrest matchers not and sameInstance.
 */
@Test
public void testWithJUnitHamcrestNotSameInstance()
{
   final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
   final int result = this.instance.multiplyIntegers(2, 3, integers);
   assertThat(result, not(sameInstance(expectedResult)));
}

Иногда желательно контролировать текст, который выводится из утверждения неудачного модульного теста. JUnit включает ядро ​​соответствия Hamcrest asDescribed() для поддержки этого. Пример кода этого показан в следующем листинге, а результат этого неудачного теста (и соответствующее утверждение) показан на снимке экрана IDE NetBeans, который следует за листингом кода.

Использование Hamcrest asDescribed () с sameInstance ()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/**
 * Test of multiplyIntegers method, of class IntegerArithmetic, using core
 * Hamcrest matchers sameInstance and asDescribed. This one will assert a
 * failure so that the asDescribed can be demonstrated (don't do this with
 * your unit tests as home)!
 */
@Test
public void testWithJUnitHamcrestSameInstanceDescribedAs()
{
   final int[] integers = {4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
   final int expectedResult = 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 * 11 * 12 *13 * 14 * 15;
   final int result = this.instance.multiplyIntegers(2, 3, integers);
   assertThat(result,
              describedAs(
                 "Not same object (different identity reference)",
                 sameInstance(expectedResult)));
}

Использование описаний describedAs() позволило сообщать о более значимом сообщении, когда не удалось выполнить соответствующее утверждение модульного теста.

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

SetFactory.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
package dustin.examples;
 
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
 
/**
 * A Factory that provides an implementation of a Set interface based on
 * supplied SetType that indicates desired type of Set implementation.
 *
 * @author Dustin
 */
public class SetFactory<T extends Object>
{
   public enum SetType
   {
      ENUM(EnumSet.class),
      HASH(HashSet.class),
      SORTED(SortedSet.class), // SortedSet is an interface, not implementation
      TREE(TreeSet.class),
      RANDOM(Set.class);       // Set is an interface, not a concrete collection
 
      private Class setTypeImpl = null;
 
      SetType(final Class newSetType)
      {
         this.setTypeImpl = newSetType;
      }
 
      public Class getSetImplType()
      {
         return this.setTypeImpl;
      }
   }
 
   private SetFactory() {}
 
   public static SetFactory newInstance()
   {
      return new SetFactory();
   }
 
   /**
    * Creates a Set using implementation corresponding to the provided Set Type
    * that has a generic parameterized type of that specified.
    *
    * @param setType Type of Set implementation to be used.
    * @param parameterizedType Generic parameterized type for the new set.
    * @return Newly constructed Set of provided implementation type and using
    *    the specified generic parameterized type; null if either of the provided
    *    parameters is null.
    * @throws ClassCastException Thrown if the provided SetType is SetType.ENUM,
    *    but the provided parameterizedType is not an Enum.
    */
   public Set<T> createSet(
      final SetType setType, final Class<T> parameterizedType)
   {
      if (setType == null || parameterizedType == null)
      {
         return null;
      }
 
      Set<T> newSet = null;
      try
      {
         switch (setType)
         {
            case ENUM:
               if (parameterizedType.isEnum())
               {
                  newSet = EnumSet.noneOf((Class<Enum>)parameterizedType);
               }
               else
               {
                  throw new ClassCastException(
                       "Provided SetType of ENUM being supplied with "
                     + "parameterized type that is not an enum ["
                     + parameterizedType.getName() + "].");
               }
               break;
            case RANDOM:
               newSet = LinkedHashSet.class.newInstance();
               break;
            case SORTED:
               newSet = TreeSet.class.newInstance();
               break;
            default:
               newSet = (Set<T>) setType.getSetImplType().getConstructor().newInstance();
               break;
         }
      }
      catch (  InstantiationException
             | IllegalAccessException
             | IllegalArgumentException
             | InvocationTargetException
             | NoSuchMethodException ex)
      {
         Logger.getLogger(SetFactory.class.getName()).log(Level.SEVERE, null, ex);
      }
      return newSet;
   }
}

Придуманный класс, код которого только что показан, предоставляет возможность использовать дополнительные «основные» совпадения Hamcrest. Как описано выше, можно использовать все эти соответствия с помощью соответствия, чтобы улучшить беглость выражения. Двумя полезными «основными» сопоставителями являются nullValue() и notNullValue() , оба из которых будут продемонстрированы в следующем листинге кода на основе JUnit (и используется вместе в одном случае).

Использование Hamcrest nullValue () и notNullValue ()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
 * Test of createSet method, of class SetFactory, with null SetType passed.
 */
@Test
public void testCreateSetNullSetType()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(null, String.class);
   assertThat(strings, nullValue());
}
 
/**
 * Test of createSet method, of class SetFactory, with null parameterized type
 * passed.
 */
@Test
public void testCreateSetNullParameterizedType()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, null);
   assertThat(strings, is(nullValue()));
}
 
@Test
public void testCreateTreeSetOfStringsNotNullIfValidParams()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, String.class);
   assertThat(strings, notNullValue());
}

Сравнение Hamcrest instanceOf также полезно и продемонстрировано в следующем листинге кода (один пример, использующий instanceOf сам по себе, и один пример, использующий его вместе с is ).

Использование Hamcrest instanceOf ()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
@Test
public void testCreateTreeSetOfStringsIsTreeSet()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, String.class);
   assertThat(strings, is(instanceOf(TreeSet.class)));
}
 
@Test
public void testCreateEnumSet()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<RoundingMode> roundingModes = factory.createSet(SetType.ENUM, RoundingMode.class);
   roundingModes.add(RoundingMode.UP);
   assertThat(roundingModes, instanceOf(EnumSet.class));
}

До сих пор многие из основных средств сопоставления Hamcrest увеличивают беглость и читаемость, но мне нравятся следующие два по еще большим причинам. Функция сравнения hasItem() Hamcrest проверяет наличие hasItem() элемента в коллекции, а еще более полезная функция hasItems() Hamcrest — наличие нескольких заданных элементов в коллекции. Это легче увидеть в коде, и следующий код демонстрирует это в действии.

Использование Hamcrest hasItem () и hasItems ()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
@Test
public void testCreateTreeSetOfStringsHasOneOfAddedStrings()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, String.class);
   strings.add("Tucson");
   strings.add("Arizona");
   assertThat(strings, hasItem("Tucson"));
}
 
@Test
public void testCreateTreeSetOfStringsHasAllOfAddedStrings()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, String.class);
   strings.add("Tucson");
   strings.add("Arizona");
   assertThat(strings, hasItems("Tucson", "Arizona"));
}

Иногда желательно проверить результат определенного проверенного метода, чтобы убедиться, что он соответствует самым разнообразным ожиданиям. Вот где Hamcrest allOf пригодится. Этот сопоставитель гарантирует, что все условия (выраженные как сопоставители) выполняются. Это проиллюстрировано в следующем листинге кода, который проверяет с одним утверждением, что сгенерированный Set не является нулевым, имеет две конкретные строки и является экземпляром TreeSet .

Использование Hamcrest allOf ()

01
02
03
04
05
06
07
08
09
10
11
12
@Test
public void testCreateSetAllKindsOfGoodness()
{
   final SetFactory factory = SetFactory.newInstance();
   final Set<String> strings = factory.createSet(SetType.TREE, String.class);
   strings.add("Tucson");
   strings.add("Arizona");
   assertThat(
      strings,
      allOf(
         notNullValue(), hasItems("Tucson", "Arizona"), instanceOf(TreeSet.class)));
}

Чтобы продемонстрировать ядро ​​Hamcrest «anyOf», предоставленное «из коробки» с новыми версиями JUnit, я собираюсь использовать еще один смехотворно изобретенный Java-класс, который нуждается в модульном тестировании.

Today.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package dustin.examples;
 
import java.util.Calendar;
import java.util.Locale;
 
/**
 * Provide what day of the week today is.
 *
 * @author Dustin
 */
public class Today
{
   /**
    * Provide the day of the week of today's date.
    *
    * @return Integer representing today's day of the week, corresponding to
    *    static fields defined in Calendar class.
    */
   public int getTodayDayOfWeek()
   {
      return Calendar.getInstance(Locale.US).get(Calendar.DAY_OF_WEEK);
   }
}

Теперь мне нужно проверить, что единственный метод в вышеприведенном классе возвращает правильное целое число, представляющее день недели правильно. Мне бы хотелось, чтобы мои тесты гарантировали, что верное целое число, представляющее день с воскресенья по субботу, будет возвращено, но тестируемый метод таков, что он может не совпадать с днем ​​недели, возвращенным при любом данном тесте. В приведенном ниже листинге кода показано, как это можно протестировать с помощью совместимого с JUnit Hamcrest «anyOf».

Использование Hamcrest anyOf ()

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * Test of getTodayDayOfWeek method, of class Today.
 */
@Test
public void testGetTodayDayOfWeek()
{
   final Today instance = new Today();
   final int todayDayOfWeek = instance.getTodayDayOfWeek();
   assertThat(todayDayOfWeek,
              describedAs(
                 "Day of week not in range.",
                 anyOf(is(Calendar.SUNDAY),
                       is(Calendar.MONDAY),
                       is(Calendar.TUESDAY),
                       is(Calendar.WEDNESDAY),
                       is(Calendar.THURSDAY),
                       is(Calendar.FRIDAY),
                       is(Calendar.SATURDAY))));
}

Несмотря на то, что allcf allOf требует, чтобы все условия соответствовали, чтобы избежать утверждения, наличие какого-либо одного условия является достаточным, чтобы гарантировать, что anyOf не приведет к подтверждению отказа.

Мой любимый способ определить, какие основные средства сравнения Hamcrest доступны с JUnit, — это использовать завершение импорта в моей Java IDE. Когда я статически импортирую содержимое пакета org.hamcrest.CoreMatchers.* все доступные средства сопоставления. Я могу заглянуть в IDE, чтобы увидеть, что означает * чтобы увидеть, какие сопоставители доступны для меня.

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

Ссылка: встроенная поддержка JCnit Core Matcher от нашего партнера JCG Дастина Маркса в блоге Inspired by Actual Events .