Утверждение свойств элементов списка с помощью assertJ

У меня есть рабочее утверждение Hamcrest:

assertThat(mylist, contains(
  containsString("15"), 
  containsString("217")));

Предполагаемое поведение:

  • mylist == asList("Abcd15", "217aB") => успех
  • myList == asList("Abcd15", "218") => сбой

Как мне перенести это выражение на assertJ. Конечно, существуют наивные решения, такие как утверждение первого и второго значения, например:

assertThat(mylist.get(0)).contains("15");
assertThat(mylist.get(1)).contains("217");

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

assertThat(mylist).elements()
  .next().contains("15")
  .next().contains("217")

Но прежде чем я напишу собственное утверждение, мне было бы интересно узнать, как другие решат эту проблему?

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

assertThat(mylist, contains(
  emptyString(),                                     //additional element
  allOf(containsString("08"), containsString("15")), //extended constraint
  containsString("217")));                           // unchanged

Для этого примера тесты, зависящие от индекса списка, должны быть перенумерованы. Тесты, использующие настраиваемое условие, должны будут переписать полное условие (обратите внимание, что ограничения в allOf не ограничиваются проверками подстроки).


person CoronA    schedule 31.12.2017    source источник
comment
Это ваш реальный вариант использования или вы бы предпочли проверить, например, что, скажем, в списке пользователей есть пользователи с именем Джон и Джек в возрасте от 25 до 45 лет?   -  person JB Nizet    schedule 31.12.2017
comment
Реальный вариант использования - это генератор исходного кода. Каждая запись в списке - это один сгенерированный фрагмент исходного кода. Исходный код не анализируется (только записывается на диск), но есть сценарии, в которых исходный код должен или не должен содержать определенные шаблоны. Решение для разложения объектов не решило бы проблему.   -  person CoronA    schedule 31.12.2017


Ответы (5)


Для такого рода утверждений Hamcrest превосходит AssertJ, вы можете имитировать Hamcrest с помощью условий, но вам нужно написать их, поскольку в AssertJ их нет из коробки (философия assertJ не должна конкурировать с Hamcrest в этом аспекте).

В следующей версии AssertJ (которая скоро будет выпущена!) Вы сможете повторно использовать Hamcrest Matcher для построения условий AssertJ, например:

Condition<String> containing123 = new HamcrestCondition<>(containsString("123"));

// assertions succeed
assertThat("abc123").is(containing123);
assertThat("def456").isNot(containing123);

В заключение, это предложение ...

assertThat(mylist).elements()
                  .next().contains("15")
                  .next().contains("217")

... к сожалению, не может работать из-за ограничения дженериков, хотя вы знаете, что у вас есть список строк, дженерики Java недостаточно мощны, чтобы выбрать определенный тип (StringAssert) в зависимости от другого (String), это означает, что вы можете только выполнять Object утверждение для элементов, но не String утверждение.

-- редактировать --

Начиная с версии 3.13.0 можно использовать asInstanceOf для получения утверждений определенного типа, это полезно, если объявленным типом является Object, но тип среды выполнения более конкретен.

Пример:

// Given a String declared as an Object
Object value = "Once upon a time in the west";

// With asInstanceOf, we switch to specific String assertion by specifying the InstanceOfAssertFactory for String
assertThat(value).asInstanceOf(InstanceOfAssertFactories.STRING)
                 .startsWith("Once");`

см. https://assertj.github.io/doc/#assertj-core-3.13.0-asInstanceOf

person Joel Costigliola    schedule 01.01.2018
comment
Мои текущие направления пытаются использовать satisfies вместо Condition, потому что Consumer<T> может утверждать более одного свойства, что обеспечивает указанную выше гибкость. - person CoronA; 01.01.2018
comment
Тем не менее, я подтверждаю ваше заявление об ограничении дженериков, но предлагаю разрешить нарушение этого ограничения. Есть ли причина, по которой assertThat(mylist).first().as(StringAssert.class).contains("15") еще не был реализован? Это не помогло бы моему сценарию (повторение списка), но было бы весьма полезно в других моих тестах. - person CoronA; 01.01.2018
comment
В настоящее время реализовано следующее: assertThat(mylist).first().asString().startsWith("prefix");, мы думали добавить другие asXxx методы, но это слишком загромождает API. В итоге мы получили этот синтаксис assertThat(list, StringAssert.class).first().startsWith("prefix");. Сказав, что я мог бы подумать о вашем предложении с as(Assert class), поскольку его легко обнаружить, один недостаток заключается в том, что as уже используется для описания утверждений. - person Joel Costigliola; 03.01.2018

Самое близкое, что я нашел, - это написать условие «ContainsSubstring» и статический метод для его создания и использовать

assertThat(list).has(containsSubstring("15", atIndex(0)))
                .has(containsSubstring("217", atIndex(1)));

Но, возможно, вам стоит просто написать цикл:

List<String> list = ...;
List<String> expectedSubstrings = Arrays.asList("15", "217");
for (int i = 0; i < list.size(); i++) {
    assertThat(list.get(i)).contains(expectedSubstrings.get(i));
}

Или написать параметризованный тест, чтобы каждый элемент проверялся на каждой подстроке самим JUnit.

person JB Nizet    schedule 31.12.2017
comment
Оптимальным было бы то, что добавление третьего элемента в первую позицию не подразумевает изменения других частей программы. К тому же наивная версия (также основанная на индексах) из моего сообщения более гибкая, потому что она допускает несколько ограничений для одного элемента списка. - person CoronA; 31.12.2017

Вы можете сделать следующее:

List<String> list1 = Arrays.asList("Abcd15", "217aB");
List<String> list2 = Arrays.asList("Abcd15", "218");

Comparator<String> containingSubstring = (o1, o2) -> o1.contains(o2) ? 0 : 1;
assertThat(list1).usingElementComparator(containingSubstring).contains("15", "217");  // passes
assertThat(list2).usingElementComparator(containingSubstring).contains("15", "217");  // fails

Ошибка, которую он дает:

java.lang.AssertionError: 
Expecting:
 <["Abcd15", "218"]>
to contain:
 <["15", "217"]>
but could not find:
 <["217"]>
person Lee Jonas    schedule 07.02.2020

Фактически, вы должны реализовать свой собственный Condition в assertj для проверки коллекции, содержащей подстроки по порядку. Например:

assertThat(items).has(containsExactly(
  stream(subItems).map(it -> containsSubstring(it)).toArray(Condition[]::new)
));

Какой подход я выбрал, чтобы удовлетворить ваши требования? напишите тестовый пример контракта, а затем реализуйте функцию, которую assertj не предоставляет, вот мой тестовый пример для hamcrest contains(containsString(...)), адаптированный к assertj containsExactly, как показано ниже:

import org.assertj.core.api.Assertions;
import org.assertj.core.api.Condition;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import java.util.Collection;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertThat;

@RunWith(Parameterized.class)
public class MatchersTest {
    private final SubstringExpectation expectation;

    public MatchersTest(SubstringExpectation expectation) {
        this.expectation = expectation;
    }

    @Parameters
    public static List<SubstringExpectation> parameters() {
        return asList(MatchersTest::hamcrest, MatchersTest::assertj);
    }

    private static void assertj(Collection<? extends String> items, String... subItems) {
        Assertions.assertThat(items).has(containsExactly(stream(subItems).map(it -> containsSubstring(it)).toArray(Condition[]::new)));
    }

    private static Condition<String> containsSubstring(String substring) {
        return new Condition<>(s -> s.contains(substring), "contains substring: \"%s\"", substring);
    }

    @SuppressWarnings("unchecked")
    private static <C extends Condition<? super T>, T extends Iterable<? extends E>, E> C containsExactly(Condition<E>... conditions) {
        return (C) new Condition<T>("contains exactly:" + stream(conditions).map(it -> it.toString()).collect(toList())) {
            @Override
            public boolean matches(T items) {
                int size = 0;
                for (E item : items) {
                    if (!matches(item, size++)) return false;
                }
                return size == conditions.length;
            }

            private boolean matches(E item, int i) {
                return i < conditions.length && conditions[i].matches(item);
            }
        };
    }

    private static void hamcrest(Collection<? extends String> items, String... subItems) {
        assertThat(items, contains(stream(subItems).map(Matchers::containsString).collect(toList())));
    }

    @Test
    public void matchAll() {
        expectation.checking(asList("foo", "bar"), "foo", "bar");
    }


    @Test
    public void matchAllContainingSubSequence() {
        expectation.checking(asList("foo", "bar"), "fo", "ba");
    }

    @Test
    public void matchPartlyContainingSubSequence() {
        try {
            expectation.checking(asList("foo", "bar"), "fo");
            fail();
        } catch (AssertionError expected) {
            assertThat(expected.getMessage(), containsString("\"bar\""));
        }
    }

    @Test
    public void matchAgainstWithManySubstrings() {
        try {
            expectation.checking(asList("foo", "bar"), "fo", "ba", "<many>");
            fail();
        } catch (AssertionError expected) {
            assertThat(expected.getMessage(), containsString("<many>"));
        }
    }

    private void fail() {
        throw new IllegalStateException("should failed");
    }

    interface SubstringExpectation {
        void checking(Collection<? extends String> items, String... subItems);
    }
}

Однако вы предпочитаете использовать связанные Conditions, а не assertj fluent api, поэтому я предлагаю вам попробовать вместо этого использовать hamcrest. другими словами, если вы используете этот стиль в assertj, вы должны написать много Conditions или адаптировать hamcrest Matchers к assertj Condition.

person holi-java    schedule 31.12.2017
comment
Этот подход подходит для простых проверок подстроки (как в моем примере). Но, скорее всего, тест станет ненадежным, если будут добавлены дополнительные проверки. Я обновлю свой вопрос, чтобы указать на критерии надежности. - person CoronA; 31.12.2017
comment
Я буду ждать дополнительных ответов, но действительно, в этом сценарии hamcrest кажется более гибким. - person CoronA; 31.12.2017
comment
@CoronA: да, если вы попытаетесь реализовать такую ​​возможность в assertj, вы действительно должны написать много Conditions или YourAsserts. кроме того, ваш assertj design api elements().next()... не полностью выполняет hamcrest contains(..). так как hamcrest contains будет соответствовать обоим элементам по порядку, а 2 коллекции имеют одинаковый размер. - person holi-java; 31.12.2017

Вы можете использовать anyMatch

assertThat(mylist)
  .anyMatch(item -> item.contains("15")
  .anyMatch(item -> item.contains("217")

но, к сожалению, сообщение об ошибке не может рассказать вам об ожиданиях.

Expecting any elements of:
  <["Abcd15", "218"]>
to match given predicate but none did.
person Stefan Birkner    schedule 25.08.2020