I have a class with SLF4J logger instanced like:
public class MyClass {
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
public void foo() {
log.warn("My warn");
}
}
And I need to test it with JMockit like:
#Test
public void shouldLogWarn(#Mocked Logger log) throws Exception {
new Expectations() {{
log.warn(anyString);
}};
MyClass my = new MyClass();
my.foo();
}
After searching a lot I figured out, I need to use MockUp somehow. But can't get it how exactly.
Btw, I'm using last version of JMockit(1.29) where you no more can setField(log) for final static fields.
JMockit has the #Capturing annotation that works for this situation
Indicates a mock field or a mock parameter for which all classes extending/implementing the mocked type will also get mocked.
Future instances of a capturing mocked type (ie, instances created sometime later during the test) will become associated with the mock field/parameter. When recording or verifying expectations on the mock field/parameter, these associated instances are regarded as equivalent to the original mocked instance created for the mock field/parameter.
This means that if you annotate it with #Capturing instead of #Mocked, every Logger that is created during the test run will be associated with one you annotated. So the following works:
#Test
public void shouldLogWarn(#Capturing final Logger logger) throws Exception {
// This really ought to be a Verifications block instead
new Expectations() {{
logger.warn(anyString);
}};
MyClass my = new MyClass();
my.foo();
}
As a side note, if all you want to do is verify that a method is called, it's better to use Verifications instead, since that is what it is intended for. So your code would look like this:
#Test
public void shouldLogWarn(#Capturing final Logger logger) throws Exception {
MyClass my = new MyClass();
my.foo();
new Verifications() {{
logger.warn(anyString);
}};
}
Alternatively, you can use #Mocked on both Logger and LoggerFactory
In some cases, #Capturing won't work as intended due to intricacies of how the annotation works. Fortunately, you can also get the same effect by using #Mocked on both Logger and LoggerFactory like so:
#Test
public void shouldLogWarn(#Mocked final LoggerFactory loggerFactory, #Mocked final Logger logger) throws Exception {
MyClass my = new MyClass();
my.foo();
new Verifications() {{
logger.warn(anyString);
}};
}
Note: JMockit 1.34 through 1.38 has a bug that prevents this from working with slf4j-log4j12, and possibly other dependencies of SLF4J. Upgrade to 1.39 or later if you run into this bug.
Related
I'm struggling with writing unit tests for one of my classes. The main class:
public class MainClass {
...
private Instance<BaseInterface> steps;
#Inject
public void setSteps(Instance<BaseInterface> steps) { this.steps = steps; }
#Asynchronous
public void callingMethod() {
...
ImmutableList<BaseInterface> stepList = STEP_ORDERING.immutableSortedCopy(steps); // Here the NPE is being thrown when calling method from test class
...
}
}
There are 5 specific implementations of BaseInterface, all 5 are correctly injected through setter by CDI at runtime (one of the implementation is annotated with #Named, the rest do not have this annotation).
However, calling the wrapper method callingMethod from the test class throws NPE. The test class:
#RunWith(MockitoJUnitRunner.class)
public class MainClassTest {
#InjectMocks
private MainClass mainClass = new MainClass();
#Mock
private Instance<BaseInterface> steps;
#Test
public void test() {
...
mainClass.callingMethod(); // Throws NPE
...
}
}
As far as I'm concerned, Mockito does not take care of bean dependency injection like CDI. Therefore, is there a way in which to tell Mockito to mock all implementations of BaseInterface and inject them into the MainClass?
Version details: Java 8, Mockito 2.8.9, JUnit 4
Thank you!
Solved. The problem was not caused by Mockito, but by the method STEP_ORDERING.immutableSortedCopy which behind the scenes calls iterator() method from Instance<T> which returned null (since it extends Iterable<T>).
Mocking the iterator does the trick:
Iterator<BaseInterface> iteratorMock = (Iterator<BaseInterface>) mock(Iterator.class);
when(steps.iterator()).thenReturn(iteratorMock);
or by creating an actual instance of iterator:
Iterator<BaseInterface> iteratorActual = new Iterator<BaseInterface>(){
// implementation here
};
when(steps.iterator()).thenReturn(iteratorActual );
In a unit test in a spring-boot environment with slf4j and logback, I want to make sure LOGGER.isTraceEnabled() returns true for a particular test class only.
The reason for that is, that sometimes we have slow and non trivial code guarded with if (LOGGER.isTraceEnabled()) {...}. In a unit test we want to make sure it does not break the application if we switch on trace.
Code under test:
public class ClassUnderTest{
private static final Logger LOGGER = LoggerFactory.getLogger(ClassUnderTest.class);
public void doSomething(Calendar gegenwart) {
if (LOGGER.isTraceEnabled()) {
// non trivial code
}
}
}
Test code:
public class ClassUnderTestUnitTest{
#Test
public void findeSnapshotMitLueke() {
ClassUnderTest toTestInstance = new ClassUnderTest ();
// I want to make sure trace logging is enabled, when
// this method is executed.
// Trace logging should not be enabled outside this
// test class.
toTestInstance.doSomething(Calendar.getInstance());
}
}
You can easily create a static mock of LoggerFactory using PowerMock and cause it to return a regular mock of Logger (using EasyMock). Then you simply define mock implementation of Logger.isTraceEnabled() and Logger.trace().
Off the top of my head:
#PrepareForTest({ LoggerFactory.class })
#RunWith(PowerMockRunner.class) // assuming JUnit...
public class ClassUnderTestUnitTest{
#Test
public void findeSnapshotMitLueke() {
Logger mockLogger = EasyMock.createMock(Logger.class);
EasyMock.expect(mockLogger.isTraceEnabled()).andReturn(true);
EasyMock.expect(mockLogger.trace(any()));
EasyMock.expectLastCall().anyTimes() // as trace is a void method.
// Repeat for other log methods ...
PowerMock.mockStatic(LoggerFactory.class);
EasyMock.expect(LoggerFactory.getLogger(ClassUnderTest.class)
.andReturn(mockLogger);
PowerMock.replay(mockLogger, LoggerFactory.class);
ClassUnderTest toTestInstance = new ClassUnderTest ();
// I want to make sure trace logging is enabled, when
// this method is executed.
// Trace logging should not be enabled outside this
// test class.
toTestInstance.doSomething(Calendar.getInstance());
// After the operation if needed you can verify that the mocked methods were called.
PowerMock.verify(mockLogger).times(...);
PowerMock.verifyStatic(LoggerFactory.class).times(...);
}
}
In case you don't want to use a framework like powermock you can do the following trick:
public class ClassUnderTest {
private Supplier<Logger> loggerSupplier = () -> getLogger(ClassUnderTest.class);
public void doSomething(Calendar gegenwart) {
if (loggerSupplier.get().isTraceEnabled()) {
// non trivial code
}
}
}
Now you are able to mock the logger:
public class ClassUnderTestUnitTest{
#Mock
private Supplier<Mock> loggerSupplier;
#Mock
private Logger logger;
#Test
public void findeSnapshotMitLueke() {
ClassUnderTest toTestInstance = new ClassUnderTest ();
when(loggerSupplier.get()).thenReturn(logger);
when(logger.isTraceEnabled()).thenReturn(true)
toTestInstance.doSomething(Calendar.getInstance());
// verifyLogger();
}
}
I developed a kind of wrapper to make it work as a custom logger. I'm instantiating this class using #CustomLog Lombok annotation just to make it easier and cleaner. The tricky thing comes next: the idea behind this wrapper is to use a common logger (as org.slf4j.Logger) along with a custom monitor class that each time I call log.error(), the proper message gets logged in the terminal and the event is sent to my monitoring tool (Prometheus in this case).
To achieve this I did the following classes:
CustomLoggerFactory the factory called by Lombok to instantiate my custom logger.
public final class CustomLoggerFactory {
public static CustomLogger getLogger(String className) {
return new CustomLogger(className);
}
}
CustomLogger will receive the class name just to then call org.slf4j.LoggerFactory.
public class CustomLogger {
private org.slf4j.Logger logger;
private PrometheusMonitor prometheusMonitor;
private String className;
public CustomLogger(String className) {
this.logger = org.slf4j.LoggerFactory.getLogger(className);
this.className = className;
this.monitor = SpringContext.getBean(PrometheusMonitor.class);
}
}
PrometheusMonitor class is the one in charge of creating the metrics and that kind of things. The most important thing here is that it's being managed by Spring Boot.
#Component
public class PrometheusMonitor {
private MeterRegistry meterRegistry;
public PrometheusMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
}
As you may noticed, to access PrometheusMonitor from CustomLogger I need an additional class in order to get the Bean / access the context from a non Spring managed class. This is the SpringContext class which has an static method to get the bean by the class supplied.
#Component
public class SpringContext implements ApplicationContextAware {
private static ApplicationContext context;
public static <T extends Object> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
#Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
SpringContext.context = context;
}
}
So all this works just fine when running the application. I ensure to load SpringContext class before anything else, so once each CustomLogger gets instantiated it just works.
But the BIG issue comes here: this is not working while unit testing my app. I tried many things and I saw some solutions that may help me but that I'm trying to avoid (e.g. using PowerMockito). Lombok is processing #CustomLog annotation before any #Before method I add to my test class. Once getBean() method is called I get an exception cause context is null.
My guesses are that I could solve it if I can force the SpringContext to be loaded before Lombok does its magic, but I'm not sure that's even possible. Many thanks for taking your time to read this. Any more info I can provide just let me know.
NOTE: It sounds like your custom logging needs are better served by logging to slf4j as normal, and registering an additional handler with the slf4j framework so that slf4j will forward any logs to you (in addition to the other handlers, such as the one making the log files).
Lombok is processing #CustomLog
The generated log field is static. If an annotation is going to help at all, you'd need #BeforeClass, but that probably also isn't in time. Lombok's magic doesn't seem relevant here. Check out what delombok tells you lombok is doing: It's just.. a static field, being initialized on declaration.
Well I managed to solve this issue changing a little how the CustomLogger works. Meaning that instead of instantiating monitor field along with the logger, you can do it the first time you'll use it. E.g.:
public class CustomLogger {
private org.slf4j.Logger logger;
private Monitor monitor;
public CustomLogger(String className) {
this.logger = org.slf4j.LoggerFactory.getLogger(className);
}
public void info(String message) {
this.logger.info(message);
}
public void error(String message) {
this.logger.error(message);
if (this.monitor == null) {
this.monitor = SpringContext.getBean(PrometheusMonitor.class);
}
this.monitor.send(message);
}
}
But after all I decided to not follow this approach because I don't think it's the best one possible and worth it.
I'm trying to use JMockit to test that a certain logging operation takes place:
public class LogClass1 {
public void doLog() {
Logger logger = LogManager.getLogger(LogClass1.class);
logger.info("This is a log message for {}", "arg1");
}
}
public class LogClass1Test {
#Mocked
private Logger logger;
#Tested
private LogClass1 x;
#Before
public void setup() {
x = new LogClass1();
}
#Test
public void testDoLog() {
new Expectations() {
{
logger.info("This is a log message for {}", "arg1");
}
};
x.doLog();
}
}
But this results in a "missing 1 invocation to org.apache.logging.log4j.Logger#info" error.
I've done similar mocking with log4j 1.x in the past, and I haven't had this problem. I'm wondering if there's some issue because log4j 2.x seems to have many more overloads of its info() methods.
I tried changing "arg1" to (Object)"arg1" in the unit test to see if I could get it to match the signature. This didn't help.
Any thoughts on how I can get this to work?
Note that Logger is an interface, and that LogClass1 obtains an instance of it through the LogManager.getLogger static factory method. So, obviously, it creates an instance of some Logger implementation class. And said class is not being mocked in the test.
What the test needs to do is to mock LogManager, so it returns the #Mocked Logger instance. That is, add a #Mocked LogManager field to the test class.
(Also, no need for that setup method since #Tested creates an instance automatically.)
In a JMockit test, I have the following code:
#Tested
private PromotionsAddOrUpdateEntryStrategy strategy;
#Mocked
private BuyXGetYPromoPreAddOrUpdateEntryCommand precommand;
#Before
public void setUp()
{
initializeCommands(precommand);
}
protected void initializeCommands(final BuyXGetYPromoPreAddOrUpdateEntryCommand command)
{
final List<AddOrUpdateEntryCommand> commands = new ArrayList<>();
commands.add(command);
strategy.setPrecommands(commands);
}
When the test is executed, then I get a NullPointerException in strategy object. Why does it happen? And what is the correct way to do this? The idea is to avoid the repetition of the initializeCommands method in all tests.
You can configure the #Tested field so that it's initialized before any #Before method runs:
#Tested(availableDuringSetup = true)
private PromotionsAddOrUpdateEntryStrategy strategy;
See the API documentation for more details.
There is a simpler solution, though, since support for injection of #Injectables into a List has recently been added (version 1.28).
So, the following should work, with no need for a #Before method:
#Tested PromotionsAddOrUpdateEntryStrategy strategy;
#Injectable AddOrUpdateEntryCommand precommand;