How do I assert that a List contains exactly one instance of a particular class?

14,400

Solution 1

You're trying to write a test to see if a List contains exactly one instance of a particular class using Hamcrest and Truth. Instead, you should be writing this test with either Hamcrest or Truth. Hamcrest and Truth are both libraries for making tests more expressive, each with their own particular usage, style, and syntax. You can use them alongside each other in your tests if you like, but chaining their methods together as you are doing is not going to work. (Maybe you got confused because both libraries can have assertions that start with assertThat?) So for this particular test, you need to pick one of them and go with it.

Both libraries, however, are lacking the built-in functionality of checking that a List has one and only one item that satisfies a condition. So with either library, you have two options: either you can do a little bit of pre-processing on the list so that you can use a built-in assertion, or you can extend the language of the library to give it this functionality.

The following is an example class that demonstrates both options for both libraries:

import com.google.common.collect.FluentIterable;
import com.google.common.truth.*;
import org.hamcrest.*;
import org.junit.Test;

import java.util.*;

import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assert_;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class ExactlyOneInstanceTest {
    List<Object> myList = Arrays.asList("", 3, 'A', new Object());

    @Test
    public void hamcrestBuiltInTestExactlyOneInstance() {
        long theNumberOfStringsInMyList = myList.stream().filter(o -> o instanceof String).count();
        assertThat(theNumberOfStringsInMyList, equalTo(1L));
    }

    @Test
    public void hamcrestExtendedTestExactlyOneInstance() {
        assertThat(myList, HasExactlyOne.itemThat(is(instanceOf(String.class))));
    }

    @Test
    public void truthBuiltInTestExactlyOneInstance() {
        long theNumberOfStringsInMyList = myList.stream().filter(o -> o instanceof String).count();
        // can't static import Truth.assertThat because of name clash,
        // but we can use this alternative form
        assert_().that(theNumberOfStringsInMyList).isEqualTo(1);
    }

    @Test
    public void truthExtendedTestExactlyOneInstance() {
        assertAbout(iterable()).that(myList).containsExactlyOneInstanceOf(String.class);
    }


    // Hamcrest custom matcher
    static class HasExactlyOne<T> extends TypeSafeDiagnosingMatcher<Iterable<? super T>> {
        Matcher<? super T> elementMatcher;

        HasExactlyOne(Matcher<? super T> elementMatcher) {
            this.elementMatcher = elementMatcher;
        }

        @Factory
        public static <T> Matcher<Iterable<? super T>> itemThat(Matcher<? super T> itemMatcher) {
            return new HasExactlyOne<>(itemMatcher);
        }

        @Override
        public void describeTo(Description description) {
            description
                .appendText("a collection containing exactly one item that ")
                .appendDescriptionOf(elementMatcher);
        }

        @Override
        protected boolean matchesSafely(Iterable<? super T> item, Description mismatchDescription) {
            return FluentIterable.from(item).filter(o -> elementMatcher.matches(o)).size() == 1;
        }
    }

    // Truth custom extension
    static <T> SubjectFactory<ExtendedIterableSubject<T>, Iterable<T>> iterable() {
        return new SubjectFactory<ExtendedIterableSubject<T>, Iterable<T>>() {
            @Override
            public ExtendedIterableSubject<T> getSubject(FailureStrategy fs, Iterable<T> target) {
                return new ExtendedIterableSubject<>(fs, target);
            }
        };
    }

    static class ExtendedIterableSubject<T> extends IterableSubject<ExtendedIterableSubject<T>, T, Iterable<T>> {
        ExtendedIterableSubject(FailureStrategy failureStrategy, Iterable<T> list) {
            super(failureStrategy, list);
        }

        void containsExactlyOneInstanceOf(Class<?> clazz) {
            if (FluentIterable.from(getSubject()).filter(clazz).size() != 1) {
                fail("contains exactly one instance of", clazz.getName());
            }
        }
    }
}

Try running and looking over that class and use whichever way seems most natural for you. When writing future tests, just try sticking with the built-in assertions available to you and try to make the intent of the @Test methods and their assertions instantly readable. If you see that you are writing the same code multiple times, or that a test method is not so simple to read, then refactor and/or extend the language of the library you're using. Repeat until everything is tested and all tests are easily understandable. Enjoy!

Solution 2

First, note that IterableSubject.containsExactly() asserts that the input "contains exactly the provided objects or fails." This means - even if you could pass Matcher objects here - that we're asserting the list contains exactly one ExpectedType instance. Neither of the existing answers correctly enforce that invariant (instead heenenee's method asserts one instance of ExpectedType and any number of other instances, and your solution asserts that the list contains any number of instances of ExpectedType). As I read your question you do intend to assert the exactly-one property, but regardless this demonstrates a problem with the accepted solution - it can accidentally lead to assertions you didn't intend to make.


When I run into limitations of the Truth API like this, the first thing I always try is simply splitting the assertion up into separate steps. This often proves to be easy to write, easy to read, and generally error-proof. Understandably, people often try to look for elegant one-liners with Truth, but generally speaking there's nothing wrong with making sequential assertions.

It's hard to beat that strategy here:

assertThat(ls).hasSize(1);
assertThat(Iterables.getOnlyElement(ls)).isInstanceOf(String.class);

If the iterable isn't of size 1 we'll get an error telling us that (along with the contents of the iterable). If it is, we assert that the only element is an instance of String. Done!


For the general case of n instances the code does admittedly get a little messier, but it's still reasonable. We just use assertWithMessage() to include additional context about the list in the isInstanceOf() assertions:

assertThat(ls).hasSize(n);
for (int i = 0; i < ls.size(); i++) {
  assertWithMessage("list: %s - index: %s", ls, i)
      .that(ls.get(i)).isInstanceOf(String.class);
}

This is both more readable and much more clearly correct than implementing your own custom Subject.


As of Truth 0.29 you can do better using "Fuzzy Truth" AKA Correspondence. This allows you to essentially describe some transformation of the collection, and then assert on the result of that transformation. In this case we'll create an INSTANCEOF_CORRESPONDENCE:

private static final Correspondence<Object, Class<?>> INSTANCEOF_CORRESPONDENCE =
    new Correspondence<Object, Class<?>>() {
      @Override
      public boolean compare(@Nullable Object actual, @Nullable Class<?> expected) {
        return expected.isInstance(actual);
      }

      @Override
      public String toString() {
        return "is instanceof";
      }
    };

Now you can write a nice one-liner!

assertThat(ls).comparingElementsUsing(INSTANCEOF_CORRESPONDENCE)
    .containsExactly(String.class);

The big benefit of this approach over custom subjects is it's much more extensible - if you decide to make a different assertion the Correspondence implementation doesn't need to change, just your assertion:

assertThat(ls).comparingElementsUsing(INSTANCEOF_CORRESPONDENCE)
    .doesNotContain(Integer.class);

There are also tentative plans to support method references and lambdas with .comparingElementsUsing() so that you'll be able to write something like:

assertThat(ls).comparingElementsUsing((o, c) -> c.isInstance(o), "is instanceof")
    .containsExactly(String.class);

Solution 3

A simpler workaround is

for (Object elt : myList) {
    assertThat(elt).isInstanceOf(ExpectedType.class);
}

heenenee's answer is more elegant:

assertThat(iterable()).that(myList).containsInstancesOf(ExpectedType.class);
Share:
14,400

Related videos on Youtube

rds
Author by

rds

Updated on June 15, 2022

Comments

  • rds
    rds almost 2 years

    I'd like to test that a list contains instances of an object.

    For instance, with a single instance:

    assertThat(mylist).containsExactly(Matchers.any(ExpectedType.class));
    

    The array returned from tested obj does contain exactly one object of instance ExpectedType. However my test fails with:

    java.lang.AssertionError: Not true that <[ExpectedType@7c781c42]> contains exactly <[an instance of ExpectedType]>. It is missing <[an instance of ExpectedType]> and has unexpected items <[ExpectedType@7c781c42]>

    How can I write this test?

  • rds
    rds about 8 years
    The problem is really that the Truth contains verb on IterableSubject checks the equality of elements. And a Matcher is not an ExpectedType. Thanks for your answer (finally accepted).
  • Ar5hv1r
    Ar5hv1r almost 7 years
    Note that your containsExactlyOneInstanceOf() isn't the same behavior as IterableSubject.containsExactly() - Truth's method requires exactly the given objects and nothing else.