Mockito; verify method was called with list, ignore order of elements in list

22,995

Solution 1

As noted in another answer, if you don't care about the order, you might do best to change the interface so it doesn't care about the order.

If order matters in the code but not in a specific test, you can use the ArgumentCaptor as you did. It clutters the code a bit.

If this is something you might do in multiple tests, you might do better to use appropriate Mockito Matchers or Hamcrest Matchers, or roll your own (if you don't find one that fills the need). A hamcrest matcher might be best as it can be used in other contexts besides mockito.

For this example you could create a hamcrest matcher as follows:

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class MyMatchers {
    public  static <T> Matcher<List<T>> sameAsSet(final List<T> expectedList) {
        return new BaseMatcher<List<T>>(){
            @Override
            public boolean matches(Object o) {
                List<T> actualList = Collections.EMPTY_LIST;
                try {
                    actualList = (List<T>) o;
                }
                catch (ClassCastException e) {
                    return false;
                }
                Set<T> expectedSet = new HashSet<T>(expectedList);
                Set<T> actualSet = new HashSet<T>(actualList);
                return actualSet.equals(expectedSet);
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("should contain all and only elements of ").appendValue(expectedList);
            }
        };
    }
}

And then the verify code becomes:

verify(mockClassB).sendEvent(argThat(MyMatchers.sameAsSet(expectedFileList)));

If you instead created a mockito matcher, you wouldn't need the argThat, which basically wraps a hamcrest matcher in a mockito matcher.

This moves the logic of sorting or converting to set out of your test and makes it reusable.

Solution 2

An ArgumentCaptor probably is the best way to do what you want.

However, it seems that you don’t actually care about the order of the files in the List. Therefore, have you considered changing ClassB so that it takes an unordered collection (like a Set) instead?

Solution 3

A one-liner using argThat which compares the two lists as sets:

verify(mock).method(argThat(list -> new HashSet<>(expected).equals(new HashSet<>(list))));

Solution 4

You can use an ArgumentCaptor and then Hamcrest's Matchers.containsInAnyOrder() for assertion like this:

ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
verify(mockClassB).method(argument.capture());
List<String> value = argument.getValue();
assertThat(value, containsInAnyOrder("expected", "values");
Share:
22,995

Related videos on Youtube

Dace
Author by

Dace

Java developer for 12 years

Updated on March 24, 2022

Comments

  • Dace
    Dace about 2 years

    I have a class (ClassA) that get the files in a directory. It scans the given directory for files matching a regex. For each matching file, it adds a File Object to a list. Once the directory is processed, it passes the List of Files to another Class (ClassB) for processing

    I am writing unit tests for ClassA, so am mocking ClassB using Mockito, and injecting it into ClassA. I then want to verify in different scenarios the contents of the list that is passed to ClassB (ie my mock)

    I've stripped back the code to the following

    public class ClassA implements Runnable {
    
        private final ClassB classB;
    
        public ClassA(final ClassB classB) {
            this.classB = classB;
        }
    
        public List<File> getFilesFromDirectories() {
            final List<File> newFileList = new ArrayList<File>();
            //        ...
            return newFileList;
        }
    
        public void run() {
            final List<File> fileList = getFilesFromDirectories();
    
            if (fileList.isEmpty()) {
                //Log Message
            } else {
                classB.sendEvent(fileList);
            }
        }
    }
    

    The test class looks like this

        @RunWith(MockitoJUnitRunner.class)
        public class AppTest {
    
        @Rule
        public TemporaryFolder folder = new TemporaryFolder();
    
        @Mock
        private ClassB mockClassB;
    
        private File testFileOne;
    
        private File testFileTwo;
    
        private File testFileThree;
    
        @Before
        public void setup() throws IOException {
            testFileOne = folder.newFile("testFileA.txt");
            testFileTwo = folder.newFile("testFileB.txt");
            testFileThree = folder.newFile("testFileC.txt");
        }
    
        @Test
        public void run_secondFileCollectorRun_shouldNotProcessSameFilesAgainBecauseofDotLastFile() throws Exception {
            final ClassA objUndertest = new ClassA(mockClassB);
    
            final List<File> expectedFileList = createSortedExpectedFileList(testFileOne, testFileTwo, testFileThree);
            objUndertest.run();
    
            verify(mockClassB).sendEvent(expectedFileList);
        }
    
        private List<File> createSortedExpectedFileList(final File... files) {
            final List<File> expectedFileList = new ArrayList<File>();
            for (final File file : files) {
                expectedFileList.add(file);
            }
            Collections.sort(expectedFileList);
            return expectedFileList;
        }
    }
    

    The problem is that this test works perfectly fine on windows, but fails on Linux. The reason being that on windows, the order that ClassA list the files matches the expectedList, so the line

    verify(mockClassB).sendEvent(expectedFileList);
    

    is causing the problem expecetdFileList = {FileA, FileB, FileC} on Windows, whereas on Linux it will be {FileC, FileB, FileA}, so the verify fails.

    The question is, how do I get around this in Mockito. Is there any way of saying, I expect this method to be be called with this parameter, but I don't care about the order of the contents of the list.

    I do have a solution, I just don't like it, I would rather have a cleaner, easier to read solution.

    I can use an ArgumentCaptor to get the actual value passed into the mock, then can sort it, and compare it to my expected values.

        final ArgumentCaptor<List> argument = ArgumentCaptor.forClass(List.class);
        verify(mockClassB).method(argument.capture());
        Collections.sort(expected);
        final List<String> value = argument.getValue();
        Collections.sort(value);
        assertEquals(expecetdFileList, value);
    
  • Dace
    Dace over 9 years
    I never thought of that. Will have to see what effect that has on the rest of the System, but off the top of my head, it might be possible. Thanks
  • Dace
    Dace over 9 years
    Going to accept this as the answer, as it addresses the question I asked, but I think I will actually go with the suggestion by Alex and will change to using a Set instead of a List
  • Duncan Jones
    Duncan Jones about 9 years
    @Dace A better choice of unordered collection is perhaps Collection, unless you are happy to accept the other constraints that come with Set (like no duplicate elements).
  • Benjamin Peter
    Benjamin Peter about 8 years
    Thanks for the code. Created a Tests in case someone cares. gist.github.com/dedeibel/1284459e46f23e85b5ee
  • witek
    witek about 2 years
    new HashSet<>(list) instead of new HashSet<>(actual)