Mocked EhCache NullPointerException in JUnit 5 test - java

I am writing Unit tests for a service I want to test. Several methods try to retrieve values from an EhCache.
I tried mocking them with Mockito and simply have the get(String key) method of Cache return null, since I want to ignore the caching for these tests.
My test class:
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.jysk.dbl.esldataservice.model.Preis;
import com.jysk.dbl.esldataservice.service.PreisService;
import com.jysk.dbl.esldataservice.service.external.PimDataService;
import com.jysk.dbl.esldataservice.service.external.SapCarService;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
public class PreisServiceTest {
#Mock
private SapCarService sapCarService;
#Mock
private ArticleDataService articleDataService;
#Mock
private CacheManager cacheManager;
#Mock
private Cache cache;
#InjectMocks
#Resource
private PreisService preisService;
#BeforeEach
public void setup() {
MockitoAnnotations.initMocks(this);
when(this.cacheManager.getCache(anyString())).thenReturn(this.cache);
when(this.cache.get(anyString())).then(null);
}
protected static final String TEST_STORE_IDENTIFIER = "1234";
private static final String ARTICLE_IDENTIFIER_1 = "12345001";
private static final String ARTICLE_IDENTIFIER_2 = "54321001";
private final Preis p1 = new Preis(ARTICLE_IDENTIFIER_1, 10.00, 15.00, "01", "01", "01");
private final Preis p2 = new Preis(ARTICLE_IDENTIFIER_2, 20.00, 25.00, "02", "02", "02");
#Test
void testGetPreisReturnsOneCorrectPreis() {
when(this.sapCarService.getPreise(Arrays.asList(ARTICLE_IDENTIFIER_1), TEST_STORE_IDENTIFIER, true)).thenReturn(Arrays.asList(this.p1));
final List<Preis> actual = this.preisService.getPreis(ARTICLE_IDENTIFIER_1, TEST_STORE_IDENTIFIER);
verify(this.sapCarService, times(1)).getPreise(anyList(), anyString(), anyBoolean());
assertNotNull(actual);
assertEquals(1, actual.size());
assertEquals(this.p1, actual);
}
}
My implementation:
private Preis searchPreisInCache(String key) {
final Element preisOptional = this.cacheManager.getCache("preis").get(key); // NPE here
if (preisOptional != null) {
final Preis preis = (Preis) preisOptional.getObjectValue();
logger.info(String.format("Preis with key '%s' found in cache 'preis'.", key));
return preis;
}
return null;
}
The stackTrace showed, that the NPE gets thrown inside the net.sf.ehcache.Cache class:
public final Element get(Object key) throws IllegalStateException, CacheException {
getObserver.begin(); // NPE thrown here
checkStatus();
if (disabled) {
getObserver.end(GetOutcome.MISS_NOT_FOUND);
return null;
}
Element element = compoundStore.get(key);
if (element == null) {
getObserver.end(GetOutcome.MISS_NOT_FOUND);
return null;
} else if (isExpired(element)) {
tryRemoveImmediately(key, true);
getObserver.end(GetOutcome.MISS_EXPIRED);
return null;
} else if (!skipUpdateAccessStatistics(element)) {
element.updateAccessStatistics();
}
getObserver.end(GetOutcome.HIT);
return element;
}
Is there any easy solution for this problem, if I simply want the Cache to return null, whenever it's called?

Mockito can't mock final methods and classes without some configuration. As Morfic pointed out, it is posible with Mockito v2.x, like explained here and here.
Basically, you have to add a text file named org.mockito.plugins.MockMaker under the directory src/test/resources/mockito-extensions with the content mock-maker-inline and tada, Mockito can mock final methods and classes.
However, this uses a different engine with different limitations, so be aware of that.

Related

BaggageField updateValue method returns false in jUnit tests

EDIT: One problem was that Tracing.current() was null. I fixed this with the new #BeforeEach instead of the old #BeforeTestMethod:
Tracer tracer;
#BeforeEach
void init() {
tracer = Tracing.newBuilder().build().tracer();
TraceContext ctx = TraceContext.newBuilder().traceId(17).spanId(17).build();
Span span = tracer.toSpan(ctx);
tracer.withSpanInScope(span);
}
Yet, updateValue still doesn't work as there are no extras in the context, so nothing to update...
Following the ideas like in those answers, I'm trying to use BaggageFields in one of my tests. All the objects are not null, but updateValue returns false and the test fails with BaggageTest.baggageTest:40 expected: <hello> but was: <null>. As said, I have no idea why the updateValue method is not working.
import static org.junit.jupiter.api.Assertions.assertEquals;
import brave.baggage.BaggageField;
import brave.baggage.CorrelationScopeConfig;
import brave.context.slf4j.MDCScopeDecorator;
import brave.propagation.CurrentTraceContext;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.ScopedSpan;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.event.annotation.BeforeTestMethod;
import org.springframework.test.context.junit.jupiter.SpringExtension;
#Slf4j
#ExtendWith(SpringExtension.class)
#ContextConfiguration
public class BaggageTest {
#Autowired
ApplicationContext context;
#BeforeTestMethod
void init() {
Tracer tracer = context.getBean(Tracer.class);
ScopedSpan span = tracer.startScopedSpan("test");
}
#Test
void baggageTest() {
BaggageField fooBar = context.getBean("fooBar", BaggageField.class);
log.info("updateValue {}", fooBar.updateValue("hello"));
assertEquals("hello", fooBar.getValue());
}
#Configuration
static class Config {
private BaggageField findOrCreate(String name) {
BaggageField field = BaggageField.getByName(name);
if (field == null) {
field = BaggageField.create(name);
}
return field;
}
#Bean("fooBar")
BaggageField fooBar() {
return findOrCreate("fooBar");
}
#Bean
CurrentTraceContext.ScopeDecorator mdcScopeDecorator() {
return MDCScopeDecorator.newBuilder()
.clear()
.add(CorrelationScopeConfig.SingleCorrelationField.newBuilder(fooBar())
.flushOnUpdate()
.build())
.build();
}
}
}
My solution is to manually build the TraceContext, using the following snippet, being careful that BaggageFields is brave.internal.baggage.BaggageFields.
#Autowired
BaggageField field;
Tracer tracer;
#BeforeEach
void init() {
tracer = Tracing.newBuilder().build().tracer();
ArrayList<BaggageField> list = new ArrayList<>();
list.add(field);
TraceContext ctx = TraceContext.newBuilder()
.addExtra(BaggageFields.newFactory(list,2).create())
.traceId(17).spanId(17).build();
Span span = tracer.toSpan(ctx);
tracer.withSpanInScope(span);
}

mockito when interact with each other in different method

I have a short piece of code and two unit test. Strangely when I launch the test separately they works well but when I launch them together it look like the second method use the "when" of the first method.
Tested method :
public ProductIssuer update(ProductIssuer productIssuer) {
findById(productIssuer.getId())
.orElseThrow(() -> new B4FinanceException(ErrorCode.USER_NOT_FOUND, "Please provide an existing user"));
return productIssuerRepository.save(productIssuer);
}
The tests :
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
public class ProductIssuerServiceTest {
#InjectMocks
private static ProductIssuerService productIssuerService;
#Mock
private static ProductIssuerRepository productIssuerRepository;
public static final UUID DEFAULT_UUID = UUID.fromString("b8fc499a-2084-11e8-b467-0ed5f89f0000");
private static final String DEFAULT_NAME = "productIssuer Name";
#BeforeEach
public void setup() {
MockitoAnnotations.initMocks(this);
}
#Test
public void updateNotFoundThrowException() {
ProductIssuer productIssuer = new ProductIssuer();
productIssuer.setName(DEFAULT_NAME);
when(productIssuerRepository.findById(any())).thenReturn(Optional.empty());
assertThatExceptionOfType(B4FinanceException.class).isThrownBy(() -> productIssuerService.update(productIssuer));
}
#Test
public void update() {
ProductIssuer productIssuer = new ProductIssuer();
productIssuer.setName(DEFAULT_NAME);
productIssuer.setId(DEFAULT_UUID);
when(productIssuerRepository.findById(any())).thenReturn(Optional.of(productIssuer));
when(productIssuerRepository.save(any())).thenReturn(productIssuer);
productIssuerService.update(productIssuer);
}
}
The result is ok for the first test (updateNotFoundThrowException) but for the second test I got a "Please provide an existing user" error.

Getting Mockito error: "Wanted but not invoked... actually, there were zero interactions with this mock"

I'm getting the above error whilst running my unit test for a java class in an Android project (in Android Studio).
The class under test:
import android.content.Context;
import android.util.Log;
import **.CustomObject;
import java.util.concurrent.CountDownLatch;
import androidx.annotation.NonNull;
public class CustomClass {
private static final String string = "a";
private static CustomObject customObject = null;
private static CountDownLatch initializedLatch = new CountDownLatch(1);
#NonNull
public static CustomObject1 getCustomObject1() {
try {
initializedLatch.await();
assert customObject != null;
return customObject;
} catch (InterruptedException e) {
throw new RuntimeException(".");
}
}
public static void methodA(final Context context,
final String string1,
) throws exception {
initializedLatch.countDown();
}
public static void methodB(#NonNull final CustomObject customObjectInput) {
customObject = customObjectInput;
}
}
The test class:
import android.content.Context;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.concurrent.CountDownLatch;
import **.CustomObject;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
#RunWith(MockitoJUnitRunner.class)
public class CustomClassTest{
#Mock
static CustomObject customObject;
#Mock
static Context context;
#Mock
CountDownLatch mCountDownLatch;
#Mock
CountDownLatch mInitializedLatch;
#InjectMocks
CustomClass customClass;
#Before
public void setUp() {
customObject = Mockito.spy(CustomObject.class);
context = Mockito.spy(Context.class);
}
#Test
public void customClassTest() {
doNothing().when(mInitializedLatch).countDown();
CustomClass.methodB(customObject);
try {
CustomClass.methodA(context, "");
} catch (Exception e) {
e.printStackTrace();
}
verify(mInitializedLatch).countDown();
try {
doNothing().when(mInitializedLatch).await();
} catch (InterruptedException e) {
e.printStackTrace();
}
Class.getCustomObject();
}
The specific message I'm getting when running customClassTest:
Wanted but not invoked:
mInitializedLatch.countDown();
-> at CustomClassTest.methodA(CustomClassTest.java:79)
Actually, there were zero interactions with this mock.
Wanted but not invoked:
mInitializedLatch.countDown();
-> at CustomClassTest.methodA(CustomClassTest.java:79)
Actually, there were zero interactions with this mock.
Running the debugger with break points at each of the relevant lines seems to suggest that the test runs fine (with all the variables being assigned correctly at the right points) until verify(mInitializedLatch).countDown();, when the message appears (and the code stops running).
Any help appreciated, thanks.
UPDATE #1:
Altered the code to remove static keyword:
import android.content.Context;
import android.util.Log;
import **.CustomObject;
import java.util.concurrent.CountDownLatch;
import androidx.annotation.NonNull;
public class CustomClass {
private final String string = "a";
private CustomObject customObject = null;
private CountDownLatch initializedLatch = new CountDownLatch(1);
#NonNull
public CustomObject1 getCustomObject1() {
try {
initializedLatch.await();
assert customObject != null;
return customObject;
} catch (InterruptedException e) {
throw new RuntimeException(".");
}
}
public void methodA(final Context context,
final String string1,
) throws exception {
initializedLatch.countDown();
}
public void methodB(#NonNull final CustomObject customObjectInput) {
customObject = customObjectInput;
}
}
import android.content.Context;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.concurrent.CountDownLatch;
import **.CustomObject;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
#RunWith(MockitoJUnitRunner.class)
public class CustomClassTest{
#Mock
CustomObject customObject;
#Mock
Context context;
#Mock
CountDownLatch mCountDownLatch;
#Mock
CountDownLatch mInitializedLatch;
#InjectMocks
CustomClass customClass;
#Before
public void setUp() {
customObject = Mockito.spy(CustomObject.class);
context = Mockito.spy(Context.class);
}
#Test
public void customClassTest() {
doNothing().when(mInitializedLatch).countDown();
customClass.methodB(customObject);
try {
customClass.methodA(context, "");
} catch (Exception e) {
e.printStackTrace();
}
verify(mInitializedLatch).countDown();
try {
doNothing().when(mInitializedLatch).await();
} catch (InterruptedException e) {
e.printStackTrace();
}
customClass.getCustomObject();
}
Error messages now read:
error: non-static method methodA(Context,String) cannot be referenced from a static context
error: non-static method getCustomObject1() cannot be referenced from a static context
The second error message is displayed six times. It seems the code isn't compiling.
In CustomClass the CountDownLatch is declared as static field and it's initialized. If you debug your class you can see Mockito is not mocking/proxing this field. All the code interaction to initializedLatch object are not intercepted by Mockito proxy, so when you set-up your test by doNothing().when(mInitializedLatch).countDown(), actually you're not setting the field into customClass.So when you use verify(mInitializedLatch).countDown(), you're actually saying to Mockito that you expect one interaction with this mock, but no interactions are made due the reason above.
You are getting no invocations because the actual call is not made with your mocked mInitializedLatch object.
While mocking any object, you need to tell the compiler to use this mocked object instead of the one indeed present in your source implementation.
This can be achieved by making the object you are looking to test as an instance variable and passing the mocked object in the constructor.
Then the calls will be made from your mocked object and mockito will be able to track those.
Example:
// Source Code
public class CustomerClass {
private final CountDownLatch initializedLatch
public CustomerClass(CountDownLatch initializedLatch) {
this.initializedLatch = initializedLatch;
}
}
Now, use this instance variable in your code instead of the static variable you defined.
In test code, create constructor of CustomerClass by passing the mocked initializedLatch object and then it will work like charm.
If you are looking to initialize the value of initializedLatch there only. You can do the same by keeping a default constructor alongside the constructor I have defined above.
This default constructor can call the parameterized constructor.
public CustomerClass() {
this(new CountDownLatch(1));
}
Edit:
You also need to change your source implementation.
import android.content.Context;
import android.util.Log;
import **.CustomObject;
import java.util.concurrent.CountDownLatch;
import androidx.annotation.NonNull;
public class CustomClass {
private static final String string = "a";
private CustomObject customObject;
private CountDownLatch initializedLatch;
public CustomClass() {
this(new CountDownLatch(1), null);
}
public CustomClass(CountDownLatch initializedLatch, CustomObject customObject) {
this.initializedLatch = initializedLatch;
this.customObject = customObject;
}
#NonNull
public CustomObject1 getCustomObject1() {
try {
initializedLatch.await();
assert customObject != null;
return customObject;
} catch (InterruptedException e) {
throw new RuntimeException(".");
}
}
public void methodA(final Context context final String string1) throws Exception {
initializedLatch.countDown();
}
public void methodB(#NonNull final CustomObject customObjectInput) {
customObject = customObjectInput;
}
}
Now, above source implementation will use initializedLatch and customObject provided in the constructor.
Test code
import android.content.Context;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import java.util.concurrent.CountDownLatch;
import **.CustomObject;
import org.junit.Test;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.MockitoJUnitRunner;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
#RunWith(MockitoJUnitRunner.class)
public class CustomClassTest{
#Mock
CustomObject customObject;
#Mock
Context context;
#Mock
CountDownLatch mInitializedLatch;
#InjectMocks
CustomClass customClass;
#Before
public void setUp() {
customClass = new CustomClass(mInitializedLatch, customObject);
}
#Test
public void customClassTest() {
doNothing().when(mInitializedLatch).countDown();
customClass.methodB(customObject);
try {
customClass.methodA(context, "");
} catch (Exception e) {
e.printStackTrace();
}
verify(mInitializedLatch).countDown();
try {
doNothing().when(mInitializedLatch).await();
} catch (InterruptedException e) {
e.printStackTrace();
}
customClass.getCustomObject();
}
Regarding the error you are getting, I don't think that's because of
the call you are making from the tests.

Adding elements to mocked list

I'm trying to unit test the method responsible for adding to map categorized books.
#Service
public class BookService {
private final List<BookServiceSource> sources;
#Autowired
public BookService(List<BookServiceSource> sources) {
this.sources = sources;
}
public Map<Bookstore, List<Book>> getBooksByCategory(CategoryType category) {
return sources.stream()
.collect(Collectors.toMap(BookServiceSource::getName,
source -> source.getBooksByCategory(category)));
}
}
BookSerivceSource is an interface. This interface is implemented by two classes. I'm gonna provide just one, as the second is really similiar.
EmpikSource (one of implementation)
package bookstore.scraper.book.booksource.empik;
import bookstore.scraper.book.Book;
import bookstore.scraper.book.booksource.BookServiceSource;
import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.enums.CategoryType;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.utilities.JSoupConnector;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.IntStream;
#Service
public class EmpikSource implements BookServiceSource {
private static final int FIRST_PART_PRICE = 0;
private static final int SECOND_PART_PRICE = 1;
private static final int BESTSELLERS_NUMBER_TO_FETCH = 5;
private static final int CATEGORIZED_BOOKS_NUMBER_TO_FETCH = 15;
private static final String DIV_PRODUCT_WRAPPER = "div.productWrapper";
private static final String DATA_PRODUCT_ID = "data-product-id";
private final EmpikUrlProperties empikUrlProperties;
private final JSoupConnector jSoupConnector;
private Map<CategoryType, String> categoryToEmpikURL;
#Autowired
public EmpikSource(EmpikUrlProperties empikUrlProperties, JSoupConnector jSoupConnector) {
this.empikUrlProperties = empikUrlProperties;
this.jSoupConnector = jSoupConnector;
categoryToEmpikURL = createCategoryToEmpikURLMap();
}
#Override
public Bookstore getName() {
return Bookstore.EMPIK;
}
#Override
public List<Book> getBooksByCategory(CategoryType categoryType) {
Document document = jSoupConnector.connect(categoryToEmpikURL.get(categoryType));
List<Book> books = new ArrayList<>();
List<Element> siteElements = document.select("div.productBox__info");
IntStream.range(0, CATEGORIZED_BOOKS_NUMBER_TO_FETCH)
.forEach(iteratedElement -> {
String author = executeFetchingAuthorProcess(siteElements, iteratedElement);
String price = convertEmpikPriceWithPossibleDiscountToActualPrice(siteElements.get(iteratedElement).select("div.productBox__price").first().text());
String title = siteElements.get(iteratedElement).select("span").first().ownText();
String productID = siteElements.get(iteratedElement).select("a").first().attr(DATA_PRODUCT_ID);
String bookUrl = createBookURL(title, productID);
books.add(Book.builder()
.author(author)
.price(price)
.title(title)
.productID(productID)
.bookURL(bookUrl)
.build());
});
return books;
}
private Map<CategoryType, String> createCategoryToEmpikURLMap() {
Map<CategoryType, String> map = new EnumMap<>(CategoryType.class);
map.put(CategoryType.CRIME, empikUrlProperties.getCrime());
map.put(CategoryType.BESTSELLER, empikUrlProperties.getBestSellers());
map.put(CategoryType.BIOGRAPHY, empikUrlProperties.getBiographies());
map.put(CategoryType.FANTASY, empikUrlProperties.getFantasy());
map.put(CategoryType.GUIDES, empikUrlProperties.getGuides());
map.put(CategoryType.MOST_PRECISE_BOOK, empikUrlProperties.getMostPreciseBook());
map.put(CategoryType.ROMANCES, empikUrlProperties.getRomances());
return map;
}
private String convertEmpikPriceWithPossibleDiscountToActualPrice(String price) {
String[] splittedElements = price.split("\\s+");
return splittedElements[FIRST_PART_PRICE] + splittedElements[SECOND_PART_PRICE];
}
private String createBookURL(String title, String productID) {
return String.format(empikUrlProperties.getConcreteBook(), title, productID);
}
//method is required as on empik site, sometimes occurs null for author and we need to change code for fetching
private static String executeFetchingAuthorProcess(List<Element> siteElements, int i) {
String author;
Element authorElements = siteElements.get(i).select("span > a").first();
if (authorElements != null)
author = authorElements.ownText();
else
author = siteElements.get(i).select("> span > span").first().text();
return author;
}
private String concatUrlWithTitle(String url, String title) {
return String.format(url, title);
}
}
JsoupConnector:
package bookstore.scraper.utilities;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.stereotype.Component;
import java.io.IOException;
#Component
public class JSoupConnector {
public Document connect(String url) {
try {
return Jsoup.connect(url).get();
} catch (IOException e) {
throw new IllegalArgumentException("Cannot connect to" + url);
}
}
}
Properties class:
package bookstore.scraper.urlproperties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
#Getter
#Setter
#Component
#ConfigurationProperties("external.library.url.empik")
public class EmpikUrlProperties {
private String mostPreciseBook;
private String bestSellers;
private String concreteBook;
private String romances;
private String biographies;
private String crime;
private String guides;
private String fantasy;
}
While debugging the test I see that the sources size is 0. How should I add mocked object to the sources list or could you tell me if there is better way to do this?
//EDIT
Forgot to paste the test :P
Test
package bookstore.scraper.book;
import bookstore.scraper.book.booksource.BookServiceSource;
import bookstore.scraper.book.booksource.empik.EmpikSource;
import bookstore.scraper.book.booksource.merlin.MerlinSource;
import bookstore.scraper.dataprovider.EmpikBookProvider;
import bookstore.scraper.dataprovider.MerlinBookProvider;
import bookstore.scraper.enums.Bookstore;
import bookstore.scraper.enums.CategoryType;
import bookstore.scraper.urlproperties.EmpikUrlProperties;
import bookstore.scraper.urlproperties.MerlinUrlProperties;
import bookstore.scraper.utilities.JSoupConnector;
import org.jsoup.nodes.Document;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.List;
import java.util.Map;
import static bookstore.scraper.dataprovider.MergedBestsellersMapProvider.prepareExpectedMergedBestSellerMap;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
#RunWith(MockitoJUnitRunner.class)
public class BookServiceTest {
#Mock
MerlinSource merlinSource;
#Mock
EmpikSource empikSource;
#Mock
BookServiceSource bookServiceSource;
#Mock
private EmpikUrlProperties empikMock;
#Mock
private MerlinUrlProperties merlinMock;
#Mock
JSoupConnector jSoupConnector;
#Mock
List<BookServiceSource> source;
#InjectMocks
BookService bookService;
#Test
public void getBooksByCategory() {
List<Book> merlinBestsellers = MerlinBookProvider.prepare5Bestsellers();
List<Book> empikBestsellers = EmpikBookProvider.prepare5Bestsellers();
Document empikDocument = mock(Document.class);
Document merlinDocument = mock(Document.class);
source.add(empikSource);
source.add(merlinSource);
when(bookServiceSource.getName()).thenReturn(Bookstore.EMPIK);
when(jSoupConnector.connect("https://www.empik.com/bestsellery/ksiazki")).thenReturn(empikDocument);
when(empikMock.getBestSellers()).thenReturn("https://www.empik.com/bestsellery/ksiazki");
when(empikSource.getBooksByCategory(CategoryType.CRIME)).thenReturn(empikBestsellers);
when(bookServiceSource.getName()).thenReturn(Bookstore.MERLIN);
when(jSoupConnector.connect("https://merlin.pl/bestseller/?option_80=10349074")).thenReturn(merlinDocument);
when(merlinMock.getBestSellers()).thenReturn("https://merlin.pl/bestseller/?option_80=10349074");
when(merlinSource.getBooksByCategory(CategoryType.CRIME)).thenReturn(merlinBestsellers);
Map<Bookstore, List<Book>> actualMap = bookService.getBooksByCategory(CategoryType.CRIME);
Map<Bookstore, List<Book>> expectedMap = prepareExpectedMergedBestSellerMap();
assertEquals(expectedMap, actualMap);
}
}
As mentioned before do not try to mock the List object.
Also generally avoid to create mocks for objects that you can simply create on your own and try to restrict yourself to mock only dependencies.
A simplified version of your test could look like this:
As your test covers quite a bit more than the Unit BookService
I decided to minimize it for this example.
You might want to do all the other stuff in a test for the specific implementation instead.
#Test
public void getBooksByCategory() {
List<Book> empikBestsellers = EmpikBookProvider.prepare5Bestsellers();
List<Book> merlinBestsellers = MerlinBookProvider.prepare5Bestsellers();
BookServiceSource bookServiceSource1 = Mockito.mock(BookServiceSource.class);
Mockito.when(bookServiceSource1.getName()).thenReturn(Bookstore.EMPIK);
Mockito.when(bookServiceSource1.getBooksByCategory(CategoryType.CRIME)).thenReturn(empikBestsellers);
BookServiceSource bookServiceSource2 = Mockito.mock(BookServiceSource.class);
Mockito.when(bookServiceSource2.getName()).thenReturn(Bookstore.MERLIN);
Mockito.when(bookServiceSource2.getBooksByCategory(CategoryType.CRIME)).thenReturn(merlinBestsellers);
List<BookServiceSource> sources = new ArrayList<>();
sources.add(bookServiceSource1);
sources.add(bookServiceSource2);
BookService service = new BookService(sources);
Map<Bookstore, List<Book>> actualMap = service.getBooksByCategory(CategoryType.CRIME);
// compare result
}
I don't believe you should be mocking the list of BookServiceSource since your adds will do nothing since it is not a real list.
This answer here should provide the information you are looking for: Mockito - Injecting a List of mocks
Edit for more clarity:
#InjectMocks should not be used if you can help it, it has a tendency to fail silently.
The other point I was attempting to make is that you are using a mocked list, and because of that when it is told to add elements it will not.
There are two solutions to the problem that you can use. Firstly you could create a when thenreturn for the stream of BookServiceSources, not the recommended solution.
Secondly what would be better is to create a testSetup method making use of the #Before annotation to create the BookService.
#Before
public void testSetup(){
List<BookServiceSource> list = new LinkedList<>();
list.add(merlinSource);
list.add(empikSource);
bookService = new BookService(list);
}
Try #Spy. It allows you to inject actual instance of a list that you have initialized by yourself and which also can be mocked partially.
#Spy
private List<BookServiceSource> sources = new ArrayList<>();
It seems that you have used different name for the List, prefer to use the smae name that field to mock is injected is; sources.
Good explanation here.
5. Mock vs. Spy in Mockito :
When Mockito creates a mock – it does so from the Class of a Type, not from an actual instance. The mock simply creates a bare-bones shell instance of the Class, entirely instrumented to track interactions with it.
On the other hand, the spy will wrap an existing instance. It will still behave in the same way as the normal instance – the only difference is that it will also be instrumented to track all the interactions with it.

Log4j2 Mock Appender

I have a class in which I take all of my Properties, and hide their passwords before logging.
#Override
public void afterPropertiesSet() throws Exception {
Properties loadedProperties = this.mergeProperties();
loadedProperties.entrySet().stream().forEach(singleProperty -> {
String key = singleProperty.getKey().toString();
String value = HIDDEN_VALUE;
if (!Arrays.stream(PASSWORD_PATTERNS).anyMatch(pattern -> key.toLowerCase().contains(pattern))) {
value = singleProperty.getValue().toString();
}
logger.info("LoadedProperty: "+ key +"=" + value);
});
}
I have migrated to log4j2 and would like to test this class, checking the output of log4j2. It currently uses log4j and works, however when I migrated to log4j2, I get
Wanted but not invoked:
mockAppender.append();
-> at com.comp.spmConf.ExceptionLoggerTest.verifyErrorMessages(ExceptionLoggerTest.java:87)
However, there were other interactions with this mock:
mockAppender.getName();
-> at org.apache.logging.log4j.core.config.AbstractConfiguration.addLoggerAppender(AbstractConfiguration.java:675)
mockAppender.getName();
-> at org.apache.logging.log4j.core.config.AppenderControl.(AppenderControl.java:51)
Here is my log4j1 test class:
import org.apache.log4j.Appender;
import org.apache.log4j.LogManager;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Properties;
#RunWith(MockitoJUnitRunner.class)
public class SpmPropertyTracerTest {
#Mock
private Appender appenderMock;
#Captor
private ArgumentCaptor captorLoggingEvent;
private SpmPropertyTracer tracer;
#Before
public void setup() {
LogManager.getRootLogger().addAppender(appenderMock);
tracer = new SpmPropertyTracer();
}
#After
public void teardown() {
LogManager.getRootLogger().removeAppender(appenderMock);
}
#Test
public void printPropertiesTest() throws Exception{
String key1 = "Foo";
String val1 = "True";
Properties properties = new Properties();
properties.setProperty(key1, val1);
tracer.setProperties(properties);
String expectedString = String.format("LoadedProperty: %s=%s", key1, val1);
tracer.afterPropertiesSet();
Mockito.verify(appenderMock).doAppend((LoggingEvent)captorLoggingEvent.capture());
LoggingEvent loggingEvent = (LoggingEvent) captorLoggingEvent.getValue();
assert expectedString.equals(loggingEvent.getRenderedMessage());
}
}
And here is my log4j2 test class, am I doing something wrong in the log4j to log4j2 migration?
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.Appender;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.util.Properties;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
#RunWith(MockitoJUnitRunner.class)
public class TestClass {
#Mock
private Appender mockAppender;
#Captor
private ArgumentCaptor<LogEvent> captorLoggingEvent;
private SpmPropertyTracer tracer;
private Logger logger;
private LogEvent logEvent;
#Before
public void setup() {
// prepare the appender so Log4j likes it
when(mockAppender.getName()).thenReturn("MockAppender");
when(mockAppender.isStarted()).thenReturn(true);
when(mockAppender.isStopped()).thenReturn(false);
logger = (Logger)LogManager.getLogger(SpmPropertyTracer.class);
logger.addAppender(mockAppender);
logger.setLevel(Level.INFO);
tracer = new SpmPropertyTracer();
}
#After
public void tearDown() {
// the appender we added will sit in the singleton logger forever
// slowing future things down - so remove it
logger.removeAppender(mockAppender);
}
#Test
public void loggingIsCaptured() throws Exception {
String key1 = "Foo";
String val1 = "True";
Properties properties = new Properties();
properties.setProperty(key1, val1);
tracer.setProperties(properties);
String expectedString = String.format("LoadedProperasdfty: %s=%s", key1, val1);
tracer.afterPropertiesSet();
verifyErrorMessages(expectedString);
}
// handy function to inspect the messages sent to the logger
private void verifyErrorMessages(String ... messages) {
verify(mockAppender, times(messages.length)).append((LogEvent)captorLoggingEvent.capture());
int i=0;
for(LogEvent loggingEvent:captorLoggingEvent.getAllValues()) {
assertEquals(messages[i++], loggingEvent.getMessage().getFormattedMessage());
}
}
The parent project was brining in a log4j dependency, thus slf4j was binding with log4j and not log4j2 and that is why the append method was not invoked. Removing that dependency fixed my errors.

Categories

Resources