Index: trunk/test/unit/org/openstreetmap/josm/gui/preferences/imagery/ImageryPreferenceTestIT.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/gui/preferences/imagery/ImageryPreferenceTestIT.java	(revision 14604)
+++ trunk/test/unit/org/openstreetmap/josm/gui/preferences/imagery/ImageryPreferenceTestIT.java	(revision 14605)
@@ -14,14 +14,20 @@
 import java.util.Locale;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.TreeMap;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
 
 import javax.imageio.ImageIO;
 
 import org.apache.commons.jcs.access.CacheAccess;
-import org.junit.Before;
-import org.junit.Rule;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
 import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.model.Statement;
 import org.openstreetmap.gui.jmapviewer.Coordinate;
 import org.openstreetmap.gui.jmapviewer.TileXY;
@@ -53,4 +59,5 @@
 import org.openstreetmap.josm.io.imagery.WMSImagery.WMSGetCapabilitiesException;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.ParallelParameterized;
 import org.openstreetmap.josm.tools.HttpClient;
 import org.openstreetmap.josm.tools.HttpClient.Response;
@@ -63,4 +70,5 @@
  * Integration tests of {@link ImageryPreference} class.
  */
+@RunWith(ParallelParameterized.class)
 public class ImageryPreferenceTestIT {
 
@@ -71,14 +79,29 @@
      * Setup rule
      */
-    @Rule
+    @ClassRule
     @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().https().preferences().projection().projectionNadGrids()
+    public static JOSMTestRules test = new JOSMTestRules().https().preferences().projection().projectionNadGrids()
                                                    .timeout((int) TimeUnit.MINUTES.toMillis(40));
 
+    static {
+        try {
+            test.apply(new Statement() {
+                @Override
+                public void evaluate() throws Throwable {
+                    // Do nothing. Hack needed because @Parameters are computed before anything else
+                }
+            }, Description.createSuiteDescription(ImageryPreferenceTestIT.class)).evaluate();
+        } catch (Throwable e) {
+            Logging.error(e);
+        }
+    }
+
+    /** Entry to test */
+    private final ImageryInfo info;
     private final Map<String, Map<ImageryInfo, List<String>>> errors = Collections.synchronizedMap(new TreeMap<>());
-    private final Map<String, byte[]> workingURLs = Collections.synchronizedMap(new TreeMap<>());
-
-    private TMSCachedTileLoaderJob helper;
-    private List<String> ignoredErrors;
+    private static final Map<String, byte[]> workingURLs = Collections.synchronizedMap(new TreeMap<>());
+
+    private static TMSCachedTileLoaderJob helper;
+    private static List<String> ignoredErrors;
 
     /**
@@ -86,8 +109,29 @@
      * @throws IOException in case of I/O error
      */
-    @Before
-    public void before() throws IOException {
+    @BeforeClass
+    public static void beforeClass() throws IOException {
         helper = new TMSCachedTileLoaderJob(null, null, new CacheAccess<>(null), new TileJobOptions(0, 0, null, 0), null);
         ignoredErrors = TestUtils.getIgnoredErrorMessages(ImageryPreferenceTestIT.class);
+    }
+
+    /**
+     * Returns list of imagery entries to test.
+     * @return list of imagery entries to test
+     */
+    @Parameters(name = "{0}")
+    public static List<Object[]> data() {
+        ImageryLayerInfo.instance.load(false);
+        return ImageryLayerInfo.instance.getDefaultLayers()
+                .stream().map(x -> new Object[] {x.getId(), x})
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * Constructs a new {@code ImageryPreferenceTestIT} instance.
+     * @param id entry ID, used only to name tests
+     * @param info entry to test
+     */
+    public ImageryPreferenceTestIT(String id, ImageryInfo info) {
+        this.info = Objects.requireNonNull(info);
     }
 
@@ -336,11 +380,9 @@
 
     /**
-     * Test that available imagery entries are valid.
-     * @throws Exception in case of error
+     * Test that available imagery entry is valid.
      */
     @Test
-    public void testValidityOfAvailableImageryEntries() throws Exception {
-        ImageryLayerInfo.instance.load(false);
-        ImageryLayerInfo.instance.getDefaultLayers().parallelStream().forEach(this::checkEntry);
+    public void testValidityOfAvailableImageryEntry() {
+        checkEntry(info);
         assertTrue(errors.toString().replaceAll("\\}, ", "\n\\}, ").replaceAll(", ImageryInfo\\{", "\n      ,ImageryInfo\\{"),
                 errors.isEmpty());
Index: trunk/test/unit/org/openstreetmap/josm/testutils/ParallelParameterized.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/ParallelParameterized.java	(revision 14605)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/ParallelParameterized.java	(revision 14605)
@@ -0,0 +1,39 @@
+/**
+ * Copyright 2012-2017 Michael Tamm and other junit-toolbox contributors
+ *
+ * Licensed 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.openstreetmap.josm.testutils;
+
+import org.junit.runners.Parameterized;
+
+/**
+ * An extension of the JUnit {@link Parameterized} runner,
+ * which executes the tests for each parameter set concurrently.
+ * <p>The maximum number of test threads will be the number of
+ * {@link Runtime#availableProcessors() available processors}.
+ *
+ * @author Michael Tamm (junit-toolbox)
+ */
+public class ParallelParameterized extends Parameterized {
+
+    /**
+     * Constructs a new {@code ParallelParameterized}.
+     * @param klass the root of the suite
+     * @throws Throwable in case of error
+     */
+    public ParallelParameterized(Class<?> klass) throws Throwable {
+        super(klass);
+        setScheduler(new ParallelScheduler());
+    }
+}
Index: trunk/test/unit/org/openstreetmap/josm/testutils/ParallelScheduler.java
===================================================================
--- trunk/test/unit/org/openstreetmap/josm/testutils/ParallelScheduler.java	(revision 14605)
+++ trunk/test/unit/org/openstreetmap/josm/testutils/ParallelScheduler.java	(revision 14605)
@@ -0,0 +1,107 @@
+/**
+ * Copyright 2012-2017 Michael Tamm and other junit-toolbox contributors
+ *
+ * Licensed 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.openstreetmap.josm.testutils;
+
+import static java.util.concurrent.ForkJoinTask.inForkJoinPool;
+
+import java.util.Deque;
+import java.util.LinkedList;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinTask;
+import java.util.concurrent.ForkJoinWorkerThread;
+
+import org.junit.runners.model.RunnerScheduler;
+
+/**
+ * Encapsulates the singleton {@link ForkJoinPool} used by {@link ParallelParameterized}
+ * to execute test classes and test methods concurrently.
+ *
+ * @author Michael Tamm (junit-toolbox)
+ */
+class ParallelScheduler implements RunnerScheduler {
+
+    static ForkJoinPool forkJoinPool = setUpForkJoinPool();
+
+    static ForkJoinPool setUpForkJoinPool() {
+        Runtime runtime = Runtime.getRuntime();
+        int numThreads = Math.max(2, runtime.availableProcessors());
+        ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = pool -> {
+            if (pool.getPoolSize() >= pool.getParallelism()) {
+                return null;
+            } else {
+                ForkJoinWorkerThread thread = ForkJoinPool.defaultForkJoinWorkerThreadFactory.newThread(pool);
+                thread.setName("JUnit-" + thread.getName());
+                return thread;
+            }
+        };
+        return new ForkJoinPool(numThreads, threadFactory, null, false);
+    }
+
+    private final Deque<ForkJoinTask<?>> _asyncTasks = new LinkedList<>();
+    private Runnable _lastScheduledChild;
+
+    @Override
+    public void schedule(Runnable childStatement) {
+        if (_lastScheduledChild != null) {
+            // Execute previously scheduled child asynchronously ...
+            if (inForkJoinPool()) {
+                _asyncTasks.addFirst(ForkJoinTask.adapt(_lastScheduledChild).fork());
+            } else {
+                _asyncTasks.addFirst(forkJoinPool.submit(_lastScheduledChild));
+            }
+        }
+        // Note: We don't schedule the childStatement immediately here,
+        // but remember it, so that we can synchronously execute the
+        // last scheduled child in the finished method() -- this way,
+        // the current thread does not immediately call join() in the
+        // finished() method, which might block it ...
+        _lastScheduledChild = childStatement;
+    }
+
+    @Override
+    public void finished() {
+        RuntimeException me = new RuntimeException();
+        if (_lastScheduledChild != null) {
+            if (inForkJoinPool()) {
+                // Execute the last scheduled child in the current thread ...
+                try {
+                    _lastScheduledChild.run();
+                } catch (Throwable t) {
+                    me.addSuppressed(t);
+                }
+            } else {
+                // Submit the last scheduled child to the ForkJoinPool too,
+                // because all tests should run in the worker threads ...
+                _asyncTasks.addFirst(forkJoinPool.submit(_lastScheduledChild));
+            }
+            // Make sure all asynchronously executed children are done, before we return ...
+            for (ForkJoinTask<?> task : _asyncTasks) {
+                // Note: Because we have added all tasks via addFirst into _asyncTasks,
+                // task.join() is able to steal tasks from other worker threads,
+                // if there are tasks, which have not been started yet ...
+                // from other worker threads ...
+                try {
+                    task.join();
+                } catch (Throwable t) {
+                    me.addSuppressed(t);
+                }
+            }
+            if (me.getSuppressed().length > 0) {
+                throw me;
+            }
+        }
+    }
+}
