javahamcrestgoogle-truth

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


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?


Solution

  • 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!