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.

pom.xml

<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.

src/test/de/metamorphant/examples/ParameterizedRegexTest.java

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.

src/test/de/metamorphant/examples/ParameterizedRegexTest.java

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);
  }
}




Contact us