- tl;dr
- Background
- Step 1: Create your setup with an empty test and regex
- Step 2a: Create failing first test
- Step 2b: Fix it in the simple-most fashion
- Steps 3–∞: Iterate
- End Result: Proven documentation
tl;dr
We show how to develop a regular expression driven by automated tests. Download setup from github.com/JohannesFKnauf/regex-tdd-parameterized-junit4-example and start playing.
git clone https://github.com/JohannesFKnauf/regex-tdd-parameterized-junit4-example
mvn clean test
Background
Regular Expressions. We all love them for the power and hate them for the obfuscation level.
This dialog is remotely based on a true story. All names have been anonymized, to protect the innocent.
Colleague from another team: “What is wrong with this Java regex for parsing a special floating point number expression? A team member wrote it and left for good. It does not work. I need to create it fresh.”
—Me: “Hard to say without knowing what you exactly need. Do you have tests?”
Colleague: “Are you kidding? Of course not.”
—Me: “Documentation?”
Colleague (bursts into laughing): “Clown g’frühstückt? Of course not.”
—Me: “Examples?”
Colleague: “Yeah, that at least. Historical samples from real executions.”
—Me: “Could be worse. Let’s create a test setup.”
Colleague: “Test setup? That sounds hard! I do not have time for this.”
—Me: “But you have time to fix the unavoidable mistakes in PROD? Think again. It really is NOT that hard. In such a simple case, you will be more productive with TDD even in the short term. Any constraints for creating the test setup?”
Colleague: “Java 8, JUnit 4, Maven.”
—Me: “Not exactly my weapons of choice, but well… will do. Let’s get ready to rumble!”
Step 1: Create your setup with an empty test and regex
We use maven as a slim wrapper for pulling the right version of junit, building and running the tests.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.metamorphant.examples</groupId>
<artifactId>junit-parameterized-regex-testing-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Run and confirm: It’s green.
mvn clean test
Step 2a: Create failing first test
We simplify here, by using just 1 single JUnit test file as
- test driver,
- host of the Regex implementation and
- place for the example definitions.
If you are in a true Java project developing more than a single regex, you will want to wrap it and separate it from the tests, of course.
package de.metamorphant.examples;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class ParameterizedRegexTest {
private static final String REGEX = "";
@Parameters(name = "{index} {2}: is {0} well-formed? {1}")
public static Collection<Object[]> examples() {
return Arrays.asList(new Object[][] {
{"", false, "empty string"}
});
}
@Parameter(0)
public String input;
@Parameter(1)
public boolean isMatchExpected;
@Parameter(2)
public String description;
@Test
public void regexTest() {
Boolean matches = input.matches(REGEX);
assertEquals(isMatchExpected, matches);
}
}
Run tests. Red!
Step 2b: Fix it in the simple-most fashion
private static final String REGEX = ".";
Run tests. Green!
Steps 3–∞: Iterate
Here I give a transcript of the remaining session. We skip some steps in between to shorten it.
Iteration 1
public static Collection<Object[]> examples() {
return Arrays.asList(new Object[][] {
{"", false, "empty string"},
{"a", false, "single non-digit"},
{"1", true, "single digit"}
});
}
private static final String REGEX = "\\d";
Iteration 2
public static Collection<Object[]> examples() {
...
{"123", true, "integer"}
...
}
private static final String REGEX = "\\d+";
Iteration 3
public static Collection<Object[]> examples() {
...
{"-123", true, "integer, negative sign"},
{"+123", true, "integer, positive sign"}
...
}
private static final String REGEX = "[-+]?\\d+";
Iteration 4
public static Collection<Object[]> examples() {
...
{"123.12", true, "float"},
...
}
private static final String REGEX = "[-+]?\\d+(\\.\\d+)?";
Iteration 5
public static Collection<Object[]> examples() {
...
{"123.12e", false, "float with exponent extension but no value"},
{"123.12e12", true, "float with exponent"},
{"123.12E12", true, "float with uppercase exponent"},
{"123.12e12.12", false, "float with non-integer exponent"},
{"123.12e+12", true, "float with exponent, positive sign"},
{"123.12e-12", true, "float with exponent, negative sign"}
...
}
private static final String REGEX = "[-+]?\\d+(\\.\\d+)?([eE][-+]?\\d+)?";
End Result: Proven documentation
Now we have a piece of documentation with true discriminative power. <This> is intended behaviour. <That> is not. All future developers touching the code will thank you – including yourself.
package de.metamorphant.examples;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class ParameterizedRegexTest {
private static final String REGEX = "[-+]?\\d+(\\.\\d+)?([eE][-+]?\\d+)?";
@Parameters(name = "{index} {2}: is {0} well-formed? {1}")
public static Collection<Object[]> examples() {
return Arrays.asList(new Object[][] {
{"", false, "empty string"},
{"a", false, "single non-digit"},
{"1", true, "single digit"},
{"123", true, "integer"},
{"-123", true, "integer, negative sign"},
{"+123", true, "integer, positive sign"},
{"123.12", true, "float"},
{"123.12e", false, "float with exponent extension but no value"},
{"123.12e12", true, "float with exponent"},
{"123.12E12", true, "float with uppercase exponent"},
{"123.12e12.12", false, "float with non-integer exponent"},
{"123.12e+12", true, "float with exponent, positive sign"},
{"123.12e-12", true, "float with exponent, negative sign"},
});
}
@Parameter(0)
public String input;
@Parameter(1)
public boolean isMatchExpected;
@Parameter(2)
public String description;
@Test
public void regexTest() {
Boolean matches = input.matches(REGEX);
assertEquals(isMatchExpected, matches);
}
}
Post header background image by nick_photoarchive from Pixabay.