You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2020/09/07 16:31:26 UTC

[logging-log4j2] branch release-2.x updated (9b2d1db -> 9e6cf16)

This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a change to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git.


    from 9b2d1db  [LOG4J2-2653] Migrate more tests to JUnit 5
     new 04fa378  Introduce annotation for JUnit 5 LCF tests
     new 9e6cf16  Migrate some parameterized tests to JUnit 5

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../junit/LogManagerLoggerContextFactoryRule.java  |   2 +-
 .../log4j/junit/LoggerContextFactoryExtension.java |  31 ++--
 ...text.java => RegisterLoggerContextFactory.java} |  10 +-
 .../logging/log4j/simple/SimpleLoggerTest.java     |   9 +-
 .../core/config/ConfigurationFactoryTest.java      | 130 +++++++++++++++++
 .../log4j/core/config/ConfigurationTest.java       | 157 ---------------------
 .../log4j/core/config/TestConfiguratorError.java   |   8 +-
 .../logging/log4j/core/config/XIncludeTest.java    | 139 ------------------
 8 files changed, 163 insertions(+), 323 deletions(-)
 copy log4j-api/src/test/java/org/apache/logging/log4j/junit/{UsingAnyThreadContext.java => RegisterLoggerContextFactory.java} (80%)
 create mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
 delete mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationTest.java
 delete mode 100644 log4j-core/src/test/java/org/apache/logging/log4j/core/config/XIncludeTest.java


[logging-log4j2] 02/02: Migrate some parameterized tests to JUnit 5

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 9e6cf16722871ba2d260efc11e5b4cc7538647a8
Author: Matt Sicker <bo...@gmail.com>
AuthorDate: Mon Sep 7 11:30:04 2020 -0500

    Migrate some parameterized tests to JUnit 5
    
    As JUnit 5 uses method parameters instead of constructor parameters, this makes it a bit trickier to port JUnit 4 parameterized tests.
    
    Backported from 3.x.
    
    Signed-off-by: Matt Sicker <bo...@gmail.com>
---
 .../core/config/ConfigurationFactoryTest.java      | 130 +++++++++++++++++
 .../log4j/core/config/ConfigurationTest.java       | 157 ---------------------
 .../logging/log4j/core/config/XIncludeTest.java    | 139 ------------------
 3 files changed, 130 insertions(+), 296 deletions(-)

diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
new file mode 100644
index 0000000..b076721
--- /dev/null
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package org.apache.logging.log4j.core.config;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
+import org.apache.logging.log4j.junit.LoggerContextSource;
+import org.apache.logging.log4j.util.Strings;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.logging.log4j.util.Unbox.box;
+import static org.junit.jupiter.api.Assertions.*;
+
+class ConfigurationFactoryTest {
+
+    static final String LOGGER_NAME = "org.apache.logging.log4j.test1.Test";
+    static final String FILE_LOGGER_NAME = "org.apache.logging.log4j.test2.Test";
+    static final String APPENDER_NAME = "STDOUT";
+
+    /**
+     * Runs various configuration checks on a configured LoggerContext that should match the equivalent configuration in
+     * {@code log4j-test1.xml}.
+     */
+    static void checkConfiguration(final LoggerContext context) {
+        final Configuration configuration = context.getConfiguration();
+        final Map<String, Appender> appenders = configuration.getAppenders();
+        // these used to be separate tests
+        assertAll(() -> assertNotNull(appenders),
+                () -> assertEquals(3, appenders.size()),
+                () -> assertNotNull(configuration.getLoggerContext()),
+                () -> assertEquals(configuration.getRootLogger(), configuration.getLoggerConfig(Strings.EMPTY)),
+                () -> assertThrows(NullPointerException.class, () -> configuration.getLoggerConfig(null)));
+
+        final Logger logger = context.getLogger(LOGGER_NAME);
+        assertEquals(Level.DEBUG, logger.getLevel());
+
+        assertEquals(1, logger.filterCount());
+        final Iterator<Filter> filterIterator = logger.getFilters();
+        assertTrue(filterIterator.hasNext());
+        assertTrue(filterIterator.next() instanceof ThreadContextMapFilter);
+
+        final Appender appender = appenders.get(APPENDER_NAME);
+        assertTrue(appender instanceof ConsoleAppender);
+        assertEquals(APPENDER_NAME, appender.getName());
+    }
+
+    static void checkFileLogger(final LoggerContext context, final Path logFile) throws IOException {
+        final long currentThreadId = Thread.currentThread().getId();
+        final Logger logger = context.getLogger(FILE_LOGGER_NAME);
+        logger.debug("Greetings from ConfigurationFactoryTest in thread#{}", box(currentThreadId));
+        try {
+            final List<String> lines = Files.readAllLines(logFile);
+            assertEquals(1, lines.size());
+            assertTrue(lines.get(0).endsWith(Long.toString(currentThreadId)));
+        } finally {
+            Files.delete(logFile);
+        }
+    }
+
+    @Test
+    @LoggerContextSource("log4j-test1.xml")
+    void xml(final LoggerContext context) throws IOException {
+        checkConfiguration(context);
+        final Path logFile = Paths.get("target", "test-xml.log");
+        checkFileLogger(context, logFile);
+    }
+
+    @Test
+    @LoggerContextSource("log4j-xinclude.xml")
+    void xinclude(final LoggerContext context) throws IOException {
+        checkConfiguration(context);
+        final Path logFile = Paths.get("target", "test-xinclude.log");
+        checkFileLogger(context, logFile);
+    }
+
+    @Test
+    @Tag("json")
+    @LoggerContextSource("log4j-test1.json")
+    void json(final LoggerContext context) throws IOException {
+        checkConfiguration(context);
+        final Path logFile = Paths.get("target", "test-json.log");
+        checkFileLogger(context, logFile);
+    }
+
+    @Test
+    @Tag("yaml")
+    @LoggerContextSource("log4j-test1.yaml")
+    void yaml(final LoggerContext context) throws IOException {
+        checkConfiguration(context);
+        final Path logFile = Paths.get("target", "test-yaml.log");
+        checkFileLogger(context, logFile);
+    }
+
+    @Test
+    @LoggerContextSource("log4j-test1.properties")
+    void properties(final LoggerContext context) throws IOException {
+        checkConfiguration(context);
+        final Path logFile = Paths.get("target", "test-properties.log");
+        checkFileLogger(context, logFile);
+    }
+}
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationTest.java
deleted file mode 100644
index a7cfe17..0000000
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/ConfigurationTest.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-
-package org.apache.logging.log4j.core.config;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Map;
-
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.Filter;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
-import org.apache.logging.log4j.junit.CleanFiles;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-import static org.hamcrest.CoreMatchers.*;
-
-import static org.junit.Assert.*;
-
-/**
- * Unit tests for testing various supported configuration file formats. Each configuration file format should provide a
- * compatible configuration to get some sweet, sweet tests.
- */
-@RunWith(Parameterized.class)
-public class ConfigurationTest {
-
-    private static final String LOGGER_NAME = "org.apache.logging.log4j.test1.Test";
-    private static final String FILE_LOGGER_NAME = "org.apache.logging.log4j.test2.Test";
-    private static final String APPENDER_NAME = "STDOUT";
-
-    private final String logFileName;
-
-    @Rule
-    public TestRule rules;
-
-    private final LoggerContextRule init;
-
-    private LoggerContext ctx;
-
-    private final SecureRandom random = new SecureRandom();
-
-    public ConfigurationTest(final String configFileName, final String logFileName) {
-        this.logFileName = logFileName;
-        this.init = new LoggerContextRule(configFileName);
-        rules = RuleChain.outerRule(new CleanFiles(logFileName)).around(this.init);
-    }
-
-    @Parameters(name = "config={0}, log={1}")
-    public static Collection<Object[]> data() {
-        return Arrays.asList(
-                new Object[][]{
-                        {"classpath:log4j-test1.xml", "target/test-xml.log"},
-                        {"classpath:log4j-test1.json", "target/test-json.log"},
-                        {"classpath:log4j-test1.yaml", "target/test-yaml.log"},
-                        {"classpath:log4j-test1.properties", "target/test-properties.log"}
-                }
-        );
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        this.ctx = this.init.getLoggerContext();
-    }
-
-    @Test
-    public void testConfiguredAppenders() throws Exception {
-        final Configuration configuration = this.ctx.getConfiguration();
-        final Map<String, Appender> appenders = configuration.getAppenders();
-        assertThat(appenders, is(notNullValue()));
-        assertThat(appenders.size(), is(equalTo(3)));
-    }
-
-    @Test
-    public void testConfigurationLoggerContext() throws Exception {
-        final Configuration configuration = this.ctx.getConfiguration();
-        assertThat(configuration.getLoggerContext(), is(notNullValue()));
-    }
-
-    @Test
-    public void testGetLoggerConfigEmpty() throws Exception {
-        final Configuration config = this.ctx.getConfiguration();
-        assertEquals(config.getRootLogger(), config.getLoggerConfig(Strings.EMPTY));
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void testGetLoggerConfigNull() throws Exception {
-        final Configuration config = this.ctx.getConfiguration();
-        assertEquals(config.getRootLogger(), config.getLoggerConfig(null));
-    }
-
-    @Test
-    public void testLogger() throws Exception {
-        final Logger logger = this.ctx.getLogger(LOGGER_NAME);
-        assertThat(logger, is(instanceOf(org.apache.logging.log4j.core.Logger.class)));
-        final org.apache.logging.log4j.core.Logger l = (org.apache.logging.log4j.core.Logger) logger;
-        assertThat(l.getLevel(), is(equalTo(Level.DEBUG)));
-        assertThat(l.filterCount(), is(equalTo(1)));
-        final Iterator<Filter> iterator = l.getFilters();
-        assertThat(iterator.hasNext(), is(true));
-        final Filter filter = iterator.next();
-        assertThat(filter, is(instanceOf(ThreadContextMapFilter.class)));
-        final Map<String, Appender> appenders = l.getAppenders();
-        assertThat(appenders, is(notNullValue()));
-        assertThat(appenders.size(), is(equalTo(1)));
-        final Appender appender = appenders.get(APPENDER_NAME);
-        assertThat(appender, is(notNullValue()));
-        assertThat(appender.getName(), is(equalTo("STDOUT")));
-    }
-
-    @Test
-    public void testLogToFile() throws Exception {
-        final Logger logger = this.ctx.getLogger(FILE_LOGGER_NAME);
-        final long random = this.random.nextLong();
-        logger.debug("This is test message number {}", random);
-        int count = 0;
-        String line = Strings.EMPTY;
-        try (final BufferedReader in = new BufferedReader(new FileReader(this.logFileName))) {
-            while (in.ready()) {
-                ++count;
-                line = in.readLine();
-            }
-        }
-        assertThat(count, is(equalTo(1)));
-        assertThat(line, endsWith(Long.toString(random)));
-    }
-
-}
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/XIncludeTest.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/XIncludeTest.java
deleted file mode 100644
index 8847da1..0000000
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/XIncludeTest.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache license, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the license for the specific language governing permissions and
- * limitations under the license.
- */
-
-package org.apache.logging.log4j.core.config;
-
-import java.io.BufferedReader;
-import java.io.FileReader;
-import java.security.SecureRandom;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.Map;
-
-import org.apache.logging.log4j.Level;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.core.Appender;
-import org.apache.logging.log4j.core.Filter;
-import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
-import org.apache.logging.log4j.junit.CleanFiles;
-import org.apache.logging.log4j.junit.LoggerContextRule;
-import org.apache.logging.log4j.util.Strings;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-
-import static org.apache.logging.log4j.hamcrest.MapMatchers.hasSize;
-import static org.hamcrest.Matchers.endsWith;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.notNullValue;
-import static org.junit.Assert.*;
-
-/**
- * Tests XInclude.
- */
-@RunWith(Parameterized.class)
-public class XIncludeTest {
-
-    private static final String LOGGER_NAME = "org.apache.logging.log4j.test1.Test";
-    private static final String FILE_LOGGER_NAME = "org.apache.logging.log4j.test2.Test";
-    private static final String APPENDER_NAME = "STDOUT";
-
-    private final String logFileName;
-
-    @Rule
-    public TestRule rules;
-
-    private final LoggerContextRule init;
-
-    private LoggerContext ctx;
-
-    private final SecureRandom random = new SecureRandom();
-
-    public XIncludeTest(final String configFileName, final String logFileName) {
-        this.logFileName = logFileName;
-        this.init = new LoggerContextRule(configFileName);
-        this.rules = RuleChain.outerRule(new CleanFiles(logFileName)).around(this.init);
-    }
-
-    @Parameters(name = "config={0}, log={1}")
-    public static Collection<Object[]> data() {
-        return Arrays.asList(
-                new Object[][]{
-                        {"classpath:log4j-xinclude.xml", "target/test-xinclude.log"},
-                }
-        );
-    }
-
-    @Before
-    public void setUp() throws Exception {
-        this.ctx = this.init.getLoggerContext();
-    }
-
-    @Test
-    public void testConfiguredAppenders() throws Exception {
-        final Configuration configuration = this.ctx.getConfiguration();
-        final Map<String, Appender> appenders = configuration.getAppenders();
-        assertThat(appenders, is(notNullValue()));
-        assertThat(appenders.size(), is(equalTo(3)));
-    }
-
-    @Test
-    public void testLogger() throws Exception {
-        final Logger logger = this.ctx.getLogger(LOGGER_NAME);
-        assertThat(logger, is(instanceOf(org.apache.logging.log4j.core.Logger.class)));
-        final org.apache.logging.log4j.core.Logger l = (org.apache.logging.log4j.core.Logger) logger;
-        assertThat(l.getLevel(), is(equalTo(Level.DEBUG)));
-        assertThat(l.filterCount(), is(equalTo(1)));
-        final Iterator<Filter> iterator = l.getFilters();
-        assertThat(iterator.hasNext(), is(true));
-        final Filter filter = iterator.next();
-        assertThat(filter, is(instanceOf(ThreadContextMapFilter.class)));
-        final Map<String, Appender> appenders = l.getAppenders();
-        assertThat(appenders, is(notNullValue()));
-        assertThat(appenders, hasSize(1));
-        final Appender appender = appenders.get(APPENDER_NAME);
-        assertThat(appender, is(notNullValue()));
-        assertThat(appender.getName(), is(equalTo("STDOUT")));
-    }
-
-    @Test
-    public void testLogToFile() throws Exception {
-        final Logger logger = this.ctx.getLogger(FILE_LOGGER_NAME);
-        final long random = this.random.nextLong();
-        logger.debug("This is test message number {}", random);
-        int count = 0;
-        String line = Strings.EMPTY;
-        try (BufferedReader in = new BufferedReader(new FileReader(this.logFileName))) {
-            while (in.ready()) {
-                ++count;
-                line = in.readLine();
-            }
-        }
-        assertThat(count, is(equalTo(1)));
-        assertThat(line, endsWith(Long.toString(random)));
-    }
-
-}


[logging-log4j2] 01/02: Introduce annotation for JUnit 5 LCF tests

Posted by ma...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 04fa3783ef5ae60eb17e68c706b6fb2b32623950
Author: Matt Sicker <bo...@gmail.com>
AuthorDate: Mon Sep 7 11:28:52 2020 -0500

    Introduce annotation for JUnit 5 LCF tests
    
    This is less awkward than the previous syntax.
    
    Signed-off-by: Matt Sicker <bo...@gmail.com>
---
 .../junit/LogManagerLoggerContextFactoryRule.java  |  2 +-
 .../log4j/junit/LoggerContextFactoryExtension.java | 31 +++++++++-------
 .../log4j/junit/RegisterLoggerContextFactory.java  | 41 ++++++++++++++++++++++
 .../logging/log4j/simple/SimpleLoggerTest.java     |  9 +++--
 .../log4j/core/config/TestConfiguratorError.java   |  8 ++---
 5 files changed, 69 insertions(+), 22 deletions(-)

diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/junit/LogManagerLoggerContextFactoryRule.java b/log4j-api/src/test/java/org/apache/logging/log4j/junit/LogManagerLoggerContextFactoryRule.java
index 060e553..8fe4680 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/junit/LogManagerLoggerContextFactoryRule.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/junit/LogManagerLoggerContextFactoryRule.java
@@ -24,7 +24,7 @@ import org.junit.rules.ExternalResource;
  * Sets the {@link LogManager}'s {@link LoggerContextFactory} to the given instance before the test and restores it to
  * the original value after the test.
  *
- * @deprecated Use {@link LoggerContextFactoryExtension} with JUnit 5
+ * @deprecated Use {@link RegisterLoggerContextFactory} with JUnit 5
  */
 public class LogManagerLoggerContextFactoryRule extends ExternalResource {
 
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/junit/LoggerContextFactoryExtension.java b/log4j-api/src/test/java/org/apache/logging/log4j/junit/LoggerContextFactoryExtension.java
index 1131e1b..7649c79 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/junit/LoggerContextFactoryExtension.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/junit/LoggerContextFactoryExtension.java
@@ -22,25 +22,32 @@ import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.junit.jupiter.api.extension.AfterAllCallback;
 import org.junit.jupiter.api.extension.BeforeAllCallback;
 import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.commons.support.ModifierSupport;
+import org.junit.platform.commons.support.ReflectionSupport;
 
-/**
- * JUnit 5 extension that sets a particular {@link LoggerContextFactory} for the entire run of tests in a class.
- *
- * @since 2.14.0
- */
-public class LoggerContextFactoryExtension implements BeforeAllCallback, AfterAllCallback {
+import java.lang.reflect.Field;
+import java.util.List;
 
-    private static final String KEY = "previousFactory";
-    private final LoggerContextFactory loggerContextFactory;
+class LoggerContextFactoryExtension implements BeforeAllCallback, AfterAllCallback {
 
-    public LoggerContextFactoryExtension(LoggerContextFactory loggerContextFactory) {
-        this.loggerContextFactory = loggerContextFactory;
-    }
+    private static final String KEY = "previousFactory";
 
     @Override
     public void beforeAll(ExtensionContext context) throws Exception {
+        final Class<?> testClass = context.getRequiredTestClass();
+        final List<Field> loggerContextFactories = ReflectionSupport.findFields(testClass,
+                f -> ModifierSupport.isStatic(f) && f.isAnnotationPresent(RegisterLoggerContextFactory.class),
+                HierarchyTraversalMode.BOTTOM_UP);
+        if (loggerContextFactories.isEmpty()) {
+            return;
+        }
+        if (loggerContextFactories.size() > 1) {
+            throw new IllegalArgumentException("More than one static LoggerContextFactory specified in " + testClass.getName());
+        }
         getStore(context).put(KEY, LogManager.getFactory());
-        LogManager.setFactory(loggerContextFactory);
+        final LoggerContextFactory factory = (LoggerContextFactory) loggerContextFactories.get(0).get(null);
+        LogManager.setFactory(factory);
     }
 
     @Override
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/junit/RegisterLoggerContextFactory.java b/log4j-api/src/test/java/org/apache/logging/log4j/junit/RegisterLoggerContextFactory.java
new file mode 100644
index 0000000..aa69b1e
--- /dev/null
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/junit/RegisterLoggerContextFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package org.apache.logging.log4j.junit;
+
+import org.apache.logging.log4j.spi.LoggerContextFactory;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * JUnit 5 extension that sets a particular {@link LoggerContextFactory} instance for the entire run of tests in a class.
+ *
+ * @since 2.14.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+@Inherited
+@ExtendWith(LoggerContextFactoryExtension.class)
+public @interface RegisterLoggerContextFactory {
+}
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/simple/SimpleLoggerTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/simple/SimpleLoggerTest.java
index ac42497..529b811 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/simple/SimpleLoggerTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/simple/SimpleLoggerTest.java
@@ -18,17 +18,16 @@ package org.apache.logging.log4j.simple;
 
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.junit.LoggerContextFactoryExtension;
+import org.apache.logging.log4j.junit.RegisterLoggerContextFactory;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 @Tag("smoke")
 public class SimpleLoggerTest {
 
-    @RegisterExtension
-    public static final LoggerContextFactoryExtension EXTENSION =
-            new LoggerContextFactoryExtension(new SimpleLoggerContextFactory());
+    @RegisterLoggerContextFactory
+    static final LoggerContextFactory FACTORY = new SimpleLoggerContextFactory();
 
     private final Logger logger = LogManager.getLogger("TestError");
 
diff --git a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/TestConfiguratorError.java b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/TestConfiguratorError.java
index 4c5021c..efba175 100644
--- a/log4j-core/src/test/java/org/apache/logging/log4j/core/config/TestConfiguratorError.java
+++ b/log4j-core/src/test/java/org/apache/logging/log4j/core/config/TestConfiguratorError.java
@@ -17,17 +17,17 @@
 package org.apache.logging.log4j.core.config;
 
 import org.apache.logging.log4j.core.LoggerContext;
-import org.apache.logging.log4j.junit.LoggerContextFactoryExtension;
+import org.apache.logging.log4j.junit.RegisterLoggerContextFactory;
 import org.apache.logging.log4j.simple.SimpleLoggerContextFactory;
+import org.apache.logging.log4j.spi.LoggerContextFactory;
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
 
 import static org.junit.jupiter.api.Assertions.assertNull;
 
 public class TestConfiguratorError {
 
-    @RegisterExtension
-    static final LoggerContextFactoryExtension extension = new LoggerContextFactoryExtension(new SimpleLoggerContextFactory());
+    @RegisterLoggerContextFactory
+    static final LoggerContextFactory FACTORY = new SimpleLoggerContextFactory();
 
     @Test
     public void testErrorNoClassLoader() throws Exception {