Вступление
Средство JUnit «Теории» — это (в основном) недокументированный экспериментальный инструмент, который позволяет вам запускать ваш метод @Test (вашу @Theory) для всех возможных комбинаций параметров (ваших @Datapoints), которые вы можете использовать.
Вы можете использовать инструмент Теории для критически важных и сложных сервисов, чтобы обеспечить их «пуленепробиваемость» против любых комбинаций параметров и параметров, которые вы можете себе представить.
Это особенно полезно для тестирования устаревших плохо закодированных служб, которые принимают множество параметров, причем некоторые из них функционально исключают некоторые другие, хотя это «технически возможно».
Простой пример
Много статей было написано на эту тему, обратитесь к ним для правильного введения, но вот короткий пример:
@RunWith(Theories.class) public class GuessTheMurdererTest { public static class Suspect { private String name; private String color; @Override public String toString() { return String.format("%s (%s)", name, color); } public Suspect(String name, String color) { this.name = name; this.color = color; } public String getName() { return name; } public String getColor() { return color; } } @DataPoints public static Suspect[] whoMadeIt = { new Suspect("Miss Rose", "Pink"), new Suspect("Mrs. Peacock", "Blue"), new Suspect("Professor Plum", "Purple") }; @DataPoints public static String[] whereWasIt = { "Library", "Lounge" }; @Theory public void dummyTest(Suspect murderer, String murderScene) { System.out.println(String.format("%s murdered Colonel Mustard in the %s", murderer, murderScene)); } }
Метод @Theory здесь — просто фиктивный метод, который печатает все возможные комбинации параметров, заданных @DataPoints.
Это будет выполнено с каждой возможной комбинацией Подозреваемого и комнаты, отсюда и следующий результат:
Miss Rose (Pink) murdered Colonel Mustard in the Library Miss Rose (Pink) murdered Colonel Mustard in the Lounge Mrs. Peacock (Blue) murdered Colonel Mustard in the Library Mrs. Peacock (Blue) murdered Colonel Mustard in the Lounge Professor Plum (Purple) murdered Colonel Mustard in the Library Professor Plum (Purple) murdered Colonel Mustard in the Lounge Professor Plum (Purple) murdered Colonel Mustard in the Lounge
Хорошо, все в порядке, но вот подвох …
Подвох: несколько параметров одного типа
Предположим, мы пытаемся протестировать какой-то старый унаследованный сервисный метод, чтобы найти уникальную комбинацию параметров, которая слишком долго вызывала «случайную» ошибку …
Старый метод не принимает объект «Подозреваемый» и «Комната» в качестве параметров, а только 2 строки …
Применительно к нашему примеру это будет примерно так:
@RunWith(Theories.class) public class GuessTheMurdererTest { @DataPoints public static String[] whoMadeIt = { "Miss Rose", "Mrs. Peacock", "Professor Plum" } ; @DataPoints public static String[] whereWasIt = { "Library", "Lounge" } ; @Theory public void dummyTest(String murderer, String murderScene) { System.out.println(String.format("%s murdered Colonel Mustard in the %s", murderer, murderScene)); } }
Ничего страшного, верно? Давайте запустим это, тогда …
Miss Rose murdered Colonel Mustard in the Miss Rose Miss Rose murdered Colonel Mustard in the Mrs. Peacock Miss Rose murdered Colonel Mustard in the Professor Plum Miss Rose murdered Colonel Mustard in the Library Miss Rose murdered Colonel Mustard in the Lounge Mrs. Peacock murdered Colonel Mustard in the Miss Rose Mrs. Peacock murdered Colonel Mustard in the Mrs. Peacock Mrs. Peacock murdered Colonel Mustard in the Professor Plum Mrs. Peacock murdered Colonel Mustard in the Library Mrs. Peacock murdered Colonel Mustard in the Lounge Professor Plum murdered Colonel Mustard in the Miss Rose Professor Plum murdered Colonel Mustard in the Mrs. Peacock Professor Plum murdered Colonel Mustard in the Professor Plum Professor Plum murdered Colonel Mustard in the Library Professor Plum murdered Colonel Mustard in the Lounge Library murdered Colonel Mustard in the Miss Rose Library murdered Colonel Mustard in the Mrs. Peacock Library murdered Colonel Mustard in the Professor Plum Library murdered Colonel Mustard in the Library Library murdered Colonel Mustard in the Lounge Lounge murdered Colonel Mustard in the Miss Rose Lounge murdered Colonel Mustard in the Mrs. Peacock Lounge murdered Colonel Mustard in the Professor Plum Lounge murdered Colonel Mustard in the Library Lounge murdered Colonel Mustard in the Lounge
Чего ждать?! Подозреваемый убил этого бедного полковника в другом подозреваемом ?! Библиотека кого-то убила? А? Звучит странно!
Фактически, бегун Теорий выбирает потенциальные назначения параметров в зависимости от их типа.
Здесь «murderer» и «murderScene» оба являются String … поэтому список возможных значений представляет собой объединение значений обоих точек данных.
Это может быть действительно громоздким в реальном проекте.
Хотя вы можете обойти это с помощью enums или пользовательских провайдеров, это довольно много …
Обходной путь: «названный» таможенный поставщик
Что если бы мы могли добавить «имя» в @Datapoints и использовать его в качестве ссылки для возможных значений определенного параметра?
К счастью, JUnit позволяет вам определять поставщиков пользовательских данных.
Поймите таможенного поставщика: фиктивный поставщик
Нам нужно определить новую аннотацию, которая определяет класс поставщика, который будет использоваться для параметра, аннотированного этой аннотацией
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @ParametersSuppliedBy(FooBarSupplier.class) public @interface FooBar{ }
Затем нам нужно определить класс поставщика.
public static class FooBarSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) { List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); list.add((PotentialAssignment.forValue("foo", "bar1")); list.add((PotentialAssignment.forValue("foo", "bar2")); list.add((PotentialAssignment.forValue("foo", "bar2")); return list; } }
Этот поставщик просто предоставит 3 жестко закодированных значения для демонстрации
Чтобы использовать этого поставщика:
@Theory public static void fooBarTheory(@FooBar String bar) { ... }
Этот метод будет вызван 3 раза, с «bar1», «bar2», «bar3»
Напишите на заказ поставщика
Давайте использовать эту функцию и добавим нашего собственного поставщика:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface DataPointsRef { String value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @ParametersSuppliedBy(WithDataPointsSupplier.class) public @interface WithDataPoints { Class<?> clazz(); String name(); } public static class WithDataPointsSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) { List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); WithDataPoints ref = (WithDataPoints) sig.getAnnotation(WithDataPoints.class); Field[] fields = ref.clazz().getFields(); for (Field field : fields) { DataPointsRef dpSupplier = field.getAnnotation(DataPointsRef.class); if (dpSupplier != null) { if (dpSupplier.value().equals(ref.name())) { Class<?> fieldType = field.getType(); if (!fieldType.isArray()) { throw new RuntimeException("Referenced Datapoint must be an array."); } try { Object values = field.get(null); for (int i = 0; i < Array.getLength(values); i++) { Object value = Array.get(values, i); list.add(PotentialAssignment.forValue(withTag.name(), value)); } } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } } return list; } }
Вы можете легко изменить это, чтобы загрузить бин из Spring Context, прочитать файл свойств, получить данные из базы данных, …
Используйте нестандартного поставщика
Давайте воспользуемся этим новым поставщиком и исправим наш тестовый пример:
@RunWith(Theories.class) public class GuessTheMurdererTest { @DataPointsRef("suspects") public static String[] whoMadeIt = { "Miss Rose", "Mrs. Peacock", "Professor Plum" }; @DataPointsRef("rooms") public static String[] whereWasIt = { "Library", "Lounge" }; @Theory public void dummyTest( @WithDataPoints(clazz=GuessTheMurdererTest.class, name="suspects") String murderer, @WithDataPoints(clazz=GuessTheMurdererTest.class, name="rooms") String murderScene) { System.out.println(String.format("%s murdered Colonel Mustard in the %s", murderer, murderScene)); } }
Our @Datapoints can now have a name, and the @Theory parameters get a new annotation referencing this name.
If we run this test, we get this output:
Miss Rose murdered Colonel Mustard in the Library Miss Rose murdered Colonel Mustard in the Lounge Mrs. Peacock murdered Colonel Mustard in the Library Mrs. Peacock murdered Colonel Mustard in the Lounge Professor Plum murdered Colonel Mustard in the Library Professor Plum murdered Colonel Mustard in the Lounge
Much better…
Let’s push it just a little more…
We just purchased the new edition of the game, new suspects are available…
@RunWith(Theories.class) public class GuessTheMurdererTest { @DataPointsRef("suspects") public static String[] whoMadeIt = { "Miss Rose", "Mrs. Peacock", "Professor Plum" } ; @DataPointsRef("suspects") public static String[] orMaybeItWasSomeoneElse = { "Mr. Slate-Grey", "Captain Brown" } ; @DataPointsRef("rooms") public static String[] whereWasIt = { "Hall", "Kitchen" } ; @Theory public void dummyTest( @WithDataPoints(clazz=GuessTheMurdererTest.class, name="suspects") String murderer, @WithDataPoints(clazz=GuessTheMurdererTest.class, name="rooms") String murderScene) { System.out.println(String.format("%s murdered Colonel Mustard in the %s", murderer, murderScene)); } }
Miss Rose murdered Colonel Mustard in the Hall Miss Rose murdered Colonel Mustard in the Kitchen Mrs. Peacock murdered Colonel Mustard in the Hall Mrs. Peacock murdered Colonel Mustard in the Kitchen Professor Plum murdered Colonel Mustard in the Hall Professor Plum murdered Colonel Mustard in the Kitchen Mr. Slate-Grey murdered Colonel Mustard in the Hall Mr. Slate-Grey murdered Colonel Mustard in the Kitchen Captain Brown murdered Colonel Mustard in the Hall Captain Brown murdered Colonel Mustard in the Kitchen
Good! Datapoints values are now aggregated by name rather than type.
Enhancing the custom supplier: introducing «Tags»
Suppose we buy yet another game’s extention, the murderer now has an accomplice.
Some suspects can only be «murderers», some can only be «accomplices», some can be both «murderer» or «accomplice»
We could easily imagine extending the Supplier to add multiple «tags» to a @Datapoint.
Let’s modify our annotations and supplier:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface TaggedDataPoints { //Multiple values allowed String[] value(); } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) @ParametersSuppliedBy(TaggedDataPointsSupplier.class) public @interface WithTags { Class<?> clazz(); String name(); } public static class TaggedDataPointsSupplier extends ParameterSupplier { @Override public List<PotentialAssignment> getValueSources(ParameterSignature sig) { List<PotentialAssignment> list = new ArrayList<PotentialAssignment>(); WithTags withTag = (WithTags) sig.getAnnotation(WithTags.class); Field[] fields = withTag.clazz().getFields(); for (Field field : fields) { TaggedDataPoints taggedDataPoints = field.getAnnotation(TaggedDataPoints.class); if (taggedDataPoints != null) { //browse tags, an select values if one matches for (String tag : taggedDataPoints.value()) { if (tag.equals(withTag.name())) { Class<?> fieldType = field.getType(); if (!fieldType.isArray()) { throw new RuntimeException("Referenced Datapoint must be an array."); } try { Object values = field.get(null); for (int i = 0; i < Array.getLength(values); i++) { Object value = Array.get(values, i); list.add(PotentialAssignment.forValue(withTag.name(), value)); } } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } } } } } return list; } }
We can now distinguish those new «suspects» and «accomplices», and add an accomplice to the @Theory:
@RunWith(Theories.class) public class GuessTheMurdererTest { @TaggedDataPoints("suspects") public static String[] suscpectsOnly = { "Miss Rose", "Mrs. Peacock", "Professor Plum" }; @TaggedDataPoints({ "suspects", "accomplices" }) public static String[] suspectsOrAccomplices = { "Mr. Slate-Grey" }; @TaggedDataPoints("accomplices") public static String[] accomplicesOnly = { "Captain Brown" }; @TaggedDataPoints("rooms") public static String[] whereWasIt = { "Hall", "Kitchen" }; @Theory public void dummyTest( @WithTags(clazz = GuessTheMurdererTest.class, name = "suspects") String murderer, @WithTags(clazz = GuessTheMurdererTest.class, name = "accomplices") String accomplice, @WithTags(clazz = GuessTheMurdererTest.class, name = "rooms") String murderScene) { // Ensure no-one is accompliced to himself... Assume.assumeTrue(!murderer.equalsIgnoreCase(accomplice)); System.out.println(String.format("%s and his/her accomplice %s both murdered Colonel Mustard in the %s", murderer, accomplice, murderScene)); // Assert that Captain brown cannot be the murderer Assert.assertFalse(murderer.equalsIgnoreCase("Captain Brown")); // Assert that Professor Plum cannot be an accomplice Assert.assertFalse(accomplice.equalsIgnoreCase("Professor Plum")); } }
If everything went fine, asserts are all ok, and you get:
Miss Rose and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Hall Miss Rose and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Kitchen Miss Rose and his/her accomplice Captain Brown both murdered Colonel Mustard in the Hall Miss Rose and his/her accomplice Captain Brown both murdered Colonel Mustard in the Kitchen Mrs. Peacock and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Hall Mrs. Peacock and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Kitchen Mrs. Peacock and his/her accomplice Captain Brown both murdered Colonel Mustard in the Hall Mrs. Peacock and his/her accomplice Captain Brown both murdered Colonel Mustard in the Kitchen Professor Plum and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Hall Professor Plum and his/her accomplice Mr. Slate-Grey both murdered Colonel Mustard in the Kitchen Professor Plum and his/her accomplice Captain Brown both murdered Colonel Mustard in the Hall Professor Plum and his/her accomplice Captain Brown both murdered Colonel Mustard in the Kitchen Mr. Slate-Grey and his/her accomplice Captain Brown both murdered Colonel Mustard in the Hall Mr. Slate-Grey and his/her accomplice Captain Brown both murdered Colonel Mustard in the Kitchen
Pretty nice isn’t it?
A few final words…
This could give you a neat way of dividing values of a same list according to a fonctional property.
Suppose we get a «Folder» list, some of them are «Input folders», some others are «Output folders», and a few are «Input/Output folders»
Of course, ideally, you would create different objects, but if the tested method just takes two String, that’s a lot of boilerplate…
Additionally, this could be used as an easy way to qualify the test dataitself, some information that does not belong to the «Domain», but belongs to the test itself.
eg: you might want to test some theories against only a subset of the @Datapoints.
Well, that’s it for today, I hope this article gave you some insights on how you can use @Theory, and how you can improve the data you feed to it using custom suppliers.