Mocking a sorted list by a Comparator (EasyMock)

2.2k views Asked by At

How to write unit tests for a sorting function (that use Comparator), when the sorted object is a mock object?

Just a little example: I have a Libary with movies. Three classes: Library, Movie, myCmpMovies(implements Comparator). I am testing the class Library levaing it isolated. For this target, I am mocking the class Movie, to test all methods from Library.

Using EasyMock.

I am sorting a list of movies in this way:

class Library{
    ArrayList<Movie> movies;
    public List<Movie> getSortedMoviesByDatesAndNames() {
        List<Movie> movies = this.getMovies();
        Comparator myCmpMovies = new myCmpMovies();
        Collections.sort(movies, myCmpMovies);
        return movies;
    }
}

Now, I am testing this method with a unit test. The mock object is Movie. I am just trying something like:

    @Test
    public List<Movie> TestGetSortedMoviesByDatesAndNames() {
        movie1= createMock(Movie.class);
        movie2= createMock(Movie.class);
        movie3= createMock(Movie.class);

        ....
    }

I know there are a few questions about this topic, but I think those are not the same problem.

Thanks in advance.

2

There are 2 answers

4
GhostCat On BEST ANSWER

Some edits on your comment: if you really have to stick with that "clumsy" Movie class, and you want to go for mocking, i would do it somehow like this:

Movie mockMovieWith(String title, Date releaseDate, ...) {
  Movie mockedMovie = createMock(Movie.class);
  expect(mockedMovie.getTitle()).andStubReturn(title);
  expect(mockedMovie.getReleaseDate())...
  replay(mockedMovie);
  return mockedMovie;
}

In other words: use a helper method to create a mocked Movie object that has exactly those properties that you think should/would be used by your "real" code that will be later using the mocked objects.

But as written below; you really should add some "anti corruption layer" here. Meaning: when that "external" library Movie class is so hard to use, then wrap something around - so that your code isn't corrupted by the bad design in that Movie class!

... and for the record, here is the initial answer.

You are getting this wrong on many levels. First of all, you are shadowing your field movies with a local variable in your method. That is rarely a good idea, as it can lead to all kinds of subtle bugs.

Then: the first thing to test isn't that method; it is the comparator itself. Meaning: you are relying on Collections.sort(); there is no need to test that part. You want to make sure that your comparator does what it should do.

And in that sense, your first test case should simply define two Movie objects; and then you call the compareTo method of your comparator and check for the expected result.

Leading to the real topic of your question: you should not need to mock Movie objects. Those things represent some kind of data. In that sense: you dont instantiate mocked data, you just create real Movie objects with known content.

For example, assuming that the core property of a Movie is its title, you go like:

Movie movieA = new Movie("Title A");
Movie movieB = new Movie("Title B");

And that is what you would feed into your comparator. Or maybe into your movie list (you still want to do at least one "integration" test that makes sure that getSortedMovies...() does what it is supposed to do).

If you think you need to mock Movie objects, than you have a bad design. It is that simple. If it is not "that" easy as shown above to instantiate a Movie object, than you should enable exactly that.

Because otherwise, you would have to do something like:

Movie mockedMovieA = createMock(Movie.class);

to then provide all the properties as mock expects, like:

expect(mockedMovieA.getTitle()).andReturn("Title A");

That would be extremely cumbersome and errorprone.

So, as said, the core thing you should look into: create a design that allows you to work with "real" Movie objects in your unit tests. And if your Movie class contains so many things that this is not possible ... than that is a clear indication, that your Movie class is way too big and doing things that dont belong into it.

The first way to resolve such "too big" problems: you could separate a "Movie" interface that contains the core methods of your current Movie class, such as getTitle(), getReleaseDate(), whatever. And then all your code only works with that interface ... which could be easily mocked; or implemented by a more "stupid" test-only class, too.

1
Timothy Truckle On

How to write unit tests for a sorting function (that use Comparator), when the sorted object is a mock object?

With UnitTest you test the observable behavior.

That means that you check that the objects in the returned collection have the correct order regardless of how the method achieves that.

    @Test
    public void TestGetSortedMoviesByDates() {
        movie1= createMock(Movie.class);
        movie2= createMock(Movie.class);
        movie3= createMock(Movie.class);

        expect(movie1.getName()).andReturn("any name");
        expect(movie2.getName()).andReturn("any name");
        expect(movie3.getName()).andReturn("any name");

        Date date = new Date(); // curent Date
        expect(movie2.getDate()).andReturn(date.clone())
        date.setYear(date.getYear()+1);
        expect(movie3.getDate()).andReturn(date.clone())
        date.setYear(date.getYear()+1);
        expect(movie1.getDate()).andReturn(date.clone())

        List<Movie> movies = Arrays.asList(movie1,movie2,movie3);
        Library library = new Library();
        // put the list into the library
        List<Movie> sortedMovies = library.getSortedMoviesByDatesAndNames();

        assertArrayEquals(sortedMovies, Arrays.asList(movie2,movie3,movie1));
    }

    @Test
    public void TestGetSortedMoviesByNamesForEqualDates() {
        movie1= createMock(Movie.class);
        movie2= createMock(Movie.class);
        movie3= createMock(Movie.class);

        expect(movie1.getName()).andReturn("ZZ Last after sort");
        expect(movie2.getName()).andReturn("AA first after sort");
        expect(movie3.getName()).andReturn("MM middle after sort");

        Date date = new Date(); // curent Date
        expect(movie2.getDate()).andReturn(date)
        expect(movie3.getDate()).andReturn(date)
        expect(movie1.getDate()).andReturn(date)

        List<Movie> movies = Arrays.asList(movie1,movie2,movie3);
        Library library = new Library();
        // put the list into the library
        List<Movie> sortedMovies = library.getSortedMoviesByDatesAndNames();

        assertArrayEquals(sortedMovies, Arrays.asList(movie2,movie3,movie1));
   }