Mockito Mock returns the same value despite different arguments

advertisements
package pl.mielecmichal.news.services.news;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import pl.mielecmichal.news.entities.news.News;
import pl.mielecmichal.news.entities.newssources.NewsSource;
import pl.mielecmichal.news.repositories.news.NewsRepository;

import static java.util.Arrays.asList;
import static org.mockito.Mockito.*;

import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class NewsServiceTest {

    NewsService newsService;

    NewsRepository newsRepository;

    private static final String FIRST_AUTHOR = "[email protected]";
    private static final String FIRST_TITLE = "First Title";
    private static final String FIRST_CONTENT = "First Content Content Content";
    private static final String FIRST_URL = "http://localhost/first";

    private static final String SECOND_AUTHOR = "[email protected]";
    private static final String SECOND_TITLE = "Second";
    private static final String SECOND_CONTENT = "Second Content";

    private static final String THIRD_AUTHOR = "[email protected]";
    private static final String THIRD_TITLE = "Third Title";
    private static final String THIRD_CONTENT = "Third Content";

    private final News firstNews = firstCorrectNews();
    private final News secondNews = secondCorrectNews();
    private final News thirdNews = thirdCorrectNews();
    private final NewsSource source = correctSource();

    public NewsServiceTest() throws MalformedURLException {

    }

    @Before
    public void setUp() throws MalformedURLException {
        newsRepository = mock(NewsRepository.class);
        newsService = new NewsService(newsRepository);
    }

    @Test
    public void saveNewNewses_savedNewsesGivenAgain_shouldSaveOnlyNew() {
        // given
        List<News> newses = new ArrayList<>();
        newses.add(firstNews);
        newses.add(secondNews);

        when(newsRepository.countByNewsSourceAndAuthorAndTitle(source, FIRST_AUTHOR, FIRST_TITLE)).thenReturn(0L);
        when(newsRepository.countByNewsSourceAndAuthorAndTitle(source, SECOND_AUTHOR, SECOND_TITLE)).thenReturn(1L);

        // when
        newsService.saveNewNewses(newses);

        // then
        verify(newsRepository, times(1)).save(asList(firstNews));
        verify(newsRepository, never()).save(newses);
    }

    private News firstCorrectNews() {
        News news = new News();
        news.setAuthor(FIRST_AUTHOR);
        news.setTitle(FIRST_TITLE);
        news.setContent(FIRST_CONTENT);
        news.setNewsSource(source);
        return news;
    }

    private News secondCorrectNews() {
        News news = new News();
        news.setAuthor(SECOND_AUTHOR);
        news.setTitle(SECOND_TITLE);
        news.setContent(SECOND_CONTENT);
        news.setNewsSource(source);
        return news;
    }

    private News thirdCorrectNews() {
        News news = new News();
        news.setAuthor(THIRD_AUTHOR);
        news.setTitle(THIRD_TITLE);
        news.setContent(THIRD_CONTENT);
        news.setNewsSource(source);
        return news;
    }

    private NewsSource correctSource() throws MalformedURLException {
        NewsSource source = new NewsSource();
        source.setUrl(new URL(FIRST_URL));
        source.setUpdateTime(LocalDateTime.now());
        return source;
    }

}

I checked under debugger, and countBy method always returns O, but arguments are different and correct in my SUT. It looks like Mockito doesn't distinguish a method arguments. Cheers!

I add full source code to show that constants are correct.


While the issue is chiefly the ordering of your fields, there are some things you can do to both reduce the likelihood of this error occurring again, and clean up your test quite a bit.

First and foremost, your three methods - firstCorrectNews, secondCorrectNews, and thirdCorrectNews - all do the exact same thing with slightly different parameters. It makes more sense to unify their purpose.

private News correctNews(final String author, final String title, final String content, final NewsSource source) {
    final News news = new News();
    news.setAuthor(author);
    news.setTitle(title);
    news.setContent(content);
    news.setNewsSource(source);
    return news;
}

If you use this method to bootstrap your test news objects, you are forced to pass in a source every time, so that you won't get caught up in anything that's dependent on the state of your test object overall.

While we're here, it's worth modifying the correctSource method too, so that we pass in the URL instead of assuming state once more.

private NewsSource correctSource(final String url) throws MalformedURLException {
    final NewsSource source = new NewsSource();
    source.setUrl(new URL(url));
    source.setUpdateTime(LocalDateTime.now());
    return source;
}

Next, we can leverage Mockito's runner class so that we don't have to new up the mocks in a @Before clause. This keeps the code a lot smaller and makes the expectations of what the mocked classes and the test class are.

@RunWith(MockitoJUnitRunner.class)
public class NewsServiceTest {

    @Mock
    NewsRepository newsRepository;

    @InjectMocks
    NewsService newsService;

    // other code to follow

}

Now, let's put it all together. This should be the exact same thing that you're testing, with the main differences being:

  • There's a lot less repetition in your code, specifically when bootstrapping expected data
  • Your mocks are clearly defined
  • All test data that you genuinely care about is in your specific test, which helps when you want to write more tests
  • Your test data is also isolated in scope, which allows you to run this test in parallel
@RunWith(MockitoJUnitRunner.class)
public class NewsServiceTest {

    @Mock
    NewsRepository newsRepository;

    @InjectMocks
    NewsService newsService;

    @Test
    public void saveNewNewses_savedNewsesGivenAgain_shouldSaveOnlyNew() {
        // given
        final String FIRST_AUTHOR = "[email protected]";
        final String FIRST_TITLE = "First Title";
        final String FIRST_CONTENT = "First Content Content Content";
        final String URL = "http://localhost/first";
        final String SECOND_AUTHOR = "[email protected]";
        final String SECOND_TITLE = "Second";
        final String SECOND_CONTENT = "Second Content";

        final List<News> newses = new ArrayList<>();

        final NewsSource newsSource = correctSource(URL);
        final News firstNews = correctNews(FIRST_AUTHOR, FIRST_TITLE, FIRST_CONTENT, newsSource);
        final News secondNews = correctNews(SECOND_AUTHOR, SECOND_TITLE, SECOND_CONTENT, newsSource);

        newses.add(firstNews);
        newses.add(secondNews);

        // when

        when(newsRepository.countByNewsSourceAndAuthorAndTitle(newsSource, FIRST_AUTHOR, FIRST_TITLE)).thenReturn(0L);
        when(newsRepository.countByNewsSourceAndAuthorAndTitle(newsSource, SECOND_AUTHOR, SECOND_TITLE)).thenReturn(1L);
        newsService.saveNewNewses(newses);

        // then
        verify(newsRepository).save(asList(firstNews));
        verify(newsRepository, never()).save(newses);
    }

    private News correctNews(final String author, final String title, final String content, final NewsSource source) {
        final News news = new News();
        news.setAuthor(author);
        news.setTitle(title);
        news.setContent(content);
        news.setNewsSource(source);
        return news;
    }

    private NewsSource correctSource(final String url) throws MalformedURLException {
        NewsSource source = new NewsSource();
        source.setUrl(new URL(url));
        source.setUpdateTime(LocalDateTime.now());
        return source;
    }
}