📜 ⬆️ ⬇️

JUnit logging tests

Logging refers to end-to-end functionality - scattered throughout the code and, as a rule, rarely covered by unit tests. A weak test coverage is obviously due to the fact that logging is not always important enough and is perceived rather as an auxiliary function and not the purpose of the method, besides testing end-to-end functionality is quite difficult.
But when the correctness of output to the log becomes critical, or the sense of beauty requires us to fully cover the code with tests — it becomes necessary to do testing loggers.

Suppose we have a simple class with a log4j logger and the doSomethingWithInt method

import org.apache.log4j.Logger; public class ClassWithLog4JLogger { private static final Logger logger = Logger.getLogger(ClassWithLog4JLogger.class); public void doSomethingWithInt(int i){ logger.info(" -  doSomethingWithInt    i = " +i); if (i>0){ logger.info(" -  i  "); }else{ logger.info(" -  i    "); } } } 

We want to test the fact that the method call

 new ClassWithLog4JLogger().doSomethingWithInt(1); 

will lead to output to the log
')
- the method doSomethingWithInt is called with the parameter i = 1
- the parameter i is greater than zero

The traditional approach to testing involves injecting a mock object (using Mockitio ) into the class under test and, after working out the test code, check how and what parameters were passed to the mock.

The problem is that it is quite difficult to inject a logger into our class - it is not passed to ClassWithLog4JLogger, but is returned from a static method, which Mockito does not know how to replace returnValue (Mockito is designed to test objects, while static). -method refers to a class and not an object). But the problem, of course, is solved - and in several ways.

Method 1. Mock for log4j-Appender


“In the absence of a maid, we have a janitor ...” Suppose we cannot replace the logger itself - but we can slip a mock upender to it and make sure that the logger sends to the appedder the events that we expect.

Add a project JUnit and Mockito

 <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-all</artifactId> <version>1.10.19</version> <scope>test</scope> </dependency> 

and write this test

 import org.apache.log4j.Appender; import org.apache.log4j.Logger; import org.apache.log4j.spi.LoggingEvent; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @RunWith(MockitoJUnitRunner.class) //   Mockito     - public class ClassWithLog4JLoggerAppenderMockTest { @Mock Appender mockAppender; @Test public void doLoggingTest() { //      Logger logger = Logger.getLogger(ClassWithLog4JLogger.class); //   mock- logger.addAppender(mockAppender); //    ClassWithLog4JLogger classWithLog4JLogger = new ClassWithLog4JLogger(); classWithLog4JLogger.doSomethingWithInt(1); // ''   mock  ArgumentCaptor<LoggingEvent> eventArgumentCaptor = ArgumentCaptor.forClass(LoggingEvent.class); //,      verify(mockAppender, times(2)).doAppend(eventArgumentCaptor.capture()); //      Assert.assertEquals(" -  doSomethingWithInt    i = 1", eventArgumentCaptor.getAllValues().get(0).getMessage()); Assert.assertEquals(" -  i  ", eventArgumentCaptor.getAllValues().get(1).getMessage()); //      ( ) Assert.assertEquals(Level.INFO, eventArgumentCaptor.getAllValues().get(0).getLevel()); Assert.assertEquals(Level.INFO, eventArgumentCaptor.getAllValues().get(1).getLevel()); } } 

Everything seems to be quite simple and does not need explanation.

The only drawback of this approach is that since we are testing not the logger but the appender, then we check not the arguments of the logger methods but the arguments that the logger passes to the appender (LoggingEvent), but their verification takes slightly more lines of code.

This approach will also work if slf4J is used as a logger. This is an add-on over log4j (and several other logging frameworks), allowing, for example, to output log parameters without string concatenation (see example below). The slf4J logger itself has no methods for adding an appender. But at the same time, it uses the downstream framework (from those found in the classpath) in the process of its work. If this framework is log4j, then we can slip the mock-appender into the log4j-logger - it in turn will be called from slf4J

So, add dependencies on slf4j and its bundle with log4j

 <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.25</version> </dependency> 

And we will test the class almost the same as in the previous example - the only difference is in the logger and passing parameters to the log (now without string concatenation)

 import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ClassWithSlf4JLogger { private static final Logger logger = LoggerFactory.getLogger(ClassWithSlf4JLogger.class); public void doSomethingWithInt(int i) { logger.info(" -  doSomethingWithInt    i = {}", i); if (i > 0) { logger.info(" -  i  "); } else { logger.info(" -  i    "); } } } 

The test for it will remain exactly the same - only the name of the class for which we get the logger will change (despite the fact that it is still log4j and not slf4j logger)!

 Logger logger = Logger.getLogger(ClassWithSlf4JLogger.class); 

Method 2. Substitution slf4j-implementation.
But what if we still want to replace not the appender, but the logger itself? It is still possible. As mentioned above - slf4 uses one of the downstream frameworks to choose from (log4j, logback, etc.) You can add another implementation to the project and remove the rest from the classpath - then slf4 will “pick up” it. And in the test implementation of the logger there are methods that allow you to check its calls.

So - add dependency

 <dependency> <groupId>uk.org.lidalia</groupId> <artifactId>slf4j-test</artifactId> <version>1.2.0</version> <scope>test</scope> </dependency> 

and - IMPORTANT (!) we delete other slf4j loggers in the build process if they are in the project.

 <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <configuration> <classpathDependencyExcludes> <classpathDependencyExcludes>org.slf4j:slf4j-log4j12</classpathDependencyExcludes> </classpathDependencyExcludes> </configuration> </plugin> </plugins> </build> 

The test (for the class used in the last example) looks extremely simple.

 import org.junit.After; import org.junit.Assert; import org.junit.Test; import uk.org.lidalia.slf4jtest.TestLogger; import uk.org.lidalia.slf4jtest.TestLoggerFactory; public class ClassWithSlf4JTestLoggerTest { //   TestLogger logger = TestLoggerFactory.getTestLogger(ClassWithSlf4JLogger.class); @Test public void doLoggingTest() { ClassWithSlf4JLogger classWithSlf4JLogger = new ClassWithSlf4JLogger(); classWithSlf4JLogger.doSomethingWithInt(1); //     Assert.assertEquals(" -  doSomethingWithInt    i = {}", logger.getLoggingEvents().asList().get(0).getMessage()); Assert.assertEquals(1, logger.getLoggingEvents().asList().get(0).getArguments().get(0)); Assert.assertEquals(" -  i  ", logger.getLoggingEvents().asList().get(1).getMessage()); Assert.assertEquals(2, logger.getLoggingEvents().asList().size()); } @After public void clearLoggers() { TestLoggerFactory.clear(); } } 

There is simply no place. But there is also a minus - the test will work only in conjunction with a maven or another build system that will remove the classes of other loggers, while the previous test sharpened for slf4j-log4j will not work. In my opinion, this is not very convenient as it binds us in the means used (be sure to start with maven) and the tools (not using other loggers in tests).

Method 3: Mock Logger with PowerMock


PowerMock is like a mockito. But cooler. What is cooler? The fact that it can work with static methods, final classes, protected and even private fields ... A kind of hammer in a jewelry shop (by the way, it is a sledgehammer depicted on the PowerMock emblem) - in everyday use the tool is too powerful, but sometimes without it - nowhere. So, it is great for testing logging - we simply override the LoggerFactory.getLogger method and send our mock object to it,

 <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>1.7.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>1.7.3</version> <scope>test</scope> </dependency> 

 import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InOrder; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.anyVararg; import static org.powermock.api.mockito.PowerMockito.mock; @RunWith(PowerMockRunner.class) //  PowerMock       LoggerFactory @PrepareForTest({LoggerFactory.class}) public class PowerMockitoLoggingTest { //    private static Logger logger = mock(Logger.class);; //    LoggerFactory.getLogger -        static{ PowerMockito.spy(LoggerFactory.class); try { PowerMockito.doReturn(logger).when(LoggerFactory.class, "getLogger", any()); }catch (Exception e){ e.printStackTrace(); } } @Test public void doLoggingTest() { ClassWithSlf4JLogger classWithSlf4JLogger = new ClassWithSlf4JLogger(); classWithSlf4JLogger.doSomethingWithInt(1); //   . //   -    ,               InOrder inOrd = Mockito.inOrder(logger); inOrd.verify(logger).info(" -  doSomethingWithInt    i = {}",1); inOrd.verify(logger).info(" -  i  "); Mockito.verify(logger, Mockito.times(1)).info(anyString()); Mockito.verify(logger, Mockito.times(1)).info(anyString(), anyVararg()); } 

Summary


All methods have the right to exist. Appender mocking appears to be the most simple, not requiring the use of new (except JUnit and Mockito) libraries, but does not work directly with the logger.

Slf4j-test requires a minimum of code - but makes you play with the substitution of classes in the classpath. PowerMock is quite simple and allows you to inject mock logger into the tested class.

Code of examples

Source: https://habr.com/ru/post/344876/


All Articles