Test for private constructor to get full code coverage

Imagine we have a simple helper class which only consists of public static methods.

public class Helper {
  public static void doSomething() {}
}

And the test:

import org.junit.Test;

public class HelperTest {
 @Test
 public void testDoSomething() {
   Helper.doSomething();
 }
}

Our code coverage tool won’t show us a 100% coverage, because the (default) constructor is not invoked. For example Intellij IDEA only shows 50%.

One solution would be to simply invoke the default constructor with new Helper.doSomething(). But we don’t want to have objects of this class and therefore override the default constructor with a private one:

public class Helper {
    private Helper() {}
    public static void doSomething() {}
}

The problem is, that now even the test cannot instantiate the class. And as we are testdriven, the question is: Should the decision to create a class without any objects be motivated by tests, too? In my opinion yes. I don’t want some other programmer to remove the private constructor. At least a test shall break.

We reach this by the following test:

import static org.junit.Assert.assertTrue;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import org.junit.Test;

public class HelperTest {

    @Test
    public void testDoSomething() throws Exception {
        Helper.doSomething();
    }

    @Test
    public void testPrivateConstructor() throws Exception {
        Constructor constructor = Helper.class.getDeclaredConstructor();
        assertTrue("Constructor is not private", Modifier.isPrivate(constructor.getModifiers()));

        constructor.setAccessible(true);
        constructor.newInstance();
    }
}

Now we have a guard to the design decision of a private constructor and our tests are back on 100% coverage again.

Find all JUnit tests in a project

In einem Kundenprojekt haben wir verschiedene Kategorien von Tests eingeführt: Unit-Tests, Integration-Tests und Selenium-Tests. Da die Tests unterschiedliche Konfigurationen erfordern (z. B. Selenium Server starten) und auch die Testausführung unterschiedlich lang ist, wollten wir diese Tests in Testsuiten von JUnit aufnehmen.

Testsuiten haben einige Vorteile, so lässt sich die Testreihenfolge festlegen (auch wenn Tests prinzipiell unabhängig voneinander sein sollten, kann es bspw. dazu genutzt werden Testdaten in das System einzufahren). Ein weiterer Vorteil besteht darin, dass Testsuiten von allen IDEs und Buildsystemem gestartet werden können. IntelliJ IDEA erlaubt keine regulären Ausdrücke bei der Auswahl der Tests, was dazu führt, dass man die *IntegrationTest.java nicht getrennt von den *Test.java Klassen ausführen kann. Stattdessen müssen immer Einstiegsklassen in die Tests angegeben werden.
Maven, genauer das Surefire-Plugin, dagegen unterstützt nur eine Filterung auf Dateiebene über die Include-Filter.
Testsuiten sind gewönliche Java-Klassen die mit @Suite annotiert werden. Diese Java-Klasse kann nun von allen IDEs und Build-Systemen als Startpunkt für die Tests genutzt werden.

@RunWith(Suite.class)
@Suite.SuiteClasses({
  UserUnitTest.class,
  AnotherUnitTest.class
})
public class UnitTestSuite {}

Der riesengroße Nachteil von Testsuiten ist, dass sie gepflegt werden müssen. Werden neue Tests geschrieben, müssen die Klassen der Suite hinzugefügt werden, da sie sonst vom CI-Server nicht ausgeführt werden. Das führte bei uns im Projekt nach relativ kurzer Zeit dazu, dass einige Testklasse zwar angelegt wurden, aber nicht in der Suite enthalten waren. Zum Glück haben wir die Shell und ein Skript zum Sammeln aller Unit-Tests ist schnell geschrieben:

find src/test/java/ -name '*.java' |
xargs grep -l "@Test" |
xargs grep -L "extends TestSetup" |
xargs grep -L "@RunWith" |
xargs basename |
sed 's/.java/.class,/g' |
sort

Der Phantasie sind natürlich keine Grenzen gesetzt, wie jetzt genau die Tests als Unit-, Integration- oder Selenium-Test erkannt werden. Im Beispiel werden alle Java-Klassen gefunden, die mindestens eine @Test Annotation haben, aber nicht von TestSetup erben und bei denen es sich nicht um Spring-Testcontext-Tests handelt, da diese im Projekt mit @RunWith markiert sind.

Wichtig ist lediglich die geeigneten grep Optionen zu verknüpfen:

  • grep -l: Das kleine L listet alle Treffer
  • grep -L: Findet alle Dateien, die nicht treffen
  • basename: Liefert den Dateinamen, damit er direkt in die Suite-Klasse eingesetzt werden kann
  • sed: Pure Faulheit: Macht aus den .java Endungen .class, damit die Ergebnisse per Copy & Paste eingesetzt werden können

Leider ist immer noch ein manuelles Prüfen der Tests notwendig, selbst wenn uns die Shell-Skripte jetzt alle Klassen liefern. Man könnte also einfach vor jedem Build dieses Skript ausführen, um alle Test-Klassen zu finden.

Gibt es bessere Vorschläge, TestSuiten automatisch zu befüllen? Ich freue mich auf Kommentare.