diff --git a/data/tagchecker.cfg b/data/tagchecker.cfg
index dcb3fe9..397a617 100644
--- a/data/tagchecker.cfg
+++ b/data/tagchecker.cfg
@@ -35,6 +35,12 @@
 # The comment at the end of a rule is displayed in validator description
 #
 # Empty lines and space signs are ignored
+#
+# Lines starting with #TEST# add unit tests (cf. TagCheckerTest)
+# Example: "#TEST# w+layer=1 # w-layer=10"
+# Syntax:  "#TEST# [nwr][+-][key]=[value] # ..."
+#                  [nwr] whether to test a node/way/relation
+                        [+-] whether test is expected to succeed/fail
 
 way  : W : highway == * && name == /.* (Ave|Blvd|Cct|Cir|Cl|Cr|Crct|Cres|Crt|Ct|Dr|Drv|Esp|Espl|Hwy|Ln|Mw|Mwy|Pl|Rd|Qy|Qys|Sq|St|Str|Ter|Tce|Tr|Wy)\.?$/i               # abbreviated street name
 
@@ -191,12 +197,14 @@ way  : W : junction             == * && highway != *
 
 # measurement values and units warnings (ticket #8687)
 way : W : layer == * && layer != /^0$|^-?[1-5]$/                                                                         # layer should be between -5 and 5
+#TEST# w+layer=1 # w+layer=5 # w+layer=-5 # w-layer=10
 *   : W : level == * && level != /^((([0-9]|-[1-9])|[1-9][0-9]*)(\.5)?)(;(([0-9]|-[1-9])|[1-9][0-9]*)(\.5)?)*$|^-0\.5$/  # level should be numbers with optional .5 increments
 *   : W : height == * && height != /^(([0-9]+\.?[0-9]*( m)?)|([1-9][0-9]*\'((10|11|[0-9])((\.[0-9]+)?)\")?))$/           # height: meters is default; period is separator; if units, put space then unit
 
 *   : W : maxheight == * && maxheight != /^(([1-9][0-9]*(\.[0-9]+)?( m)?)|([0-9]+\'([0-9]|10|11)(\.[0-9]*)?\"))$/        # maxheight: meters is default; period is separator; if units, put space then unit
 way : W : width == * && width != /^(([0-9]+\.?[0-9]*( [a-z]+)?)|([0-9]+\'[0-9]+\.?[0-9]*\"))$/                           # width: meters is default; period is separator; if units, put space then unit
 *   : W : maxwidth == * && maxwidth != /^(([0-9]+\.?[0-9]*( m)?)|([0-9]+\'[0-9]+\.?[0-9]*\"))$/                          # maxwidth: meters is default; period is separator; if units, put space then unit
+#TEST# w+maxwidth=2 # w+maxwidth=2 m # w-maxwidth=2m # w+maxwidth=2.5 # w-maxwidth=2,5
 way : W : maxspeed == * && maxspeed != /^(signals|none|unposted|unknown|variable|walk|[1-9][0-9]*( [a-z]+)?|[A-Z][A-Z]:(urban|rural|living_street|motorway))$/  # unusual maxspeed format
 way : W : voltage == * && voltage == /(.*[A-Za-z].*)|.*,.*|.*( ).*/                                                      # voltage should be in volts with no units/delimiter/spaces
 # some users are using frequency for other purposes (not electromagnetic) with the values 'perennial' and 'intermittent'; the vast majority are 0, 16.7, 50 and 60
diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/TagCheckerTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/TagCheckerTest.java
new file mode 100644
index 0000000..a14a32a
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/data/validation/tests/TagCheckerTest.java
@@ -0,0 +1,66 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import org.hamcrest.core.Is;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.Way;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Collections;
+
+public class TagCheckerTest {
+
+    @Before
+    public void setUp() throws Exception {
+        Main.pref = new Preferences();
+        new TagChecker().initialize();
+    }
+
+    @Test
+    public void testTests() throws IOException {
+        final BufferedReader reader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream(
+                TagChecker.DATA_FILE.substring("resource:/".length())), "UTF-8"));
+        String line;
+        while ((line = reader.readLine()) != null) {
+            if (line.startsWith("#TEST#")) {
+                for (final String test : line.substring("#TEST#".length()).split("\\s*#\\s*")) {
+                    final String[] t = test.trim().split("=", 2);
+                    if (t.length != 2) continue;
+                    final String key = t[0].substring(2);
+                    final String value = t[1];
+                    final boolean expected = t[0].charAt(1) == '+';
+                    final OsmPrimitive p = t[0].charAt(0) == 'n'
+                            ? new Node()
+                            :t[0].charAt(0) == 'w'
+                            ? new Way()
+                            : t[0].charAt(0) == 'r'
+                            ? new Relation()
+                            : null;
+                    final boolean actual = matches(p, key, value);
+                    System.out.println(test.trim() + " yields " + actual + ", expecting " + actual);
+                    Assert.assertThat(test.trim(), actual, Is.is(expected));
+                }
+            }
+        }
+    }
+
+    protected boolean matches(final OsmPrimitive p, final String key, final String value) {
+        boolean matchAll = true;
+        for (final TagChecker.CheckerData check : TagChecker.checkerData) {
+            if (check.match(p, Collections.singletonMap(key, value))) {
+                matchAll = false;
+                break;
+            }
+        }
+        return matchAll;
+    }
+}
