J'ai un code sous test qui appelle un enregistreur Java pour signaler son état . Dans le code de test JUnit, j'aimerais vérifier que l'entrée de journal correcte a été effectuée dans cet enregistreur. Quelque chose dans les lignes suivantes:
methodUnderTest(bool x){
if(x)
logger.info("x happened")
}
@Test tester(){
// perhaps setup a logger first.
methodUnderTest(true);
assertXXXXXX(loggedLevel(),Level.INFO);
}
Je suppose que cela pourrait être fait avec un enregistreur spécialement adapté (ou un gestionnaire, ou un formateur), mais je préférerais réutiliser une solution déjà existante. (Et, pour être honnête, il n’est pas clair pour moi comment obtenir l’enregistrement logRecord à partir d’un enregistreur, mais supposons que cela soit possible.)
Merci beaucoup pour ces réponses (étonnamment) rapides et utiles; ils m'ont mis sur la bonne voie pour ma solution.
La base de code où je veux utiliser ceci utilise Java.util.logging comme mécanisme de journalisation, et je ne me sens pas assez à l'aise dans ces codes pour changer complètement cela en log4j ou en interfaces/façades de journalisation. Mais sur la base de ces suggestions, je "piraté" une extension j.u.l.handler et cela fonctionne comme un régal.
Un bref résumé suit. Étendre Java.util.logging.Handler
:
class LogHandler extends Handler
{
Level lastLevel = Level.FINEST;
public Level checkLevel() {
return lastLevel;
}
public void publish(LogRecord record) {
lastLevel = record.getLevel();
}
public void close(){}
public void flush(){}
}
De toute évidence, vous pouvez stocker autant que vous le souhaitez/vouloir/avoir besoin de la LogRecord
, ou les mettre tous dans une pile jusqu'à ce que vous obteniez un débordement.
Lors de la préparation du test Junit, vous créez un Java.util.logging.Logger
auquel vous ajoutez une nouvelle LogHandler
:
@Test tester() {
Logger logger = Logger.getLogger("my junit-test logger");
LogHandler handler = new LogHandler();
handler.setLevel(Level.ALL);
logger.setUseParentHandlers(false);
logger.addHandler(handler);
logger.setLevel(Level.ALL);
L'appel à setUseParentHandlers()
consiste à faire taire les gestionnaires normaux, afin que (pour cette exécution du test Junit) aucune journalisation inutile ne se produise. Faites tout ce dont votre code à tester a besoin pour utiliser cet enregistreur, lancez le test et assertEquality:
libraryUnderTest.setLogger(logger);
methodUnderTest(true); // see original question.
assertEquals("Log level as expected?", Level.INFO, handler.checkLevel() );
}
(Bien entendu, vous déplaceriez une grande partie de ce travail dans une méthode @Before
et apporteriez diverses améliorations, mais cela encombrerait cette présentation.)
J'ai eu besoin de cela plusieurs fois aussi. J'ai rassemblé ci-dessous un petit échantillon que vous voudriez adapter à vos besoins. En gros, vous créez votre propre Appender
et vous l'ajoutez à l'enregistreur souhaité. Si vous souhaitez tout collecter, le logger racine est un bon point de départ, mais vous pouvez en utiliser un plus spécifique si vous le souhaitez. N'oubliez pas de supprimer Appender lorsque vous avez terminé, sinon vous pourriez créer une fuite de mémoire. Ci-dessous, je l'ai fait dans le test, mais setUp
ou @Before
et tearDown
ou @After
pourraient être de meilleurs endroits, en fonction de vos besoins.
En outre, l’implémentation ci-dessous collecte tout ce qui se trouve dans un List
en mémoire. Si vous vous connectez beaucoup, vous pouvez envisager d’ajouter un filtre pour supprimer les entrées ennuyeuses ou pour écrire le journal dans un fichier temporaire sur le disque (astuce: LoggingEvent
est Serializable
, vous devriez donc pouvoir sérialiser les objets le message du journal est.)
import org.Apache.log4j.AppenderSkeleton;
import org.Apache.log4j.Level;
import org.Apache.log4j.Logger;
import org.Apache.log4j.spi.LoggingEvent;
import org.junit.Test;
import Java.util.ArrayList;
import Java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
public class MyTest {
@Test
public void test() {
final TestAppender appender = new TestAppender();
final Logger logger = Logger.getRootLogger();
logger.addAppender(appender);
try {
Logger.getLogger(MyTest.class).info("Test");
}
finally {
logger.removeAppender(appender);
}
final List<LoggingEvent> log = appender.getLog();
final LoggingEvent firstLogEntry = log.get(0);
assertThat(firstLogEntry.getLevel(), is(Level.INFO));
assertThat((String) firstLogEntry.getMessage(), is("Test"));
assertThat(firstLogEntry.getLoggerName(), is("MyTest"));
}
}
class TestAppender extends AppenderSkeleton {
private final List<LoggingEvent> log = new ArrayList<LoggingEvent>();
@Override
public boolean requiresLayout() {
return false;
}
@Override
protected void append(final LoggingEvent loggingEvent) {
log.add(loggingEvent);
}
@Override
public void close() {
}
public List<LoggingEvent> getLog() {
return new ArrayList<LoggingEvent>(log);
}
}
Effectivement, vous testez un effet secondaire d'une classe dépendante. Pour les tests unitaires, il vous suffit de vérifier que
logger.info()
a été appelé avec le paramètre correct. Utilisez donc un framework moqueur pour émuler le logger et cela vous permettra de tester le comportement de votre propre classe.
Voici une solution de consignation simple et efficace.
Il n’est pas nécessaire d’ajouter/créer une nouvelle classe.
Il s’appuie sur ListAppender
: un ajouteur de journal de boîte blanche dans lequel les entrées de journal sont ajoutées dans un champ public List
que nous pourrions ainsi utiliser pour faire nos assertions.
Voici un exemple simple.
Classe Foo:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
static final Logger LOGGER = LoggerFactory.getLogger(Foo .class);
public void doThat() {
logger.info("start");
//...
logger.info("finish");
}
}
Classe FooTest:
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
public class FooTest {
@Test
void doThat() throws Exception {
// get Logback Logger
Logger fooLogger = (Logger) LoggerFactory.getLogger(Foo.class);
// create and start a ListAppender
ListAppender<ILoggingEvent> listAppender = new ListAppender<>();
listAppender.start();
// add the appender to the logger
fooLogger.addAppender(listAppender);
// call method under test
Foo foo = new Foo();
foo.doThat();
// JUnit assertions
List<ILoggingEvent> logsList = listAppender.list;
assertEquals("start", logsList.get(0)
.getMessage());
assertEquals(Level.INFO, logsList.get(0)
.getLevel());
assertEquals("finish", logsList.get(1)
.getMessage());
assertEquals(Level.INFO, logsList.get(1)
.getLevel());
}
}
Les assertions JUnit ne paraissent pas très adaptées pour affirmer certaines propriétés spécifiques des éléments de la liste.
Les bibliothèques Matcher/assertion comme AssertJ ou Hamcrest semblent mieux pour cela:
Avec AssertJ ce serait:
import org.assertj.core.api.Assertions;
Assertions.assertThat(listAppender.list)
.extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel)
.containsExactly(Tuple.tuple("start", Level.INFO), Tuple.tuple("finish", Level.INFO));
Une autre option consiste à simuler Appender et à vérifier si un message a été connecté à cet appender. Exemple pour Log4j 1.2.x et mockito:
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import org.Apache.log4j.Appender;
import org.Apache.log4j.Level;
import org.Apache.log4j.Logger;
import org.Apache.log4j.spi.LoggingEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
public class MyTest {
private final Appender appender = mock(Appender.class);
private final Logger logger = Logger.getRootLogger();
@Before
public void setup() {
logger.addAppender(appender);
}
@Test
public void test() {
// when
Logger.getLogger(MyTest.class).info("Test");
// then
ArgumentCaptor<LoggingEvent> argument = ArgumentCaptor.forClass(LoggingEvent.class);
verify(appender).doAppend(argument.capture());
assertEquals(Level.INFO, argument.getValue().getLevel());
assertEquals("Test", argument.getValue().getMessage());
assertEquals("MyTest", argument.getValue().getLoggerName());
}
@After
public void cleanup() {
logger.removeAppender(appender);
}
}
Le moquage est une option ici, bien que cela soit difficile, car les enregistreurs sont généralement des statiques finales privées. Par conséquent, la configuration d'un enregistreur factice ne serait pas un jeu d'enfant ou exigerait une modification de la classe à tester.
Vous pouvez créer un Appender personnalisé (ou le nom de son choix) et l'enregistrer, soit via un fichier de configuration réservé au test, soit à l'exécution (d'une certaine manière, en fonction du cadre de journalisation) . Vous pouvez ensuite obtenir cet appender. (soit statiquement, si déclaré dans le fichier de configuration, soit par sa référence actuelle, si vous le branchez au moment de l'exécution), et vérifiez son contenu.
Inspiré par la solution de @ RonaldBlaschke, j'ai proposé ceci:
public class Log4JTester extends ExternalResource {
TestAppender appender;
@Override
protected void before() {
appender = new TestAppender();
final Logger rootLogger = Logger.getRootLogger();
rootLogger.addAppender(appender);
}
@Override
protected void after() {
final Logger rootLogger = Logger.getRootLogger();
rootLogger.removeAppender(appender);
}
public void assertLogged(Matcher<String> matcher) {
for(LoggingEvent event : appender.events) {
if(matcher.matches(event.getMessage())) {
return;
}
}
fail("No event matches " + matcher);
}
private static class TestAppender extends AppenderSkeleton {
List<LoggingEvent> events = new ArrayList<LoggingEvent>();
@Override
protected void append(LoggingEvent event) {
events.add(event);
}
@Override
public void close() {
}
@Override
public boolean requiresLayout() {
return false;
}
}
}
... ce qui vous permet de faire:
@Rule public Log4JTester logTest = new Log4JTester();
@Test
public void testFoo() {
user.setStatus(Status.PREMIUM);
logTest.assertLogged(
stringContains("Note added to account: premium customer"));
}
Vous pourriez probablement le faire utiliser de façon plus intelligente hamcrest, mais je m'en suis tenu à cela.
Voici ce que j'ai fait pour la journalisation.
J'ai créé une classe TestAppender:
public class TestAppender extends AppenderBase<ILoggingEvent> {
private Stack<ILoggingEvent> events = new Stack<ILoggingEvent>();
@Override
protected void append(ILoggingEvent event) {
events.add(event);
}
public void clear() {
events.clear();
}
public ILoggingEvent getLastEvent() {
return events.pop();
}
}
Puis, dans le parent de ma classe de test unitaire testng, j'ai créé une méthode:
protected TestAppender testAppender;
@BeforeClass
public void setupLogsForTesting() {
Logger root = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
testAppender = (TestAppender)root.getAppender("TEST");
if (testAppender != null) {
testAppender.clear();
}
}
J'ai un fichier logback-test.xml défini dans src/test/resources et j'ai ajouté un testeur de test:
<appender name="TEST" class="com.intuit.icn.TestAppender">
<encoder>
<pattern>%m%n</pattern>
</encoder>
</appender>
et ajouté cet appender à l'appender root:
<root>
<level value="error" />
<appender-ref ref="STDOUT" />
<appender-ref ref="TEST" />
</root>
Maintenant, dans mes classes de test qui s'étendent de ma classe de test parent, je peux obtenir l'appender, obtenir le dernier message enregistré et vérifier le message, le niveau, le jetable.
ILoggingEvent lastEvent = testAppender.getLastEvent();
assertEquals(lastEvent.getMessage(), "...");
assertEquals(lastEvent.getLevel(), Level.WARN);
assertEquals(lastEvent.getThrowableProxy().getMessage(), "...");
Comme mentionné par les autres, vous pouvez utiliser un cadre moqueur. Pour que cela fonctionne, vous devez exposer l'enregistreur de votre classe (bien que je préférerais que le paquet soit privé plutôt que de créer un séparateur public).
L'autre solution consiste à créer un faux enregistreur à la main. Vous devez écrire le faux enregistreur (plus de code d'installation) mais dans ce cas, je préférerais une lisibilité améliorée des tests par rapport au code enregistré dans le cadre moqueur.
Je ferais quelque chose comme ça:
class FakeLogger implements ILogger {
public List<String> infos = new ArrayList<String>();
public List<String> errors = new ArrayList<String>();
public void info(String message) {
infos.add(message);
}
public void error(String message) {
errors.add(message);
}
}
class TestMyClass {
private MyClass myClass;
private FakeLogger logger;
@Before
public void setUp() throws Exception {
myClass = new MyClass();
logger = new FakeLogger();
myClass.logger = logger;
}
@Test
public void testMyMethod() {
myClass.myMethod(true);
assertEquals(1, logger.infos.size());
}
}
Sensationnel. Je ne sais pas pourquoi c'était si difficile. J'ai constaté que je ne pouvais utiliser aucun des exemples de code ci-dessus parce que j'utilisais log4j2 sur slf4j. Ceci est ma solution:
public class SpecialLogServiceTest {
@Mock
private Appender appender;
@Captor
private ArgumentCaptor<LogEvent> captor;
@InjectMocks
private SpecialLogService specialLogService;
private LoggerConfig loggerConfig;
@Before
public void setUp() {
// prepare the appender so Log4j likes it
when(appender.getName()).thenReturn("MockAppender");
when(appender.isStarted()).thenReturn(true);
when(appender.isStopped()).thenReturn(false);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig("org.example.SpecialLogService");
loggerConfig.addAppender(appender, AuditLogCRUDService.LEVEL_AUDIT, null);
}
@After
public void tearDown() {
loggerConfig.removeAppender("MockAppender");
}
@Test
public void writeLog_shouldCreateCorrectLogMessage() throws Exception {
SpecialLog specialLog = new SpecialLogBuilder().build();
String expectedLog = "this is my log message";
specialLogService.writeLog(specialLog);
verify(appender).append(captor.capture());
assertThat(captor.getAllValues().size(), is(1));
assertThat(captor.getAllValues().get(0).getMessage().toString(), is(expectedLog));
}
}
Pour log4j2, la solution est légèrement différente car AppenderSkeleton n'est plus disponible. En outre, l'utilisation de Mockito ou d'une bibliothèque similaire pour créer un Appender avec un ArgumentCaptor ne fonctionnera pas si vous attendez plusieurs messages de journalisation car MutableLogEvent est réutilisé avec plusieurs messages de journal. La meilleure solution que j'ai trouvée pour log4j2 est:
private static MockedAppender mockedAppender;
private static Logger logger;
@Before
public void setup() {
mockedAppender.message.clear();
}
/**
* For some reason mvn test will not work if this is @Before, but in Eclipse it works! As a
* result, we use @BeforeClass.
*/
@BeforeClass
public static void setupClass() {
mockedAppender = new MockedAppender();
logger = (Logger)LogManager.getLogger(MatchingMetricsLogger.class);
logger.addAppender(mockedAppender);
logger.setLevel(Level.INFO);
}
@AfterClass
public static void teardown() {
logger.removeAppender(mockedAppender);
}
@Test
public void test() {
// do something that causes logs
for (String e : mockedAppender.message) {
// add asserts for the log messages
}
}
private static class MockedAppender extends AbstractAppender {
List<String> message = new ArrayList<>();
protected MockedAppender() {
super("MockedAppender", null, null);
}
@Override
public void append(LogEvent event) {
message.add(event.getMessage().getFormattedMessage());
}
}
Pour ma part, vous pouvez simplifier votre test en utilisant JUnit
avec Mockito
. Je vous propose la solution suivante:
import org.Apache.log4j.Appender;
import org.Apache.log4j.Level;
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.InjectMocks;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import Java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.Tuple;
import static org.mockito.Mockito.times;
@RunWith(MockitoJUnitRunner.class)
public class MyLogTest {
private static final String FIRST_MESSAGE = "First message";
private static final String SECOND_MESSAGE = "Second message";
@Mock private Appender appender;
@Captor private ArgumentCaptor<LoggingEvent> captor;
@InjectMocks private MyLog;
@Before
public void setUp() {
LogManager.getRootLogger().addAppender(appender);
}
@After
public void tearDown() {
LogManager.getRootLogger().removeAppender(appender);
}
@Test
public void shouldLogExactlyTwoMessages() {
testedClass.foo();
then(appender).should(times(2)).doAppend(captor.capture());
List<LoggingEvent> loggingEvents = captor.getAllValues();
assertThat(loggingEvents).extracting("level", "renderedMessage").containsExactly(
Tuple(Level.INFO, FIRST_MESSAGE)
Tuple(Level.INFO, SECOND_MESSAGE)
);
}
}
C’est pourquoi nous avons Nice flexibilité pour les tests avec quantité de messages différente
Mocking the Appender peut vous aider à capturer les lignes de journal . Vous trouverez un exemple sur: http://clearqa.blogspot.co.uk/2016/12/test-log-lines.html
// Fully working test at: https://github.com/njaiswal/logLineTester/blob/master/src/test/Java/com/nj/Utils/UtilsTest.Java
@Test
public void testUtilsLog() throws InterruptedException {
Logger utilsLogger = (Logger) LoggerFactory.getLogger("com.nj.utils");
final Appender mockAppender = mock(Appender.class);
when(mockAppender.getName()).thenReturn("MOCK");
utilsLogger.addAppender(mockAppender);
final List<String> capturedLogs = Collections.synchronizedList(new ArrayList<>());
final CountDownLatch latch = new CountDownLatch(3);
//Capture logs
doAnswer((invocation) -> {
LoggingEvent loggingEvent = invocation.getArgumentAt(0, LoggingEvent.class);
capturedLogs.add(loggingEvent.getFormattedMessage());
latch.countDown();
return null;
}).when(mockAppender).doAppend(any());
//Call method which will do logging to be tested
Application.main(null);
//Wait 5 seconds for latch to be true. That means 3 log lines were logged
assertThat(latch.await(5L, TimeUnit.SECONDS), is(true));
//Now assert the captured logs
assertThat(capturedLogs, hasItem(containsString("One")));
assertThat(capturedLogs, hasItem(containsString("Two")));
assertThat(capturedLogs, hasItem(containsString("Three")));
}
En utilisant Jmockit (1.21), j'ai pu écrire ce test simple. Le test s'assure qu'un message d'erreur spécifique n'est appelé qu'une seule fois.
@Test
public void testErrorMessage() {
final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger( MyConfig.class );
new Expectations(logger) {{
//make sure this error is happens just once.
logger.error( "Something went wrong..." );
times = 1;
}};
new MyTestObject().runSomethingWrong( "aaa" ); //SUT that eventually cause the error in the log.
}
Ce que j'ai fait si tout ce que je veux faire, c'est voir qu'une chaîne a été enregistrée (par opposition à la vérification d'instructions de journal exactes et trop fragiles) consiste à rediriger StdOut vers un tampon, à effectuer un conteneur, puis à réinitialiser StdOut:
PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer));
// Do something that logs
assertTrue(buffer.toString().contains(myMessage));
System.setOut(original);
si vous utilisez Java.util.logging.Logger
, cet article peut s'avérer très utile. Il crée un nouveau gestionnaire et formule des assertions dans le journal. Sortie: http://octodecillion.com/blog/jmockit-test-logging/
Utilisez le code ci-dessous. J'utilise le même code pour mon test d'intégration Spring, où j'utilise log back pour la journalisation. Utilisez la méthode assertJobIsScheduled pour affirmer le texte imprimé dans le journal.
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.LoggingEvent;
import ch.qos.logback.core.Appender;
private Logger rootLogger;
final Appender mockAppender = mock(Appender.class);
@Before
public void setUp() throws Exception {
initMocks(this);
when(mockAppender.getName()).thenReturn("MOCK");
rootLogger = (Logger) LoggerFactory.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
rootLogger.addAppender(mockAppender);
}
private void assertJobIsScheduled(final String matcherText) {
verify(mockAppender).doAppend(argThat(new ArgumentMatcher() {
@Override
public boolean matches(final Object argument) {
return ((LoggingEvent)argument).getFormattedMessage().contains(matcherText);
}
}));
}
Vous pouvez essayer de tester deux choses.
Ces deux choses sont en réalité différentes, et pourraient donc être testées séparément. Cependant, tester le second (le texte des messages) est tellement problématique que je recommande de ne pas le faire du tout. En fin de compte, le test du texte d'un message consiste à vérifier qu'une chaîne de texte (le texte du message attendu) est identique à la chaîne de texte utilisée dans votre code de journalisation ou qu'elle peut en être dérivée de manière triviale.
Notez qu'avoir votre code de programme (implémentant peut-être une logique métier) appelant directement l'interface de journalisation de texte est de mauvaise conception (mais malheureusement très commun). Le code responsable de la logique applicative décide également de la stratégie de journalisation et du texte des messages de journalisation. Il associe la logique métier au code de l'interface utilisateur (oui, les messages de journalisation font partie de l'interface utilisateur de votre programme). Ces choses devraient être séparées.
Je recommande donc que la logique applicative ne génère pas directement le texte des messages de journal. Au lieu de cela, déléguez-le à un objet de journalisation.
implements
et interface
, qui décrit l'API interne que votre logique métier peut utiliser.interface
.Vous pouvez ensuite vérifier que vos classes de logique métier communiquent correctement les événements à l'interface de journalisation en créant un journal factice implémentant l'API de journalisation interne et en utilisant l'injection de dépendances lors de la phase de configuration de votre test.
Comme ça:
public class MyService {// The class we want to test
private final MyLogger logger;
public MyService(MyLogger logger) {
this.logger = Objects.requireNonNull(logger);
}
public void performTwiddleOperation(Foo foo) {// The method we want to test
...// The business logic
logger.performedTwiddleOperation(foo);
}
};
public interface MyLogger {
public void performedTwiddleOperation(Foo foo);
...
};
public final class MySl4jLogger: implements MyLogger {
...
@Override
public void performedTwiddleOperation(Foo foo) {
logger.info("twiddled foo " + foo.getId());
}
}
public final void MyProgram {
public static void main(String[] argv) {
...
MyLogger logger = new MySl4jLogger(...);
MyService service = new MyService(logger);
startService(service);// or whatever you must do
...
}
}
public class MyServiceTest {
...
static final class MyMockLogger: implements MyLogger {
private Food.id id;
private int nCallsPerformedTwiddleOperation;
...
@Override
public void performedTwiddleOperation(Foo foo) {
id = foo.id;
++nCallsPerformedTwiddleOperation;
}
void assertCalledPerformedTwiddleOperation(Foo.id id) {
assertEquals("Called performedTwiddleOperation", 1, nCallsPerformedTwiddleOperation);
assertEquals("Called performedTwiddleOperation with correct ID", id, this.id);
}
};
@Test
public void testPerformTwiddleOperation_1() {
// Setup
MyMockLogger logger = new MyMockLogger();
MyService service = new MyService(logger);
Foo.Id id = new Foo.Id(...);
Foo foo = new Foo(id, 1);
// Execute
service.performedTwiddleOperation(foo);
// Verify
...
logger.assertCalledPerformedTwiddleOperation(id);
}
}
Une autre idée qui mérite d'être mentionnée, même s'il s'agit d'un sujet plus ancien, consiste à créer un producteur de CDI pour injecter votre enregistreur afin de faciliter les moqueries. (Et cela donne aussi l'avantage de ne plus avoir à déclarer la "déclaration complète du logger", mais c'est hors sujet)
Exemple:
Création du logger à injecter:
public class CdiResources {
@Produces @LoggerType
public Logger createLogger(final InjectionPoint ip) {
return Logger.getLogger(ip.getMember().getDeclaringClass());
}
}
Le qualificatif:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface LoggerType {
}
Utilisation de l'enregistreur dans votre code de production:
public class ProductionCode {
@Inject
@LoggerType
private Logger logger;
public void logSomething() {
logger.info("something");
}
}
Test de l’enregistreur dans votre code de test (en donnant un exemple easyMock):
@TestSubject
private ProductionCode productionCode = new ProductionCode();
@Mock
private Logger logger;
@Test
public void testTheLogger() {
logger.info("something");
replayAll();
productionCode.logSomething();
}
L'API pour Log4J2 est légèrement différente. Vous pouvez aussi utiliser son appender async. J'ai créé un appender verrouillé pour cela:
public static class LatchedAppender extends AbstractAppender implements AutoCloseable {
private final List<LogEvent> messages = new ArrayList<>();
private final CountDownLatch latch;
private final LoggerConfig loggerConfig;
public LatchedAppender(Class<?> classThatLogs, int expectedMessages) {
this(classThatLogs, null, null, expectedMessages);
}
public LatchedAppender(Class<?> classThatLogs, Filter filter, Layout<? extends Serializable> layout, int expectedMessages) {
super(classThatLogs.getName()+"."+"LatchedAppender", filter, layout);
latch = new CountDownLatch(expectedMessages);
final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
final Configuration config = ctx.getConfiguration();
loggerConfig = config.getLoggerConfig(LogManager.getLogger(classThatLogs).getName());
loggerConfig.addAppender(this, Level.ALL, ThresholdFilter.createFilter(Level.ALL, null, null));
start();
}
@Override
public void append(LogEvent event) {
messages.add(event);
latch.countDown();
}
public List<LogEvent> awaitMessages() throws InterruptedException {
assertTrue(latch.await(10, TimeUnit.SECONDS));
return messages;
}
@Override
public void close() {
stop();
loggerConfig.removeAppender(this.getName());
}
}
Utilisez-le comme ceci:
try (LatchedAppender appender = new LatchedAppender(ClassUnderTest.class, 1)) {
ClassUnderTest.methodThatLogs();
List<LogEvent> events = appender.awaitMessages();
assertEquals(1, events.size());
//more assertions here
}//appender removed