Index: test/unit/org/openstreetmap/josm/gui/bbox/SizeButtonTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/bbox/SizeButtonTest.java	(revision 17428)
+++ test/unit/org/openstreetmap/josm/gui/bbox/SizeButtonTest.java	(working copy)
@@ -5,25 +5,17 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.Test;
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.gui.bbox.SizeButton.AccessibleSizeButton;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
 /**
  * Unit tests of {@link SizeButton} class.
  */
+@FullPreferences
 class SizeButtonTest {
 
-    /**
-     * Setup tests
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences();
 
     /**
      * Unit test of {@link SizeButton#SizeButton}.
Index: test/unit/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandlerTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandlerTest.java	(revision 17428)
+++ test/unit/org/openstreetmap/josm/gui/datatransfer/OsmTransferHandlerTest.java	(working copy)
@@ -6,7 +6,6 @@
 
 import java.util.Collections;
 
-import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.Test;
 import org.openstreetmap.josm.actions.CopyAction;
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -15,21 +14,17 @@
 import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
+import org.openstreetmap.josm.testutils.annotations.Main;
+import org.openstreetmap.josm.testutils.annotations.Projection;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
 /**
  * Unit tests of {@link OsmTransferHandler} class.
  */
+@FullPreferences
+@Projection
+@Main
 class OsmTransferHandlerTest {
-    /**
-     * Prefs to use OSM primitives
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences().projection().main();
-
     private final OsmTransferHandler transferHandler = new OsmTransferHandler();
 
     /**
Index: test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentExceptionTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentExceptionTest.java	(revision 17428)
+++ test/unit/org/openstreetmap/josm/io/auth/CredentialsAgentExceptionTest.java	(working copy)
@@ -3,25 +3,13 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import org.junit.jupiter.api.extension.RegisterExtension;
 import org.junit.jupiter.api.Test;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
 /**
  * Unit tests of {@link CredentialsAgentException} class.
  */
 class CredentialsAgentExceptionTest {
-
     /**
-     * Setup test
-     */
-    @RegisterExtension
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
-
-    /**
      * Unit test of {@code CredentialsAgentException#CredentialsAgentException}
      */
     @Test
Index: test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java	(revision 17428)
+++ test/unit/org/openstreetmap/josm/testutils/JOSMTestRules.java	(working copy)
@@ -60,6 +60,13 @@
 import org.openstreetmap.josm.io.OsmTransferCanceledException;
 import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.spi.preferences.Setting;
+import org.openstreetmap.josm.testutils.annotations.FullPreferences;
+import org.openstreetmap.josm.testutils.annotations.JosmHome;
+import org.openstreetmap.josm.testutils.annotations.Main;
+import org.openstreetmap.josm.testutils.annotations.OsmApiType;
+import org.openstreetmap.josm.testutils.annotations.Presets;
+import org.openstreetmap.josm.testutils.annotations.Projection;
+import org.openstreetmap.josm.testutils.annotations.ProjectionNadGrids;
 import org.openstreetmap.josm.testutils.mockers.EDTAssertionMocker;
 import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
 import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
@@ -134,7 +141,9 @@
     /**
      * Enable the use of default preferences.
      * @return this instance, for easy chaining
+     * @deprecated Use {@link FullPreferences} instead.
      */
+    @Deprecated
     public JOSMTestRules preferences() {
         josmHome();
         usePreferences = true;
@@ -144,7 +153,9 @@
     /**
      * Set JOSM home to a valid, empty directory.
      * @return this instance, for easy chaining
+     * @deprecated Use {@link JosmHome} instead.
      */
+    @Deprecated
     private JOSMTestRules josmHome() {
         josmHome = new TemporaryFolder();
         return this;
@@ -153,7 +164,9 @@
     /**
      * Enables the i18n module for this test in english.
      * @return this instance, for easy chaining
+     * @deprecated Use {org.openstreetmap.josm.testutils.annotations.I18n} instead.
      */
+    @Deprecated
     public JOSMTestRules i18n() {
         return i18n("en");
     }
@@ -162,7 +175,9 @@
      * Enables the i18n module for this test.
      * @param language The language to use.
      * @return this instance, for easy chaining
+     * @deprecated Use {org.openstreetmap.josm.testutils.annotations.I18n} instead.
      */
+    @Deprecated
     public JOSMTestRules i18n(String language) {
         i18n = language;
         return this;
@@ -172,7 +187,9 @@
      * Mock this test's assumed JOSM version (as reported by {@link Version}).
      * @param revisionProperties mock contents of JOSM's {@code REVISION} properties file
      * @return this instance, for easy chaining
+     * @deprecated Use {@link OverrideAssumeRevision} instead.
      */
+    @Deprecated
     public JOSMTestRules assumeRevision(final String revisionProperties) {
         this.assumeRevisionString = revisionProperties;
         return this;
@@ -181,7 +198,9 @@
     /**
      * Enable the dev.openstreetmap.org API for this test.
      * @return this instance, for easy chaining
+     * @deprecated Use {@link OsmApiType} instead.
      */
+    @Deprecated
     public JOSMTestRules devAPI() {
         preferences();
         useAPI = APIType.DEV;
@@ -191,7 +210,9 @@
     /**
      * Use the {@link FakeOsmApi} for testing.
      * @return this instance, for easy chaining
+     * @deprecated Use {@link OsmApiType} instead.
      */
+    @Deprecated
     public JOSMTestRules fakeAPI() {
         useAPI = APIType.FAKE;
         return this;
@@ -200,7 +221,9 @@
     /**
      * Set up default projection (Mercator)
      * @return this instance, for easy chaining
+     * @deprecated Use {@link Projection} instead.
      */
+    @Deprecated
     public JOSMTestRules projection() {
         useProjection = true;
         return this;
@@ -209,7 +232,9 @@
     /**
      * Set up loading of NTV2 grit shift files to support projections that need them.
      * @return this instance, for easy chaining
+     * @deprecated Use {@link ProjectionNadGrids} instead.
      */
+    @Deprecated
     public JOSMTestRules projectionNadGrids() {
         useProjectionNadGrids = true;
         return this;
@@ -257,7 +282,9 @@
      * Use presets in this test.
      * @return this instance, for easy chaining
      * @since 12568
+     * @deprecated Use {@link Presets} instead.
      */
+    @Deprecated
     public JOSMTestRules presets() {
         preferences();
         usePresets = true;
@@ -268,7 +295,9 @@
      * Use boundaries dataset in this test.
      * @return this instance, for easy chaining
      * @since 12545
+     * @deprecated Use {@link org.openstreetmap.josm.testutils.annotations.Territories} instead.
      */
+    @Deprecated
     public JOSMTestRules territories() {
         territories = true;
         return this;
@@ -351,7 +380,9 @@
      *         global variables in this test.
      * @return this instance, for easy chaining
      * @since 12557
+     * @deprecated Use {@link Main} instead
      */
+    @Deprecated
     public JOSMTestRules main() {
         return this.main(
             WindowlessMapViewStateMocker::new,
@@ -368,7 +399,9 @@
      *        of {@link org.openstreetmap.josm.gui.NavigatableComponent}, null to skip.
      *
      * @return this instance, for easy chaining
+     * @deprecated Use {@link Main} instead
      */
+    @Deprecated
     public JOSMTestRules main(
         final Runnable mapViewStateMockingRunnable,
         final Runnable navigableComponentMockingRunnable
@@ -397,8 +430,11 @@
         return this;
     }
 
-    private static class MockVersion extends Version {
-        MockVersion(final String propertiesString) {
+    /*
+     * Mock the JOSM version. This should only be used in JOSM Core test extensions.
+     */
+    public static class MockVersion extends Version {
+        public MockVersion(final String propertiesString) {
             super.initFromRevisionInfo(
                 new ByteArrayInputStream(propertiesString.getBytes(StandardCharsets.UTF_8))
             );
@@ -409,7 +445,7 @@
     public Statement apply(Statement base, Description description) {
         // First process any Override* annotations for per-test overrides.
         // The following only work because "option" methods modify JOSMTestRules in-place
-        final OverrideAssumeRevision overrideAssumeRevision = description.getAnnotation(OverrideAssumeRevision.class);
+        OverrideAssumeRevision overrideAssumeRevision = description.getAnnotation(OverrideAssumeRevision.class);
         if (overrideAssumeRevision != null) {
             this.assumeRevision(overrideAssumeRevision.value());
         }
@@ -790,10 +826,13 @@
     /**
      * Override this test's assumed JOSM version (as reported by {@link Version}).
      * @see JOSMTestRules#assumeRevision(String)
+     * @deprecated Use {@link org.openstreetmap.josm.testutils.annotations.AssumeRevision}
+     *             when using JUnit 5 Extensions.
      */
     @Documented
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.METHOD)
+    @Deprecated
     public @interface OverrideAssumeRevision {
         /**
          * Returns overridden assumed JOSM version.
Index: test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/AssumeRevision.java	(working copy)
@@ -0,0 +1,67 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+
+/**
+ * Override this test's assumed JOSM version (as reported by {@link Version}).
+ * @author Taylor Smock
+ * @see JOSMTestRules#assumeRevision(String)
+ * @since xxx
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+@ExtendWith(AssumeRevision.AssumeRevisionExtension.class)
+public @interface AssumeRevision {
+    /**
+     * Returns overridden assumed JOSM version.
+     * @return overridden assumed JOSM version
+     */
+    String value();
+
+    /**
+     * Override the JOSM revision information. Use {@link AssumeRevision} instead of directly using this extension.
+     * @author Taylor Smock
+     * @since xxx
+     */
+    class AssumeRevisionExtension implements BeforeEachCallback, AfterEachCallback {
+
+        @Override
+        public void afterEach(ExtensionContext context) throws Exception {
+            Store store = context.getStore(Namespace.create(AssumeRevisionExtension.class));
+            String originalVersion = store.getOrDefault(store, String.class, null);
+            if (originalVersion != null) {
+                TestUtils.setPrivateStaticField(Version.class, "instance", originalVersion);
+            }
+        }
+
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            Optional<AssumeRevision> annotation = AnnotationSupport.findAnnotation(context.getElement(), AssumeRevision.class);
+            if (annotation.isPresent()) {
+                Store store = context.getStore(Namespace.create(AssumeRevisionExtension.class));
+                store.put("originalVersion", Version.getInstance());
+                final Version replacementVersion = new JOSMTestRules.MockVersion(annotation.get().value());
+                TestUtils.setPrivateStaticField(Version.class, "instance", replacementVersion);
+            }
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/BasicPreferences.java	(working copy)
@@ -0,0 +1,49 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
+import org.openstreetmap.josm.data.preferences.JosmUrls;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Allow tests to use JOSM preferences (see {@link JOSMTestRules#preferences()})
+ * @author Taylor Smock
+ * @see FullPreferences
+ * @since xxx
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(BasicPreferences.BasicPreferencesExtension.class)
+public @interface BasicPreferences {
+
+    /**
+     * Initialize basic preferences. This is often more than enough for basic tests.
+     * @author Taylor Smock
+     *
+     */
+    class BasicPreferencesExtension implements BeforeAllCallback {
+
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            Preferences pref = Preferences.main();
+            Config.setPreferencesInstance(pref);
+            Config.setBaseDirectoriesProvider(JosmBaseDirectories.getInstance());
+            Config.setUrlsProvider(JosmUrls.getInstance());
+            context.getStore(Namespace.create(BasicPreferencesExtension.class)).put("preferences", pref);
+        }
+
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/FullPreferences.java	(working copy)
@@ -0,0 +1,53 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Map;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.spi.preferences.Setting;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.annotations.BasicPreferences.BasicPreferencesExtension;
+
+/**
+ * Allow tests to use JOSM preferences (see {@link JOSMTestRules#preferences()})
+ * @author Taylor Smock
+ * @see BasicPreferences (often enough for simple tests).
+ * @since xxx
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@JosmHome
+@BasicPreferences
+@ExtendWith(FullPreferences.UsePreferencesExtension.class)
+public @interface FullPreferences {
+    /**
+     * Initialize preferences.
+     */
+    class UsePreferencesExtension implements BeforeEachCallback {
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            Preferences pref = context.getStore(Namespace.create(BasicPreferencesExtension.class)).get("preferences", Preferences.class);
+            @SuppressWarnings("unchecked")
+            final Map<String, Setting<?>> defaultsMap = (Map<String, Setting<?>>) TestUtils.getPrivateField(pref, "defaultsMap");
+            defaultsMap.clear();
+            pref.resetToInitialState();
+            pref.enableSaveOnPut(false);
+            // No pref init -> that would only create the preferences file.
+            // We force the use of a wrong API server, just in case anyone attempts an upload
+            Config.getPref().put("osm-server.url", "http://invalid");
+        }
+
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/I18n.java	(working copy)
@@ -0,0 +1,51 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Enables the i18n module for this test.
+ * @author Taylor Smock
+ * @see JOSMTestRules#i18n(String)
+ *
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(I18n.I18nExtension.class)
+public @interface I18n {
+    /**
+     * Get the language to use for i18n
+     * @return The language (default "en").
+     */
+    String language() default "en";
+
+    /**
+     * Enables the i18n module for this test.
+     * @author Taylor Smock
+     * @see JOSMTestRules#i18n(String)
+     *
+     */
+    class I18nExtension implements BeforeAllCallback {
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            Optional<I18n> annotation = AnnotationSupport.findAnnotation(context.getElement(), I18n.class);
+            String language = "en";
+            if (annotation.isPresent()) {
+                language = annotation.get().language();
+            }
+            org.openstreetmap.josm.tools.I18n.set(language);
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/JosmHome.java	(working copy)
@@ -0,0 +1,70 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.UUID;
+
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.openstreetmap.josm.data.preferences.JosmBaseDirectories;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Use the JOSM home directory. See {@link JOSMTestRules}.
+ * Typically only used by {@link FullPreferences}.
+ *
+ * @author Taylor Smock
+ *
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(JosmHome.JosmHomeExtension.class)
+public @interface JosmHome {
+    /**
+     * Create a JOSM home directory. Prefer using {@link JosmHome}.
+     * @author Taylor Smock
+     */
+    class JosmHomeExtension implements BeforeAllCallback, AfterAllCallback {
+        @Override
+        public void afterAll(ExtensionContext context) throws Exception {
+            Path tempDir = context.getStore(Namespace.create(JosmHome.class)).get("home", Path.class);
+            Files.walkFileTree(tempDir, new SimpleFileVisitor<Path>() {
+                @Override
+                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+                    Files.delete(dir);
+                    return FileVisitResult.CONTINUE;
+                }
+
+                @Override
+                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                    Files.delete(file);
+                    return FileVisitResult.CONTINUE;
+                }
+            });
+        }
+
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            Path tempDir = Files.createTempDirectory(UUID.randomUUID().toString());
+            context.getStore(Namespace.create(JosmHome.class)).put("home", tempDir);
+            File home = tempDir.toFile();
+            System.setProperty("josm.home", home.getAbsolutePath());
+            JosmBaseDirectories.getInstance().clearMemos();
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/Main.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/Main.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/Main.java	(working copy)
@@ -0,0 +1,75 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.mockers.WindowlessMapViewStateMocker;
+import org.openstreetmap.josm.testutils.mockers.WindowlessNavigatableComponentMocker;
+
+/**
+ * Use the {@link MainApplication#main}, {@code Main.contentPanePrivate}, {@code Main.mainPanel}, global variables in this test.
+ * @author Taylor Smock
+ * @see JOSMTestRules#main()
+ * @since xxx
+ */
+@Documented
+@Retention(RUNTIME)
+@Target(TYPE)
+@ExtendWith(Main.MainExtension.class)
+public @interface Main {
+    /**
+     * Get the class to use as the mocker for the map view
+     * @return The mocker class for the map view
+     */
+    Class<?> mapViewStateMocker() default WindowlessMapViewStateMocker.class;
+    /**
+     * Get the class to use for the navigable component
+     * @return The class to use for the navigable component.
+     */
+    Class<?> navigableComponentMocker() default WindowlessNavigatableComponentMocker.class;
+
+    /**
+     * Initialize the MainApplication
+     * @author Taylor Smock
+     */
+    class MainExtension implements BeforeAllCallback {
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            Optional<Main> annotation = AnnotationSupport.findAnnotation(context.getElement(), Main.class);
+            Class<?> mapViewStateMocker = null;
+            Class<?> navigableComponentMocker = null;
+            if (annotation.isPresent()) {
+                mapViewStateMocker = annotation.get().mapViewStateMocker();
+                navigableComponentMocker = annotation.get().navigableComponentMocker();
+            }
+
+            // apply mockers to MapViewState and NavigableComponent whether we're headless or not
+            // as we generally don't create the josm main window even in non-headless mode.
+            if (mapViewStateMocker != null) {
+                mapViewStateMocker.getConstructor().newInstance();
+            }
+            if (navigableComponentMocker != null) {
+                navigableComponentMocker.getConstructor().newInstance();
+            }
+
+            new MainApplication();
+            JOSMFixture.initContentPane();
+            JOSMFixture.initMainPanel(true);
+            JOSMFixture.initToolbar();
+            JOSMFixture.initMainMenu();
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/OsmApiType.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/OsmApiType.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/OsmApiType.java	(working copy)
@@ -0,0 +1,86 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.io.OsmApi;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.testutils.FakeOsmApi;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Specify the OSM API to use for the test. {@link APIType#NONE} has no effect.
+ *
+ * @author Taylor Smock
+ * @see JOSMTestRules#devAPI()
+ * @see JOSMTestRules#fakeAPI()
+ * @since xxx
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({ TYPE, METHOD })
+@FullPreferences
+@ExtendWith(OsmApiType.OsmApiTypeExtension.class)
+public @interface OsmApiType {
+    /**
+     * API types to initialize
+     * @author Taylor Smock
+     *
+     */
+    enum APIType {
+        NONE, FAKE, DEV;
+    }
+
+    /**
+     * The API type to use
+     * @return The API type to use (default NONE)
+     */
+    APIType apiType() default APIType.NONE;
+
+    /**
+     * Initialize the OSM api
+     * @author Taylor Smock
+     *
+     */
+    class OsmApiTypeExtension implements BeforeAllCallback, BeforeEachCallback {
+
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            Optional<OsmApiType> annotation = AnnotationSupport.findAnnotation(context.getElement(), OsmApiType.class);
+            APIType useAPI = APIType.NONE;
+            if (annotation.isPresent()) {
+                useAPI = annotation.get().apiType();
+            }
+            // Set API
+            if (useAPI == APIType.DEV) {
+                Config.getPref().put("osm-server.url", "https://api06.dev.openstreetmap.org/api");
+            } else if (useAPI == APIType.FAKE) {
+                FakeOsmApi api = FakeOsmApi.getInstance();
+                Config.getPref().put("osm-server.url", api.getServerUrl());
+            }
+
+            // Initialize API
+            if (useAPI != APIType.NONE) {
+                OsmApi.getOsmApi().initialize(null);
+            }
+        }
+
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            beforeEach(context);
+        }
+
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/Presets.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/Presets.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/Presets.java	(working copy)
@@ -0,0 +1,48 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Use presets in this test.
+ *
+ * @author Taylor Smock
+ * @see JOSMTestRules#presets()
+ * @since xxx
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({ TYPE, METHOD })
+@ExtendWith(Presets.PresetsExtension.class)
+public @interface Presets {
+    /**
+     * Initialize the presets
+     * @author Taylor Smock
+     *
+     */
+    class PresetsExtension implements BeforeAllCallback, BeforeEachCallback {
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            TaggingPresets.readFromPreferences();
+        }
+
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            // TODO only run if method level
+            beforeAll(context);
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/Projection.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/Projection.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/Projection.java	(working copy)
@@ -0,0 +1,53 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
+import org.openstreetmap.josm.data.projection.Projections;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Use projections in tests (Mercator).
+ * @author Taylor Smock
+ * @see JOSMTestRules#projection()
+ *
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+@ExtendWith(Projection.ProjectionExtension.class)
+public @interface Projection {
+    /**
+     * The value to use for the projection. Defaults to EPSG:3857 (Mercator).
+     * @return The value to use to get the projection from {@link Projections#getProjectionByCode}.
+     */
+    String projectionCode() default "EPSG:3857";
+
+    /**
+     * Use projections in tests. Use {@link Projection} preferentially.
+     * @author Taylor Smock
+     *
+     */
+    class ProjectionExtension implements BeforeEachCallback {
+        @Override
+        public void beforeEach(ExtensionContext context) throws Exception {
+            Optional<Projection> annotation = AnnotationSupport.findAnnotation(context.getElement(), Projection.class);
+            if (annotation.isPresent()) {
+                ProjectionRegistry.setProjection(Projections.getProjectionByCode(annotation.get().projectionCode()));
+            } else {
+                ProjectionRegistry.setProjection(Projections.getProjectionByCode("EPSG:3857")); // Mercator
+            }
+        }
+
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/ProjectionNadGrids.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/ProjectionNadGrids.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/ProjectionNadGrids.java	(working copy)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Set up loading of NTV2 grit shift files to support projections that need them.
+ * @author Taylor Smock
+ * @see JOSMTestRules#projectionNadGrids()
+ * @since xxx
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(ProjectionNadGrids.ProjectionNadGridsExtension.class)
+public @interface ProjectionNadGrids {
+    /**
+     * Set up loading of NTV2 grit shift files to support projections that need them.
+     * Use {@link ProjectionNadGrids} instead.
+     * @author Taylor Smock
+     * @see JOSMTestRules#projectionNadGrids()
+     */
+    class ProjectionNadGridsExtension implements BeforeAllCallback {
+        @Override
+        public void beforeAll(ExtensionContext context) throws Exception {
+            MainApplication.setupNadGridSources();
+        }
+    }
+}
Index: test/unit/org/openstreetmap/josm/testutils/annotations/Territories.java
===================================================================
--- test/unit/org/openstreetmap/josm/testutils/annotations/Territories.java	(nonexistent)
+++ test/unit/org/openstreetmap/josm/testutils/annotations/Territories.java	(working copy)
@@ -0,0 +1,38 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.testutils.annotations;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+/**
+ * Use boundaries dataset in this test.
+ * @see JOSMTestRules#territories()
+ * @author Taylor Smock
+ * @since xxx
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@ExtendWith(Territories.TerritoriesExtension.class)
+public @interface Territories {
+    /**
+     * Initialize boundaries prior to use
+     * @author Taylor Smock
+     *
+     */
+    class TerritoriesExtension implements BeforeAllCallback {
+        @Override
+        public void beforeAll(ExtensionContext arg0) throws Exception {
+            org.openstreetmap.josm.tools.Territories.initializeInternalData();
+        }
+
+    }
+}
