Testdatenerstellung: Permutationen

Beim Testen mit DataProvidern kann es manchmal sinnvoll sein, dass man viele
verschieden Eingabeparameter testen möchte. Z.B. wenn man eine Methode tested,
die als einen Parameter eine Buffergröße hat, macht es Sinn dieselben Testdaten
mit verschiedenen Buffereinstellungen durchzuprobieren. Da die Ergebnisse
nicht von der Puffergröße abhängen kann man einfach alle Tests mit allen
Puffergrößen laufen lassen.

/**
 * <p>
 * Creates a permutation of the given 2-dimensional array
 * and the given 1-dimensional array.
 * </p>
 * <code>
 * <pre>
 * permutate(new Object[][] { {"foo", "bar"}, {"alice","bob"}},
 *           new Object[] {1,2},
 *           1)
 * </pre>
 * </code> returns <code>
 * <pre>
 * new Object[][] {
 *      {"foo", 1, "bar"},
 *      {"alice", 2,"bob"},
 *      {"foo", 1, "bar"},
 *      {"alice", 2, "bob"}
 * }
 * </pre>
 * </code>
 * 
 * @param aData a 2-dimensional data object
 * @param aAdditionalColumn the additional column's data
 * @param aIndex the index where the additional column
 *               is added to <code>aData</code>
 * @return the permutated array
 */
public Object[][] permutate(
                            Object[][] aData,
                            Object[] aAdditionalColumn,
                            int aIndex)
{
    final List<Object[]> result = new ArrayList<Object[]>();

    for (Object additionalValue : aAdditionalColumn)
    {
        for (Object[] data : aData)
        {
            final Object[] newData = new Object[data.length + 1];

            if (aIndex > 0)
            {
                System.arraycopy(data, 0, newData, 0, aIndex);
            }
            newData[aIndex] = additionalValue;

            if (aIndex + 1 < newData.length)
            {
                System.arraycopy(data,
                                 aIndex,
                                 newData,
                                 aIndex + 1,
                                 newData.length - aIndex - 1);
            }
            result.add(newData);
        }
    }

    return result.toArray(new Object[0][]);
}

Note: The code above is released to public domain.

TestNG: mit DataProvidern auf Exceptions testen

Das Testframework TestNG bietet DataProvider an, mit deren Hilfe es leicht möglich ist viele Kombinationen von Eingabeparametern zu überprüfen. Aber was macht man, wenn man bei bestimmten Eingaben eine Exception erwartet? Einen zweiten Test mit DataProvidern, so dass man einen Test für valide Eingaben und einen für invalide Eingaben hat? Oder sollte man die invaliden Eingaben lieber in separaten Testcases prüfen, damit man auch prüfen kann, dass die korrekte Exception fliegt?

Ich finde beide Lösungen etwas unübersichtlich. Aber zum Glück gibt es Abhilfe.

public String append(String s, String t)
{
  if (s == null || t == null) throw new NullPointerException();
  return s + t;
}

@DataProvider(name = "providerAppend")
public Object[][] providerAppend()
{
  return new Object[][] {
    { null, null, new NullPointerException() },
    { "", null, new NullPointerException() },
    { null, "", new NullPointerException() },
    {"abc", "def", "abcdef" },
    {"abc", "", "abc" },
    {"", "def", "def" }
  };
}

@Test(dataProvider = "providerAppend")
public void testAppend(String s, String t, Object expected)
{
  if (expected instanceof Exception)
  {
    try
    {
      append(s, t);
      Assert.fail("Expected an exception: " + expected.getClass());
    }
    catch (Exception e)
    {
      Assert.assertEquals(e.getClass(), expected.getClass());
    }
  }
  else
  {
    Assert.assertEquals(append(s,t), expected);
  }
}

In obiger Lösung gehen wir einfach davon aus, dass es für den DataProvider egal ist, ob der Rückgabewert nun eine Wert oder eine Exception ist. Das handling wird dann in der Testmethode übernommen, und zwar dadurch, dass wir unterschiedliche Testimplementierungen wählen, je nachdem ob der erwartete Wert vom Typ Exception ist oder nicht. Wenn er vom Typ Exception ist, dann stellen wir sicher, dass eine Exception geworfen wird, und das die Klasse identisch mit der Erwartung ist. Manchmal kann es sinnvoll sein, denn Klassenvergleich gegen eine instanceof Prüfung zu ersetzen.


Caused by: java.rmi.RemoteException: -1

sun-java-logo

Caused by: java.rmi.RemoteException: -1

Dieser Fehler ist meist die Folge einer ArrayIndexOutOfBoundsException die während eines RMI calls auftrat. Irgendwo auf der Gegenseite ist die geflogen und wurde dann in eine RemoteException umgewandelt.


No enclosing instance of type InnerClassExample is accessible.

sun-java-logo

Die Fehlermeldung No enclosing instance of type InnerClassExample is accessible. Must qualify the allocation with an enclosing instance of type InnerClassExample (e.g. x.new A() where x is an instance of InnerClassExample). bekommt man immer dann, wenn man eine Klasse instantieren möchte, die als nicht statische innere Klasse definiert wurde. Wie zum Beispiel hier:

public class InnerClassExample
{
    private static void aFunction()
    {
        InnerClass privateClass = new InnerClass();
    }

    private class InnerClass {}
}

Die Ursache ist, dass InnerClass nicht static definiert wurde. Wenn man InnerClass static macht funktionierts wie man das erwarten würde. Ich behaupte einfach mal, dass man in 95,3% der Fälle auch wirklich eine statische innere Klasse haben will, man kann das static also einfach dazuschreiben.

Und so siehts dann aus:

public class InnerClassExample
{
    private static void aFunction()
    {
        InnerClass privateClass = new InnerClass();
    }

    private static class InnerClass {}
}

Hintergrund: Innere Klassen sind an ihre äußeren Klassen gebunden. So ist es möglich von der inneren Klasse auf Member der äußeren zuzugreifen. Damit das funktioniert muss klar sein von welcher Instanz der Member gelesen werden soll. Daher braucht es eine eins-zu-eins Beziehung zwischen äußerer und innerer Klasse. Diese Beziehung ist gegeben, wenn die innere Klasse in einer nicht statischen Methode erzeugt wird. Will man aber die innere Klasse aus einer statischen Methode, oder von einer anderen Klasse aus erzeugen, dann weiss Java nicht zu welcher Instanz der äußeren Klasse die Innere gehört.

Man hat also eigentlich mehrere Möglichkeiten:

1. Man macht die innere Klasse statisch. Dann kann man nicht mehr auf Member der äußeren Klasse zugreifen, aber man kann die Innere überall erzeugen.

2. Man zieht die innere Klasse in eine eigene Java Datei raus. Hat im Prinzip die gleichen Nachteile wie 1.

3. Man erzeugt eine extra Instanz der äußeren Klasse und benutzt deren new Operator. Das sieht dann so aus:

new InnerClassExample().new InnerClass();

Jep, das sieht etwas gewöhnungsbedürftig aus und ich hab das bislang noch nie in Code von erfahrenen Java-Programmierern gesehen.

4. Eine Variante von 3. ist, wenn man bereits eine Instanz von der äußeren Klasse hat, dann kann man die auch nehmen, z.B. so:

outclassInstance.new InnerClass();

Hab ich noch nie in der freien Natur gesehen. (Und das obwohl die Fehlermeldung genau diese Lösung vorschlägt.)

5. Man sieht ein, dass man eine innere Klasse nur innerhalb der äußeren Klasse benutzen sollte. Also macht man die Innere private (oder wenigstens protected) und baut seinen Code so um, dass er ohne die innere Klasse auskommt.


Hall Of Fame: Die schönsten Methoden-Signaturen

02 Eine der schönsten Methoden-Signaturen, die ich bisher gesehen habe:

SomeException runCommand(Command aCommand, boolean aRunSilent) throws SomeException;

Mit Findbugs Bugs finden

Findbugs ist ein guter Helfer wenn es um das Aufspüren von Fehlern in Java geht. Im Folgenden möchte ich einige Features des Eclipse Findbugs-Plugins vorstellen.

Das Beispiel zeigt eine Null-Pointer Dereferenzierung in Zeile 14, die von Eclipse nicht erkannt wird.

FehlerDenEclipseNichtErkennt

FehlerDenEclipseNichtErkennt

Findbugs findet den Fehler:

Findbugs findet den Fehler und markiert ihn durch einen netten kleinen Käfer.

Findbugs findet den Fehler und markiert ihn durch einen netten kleinen Käfer.

Aber Findbugs kann noch mehr. Z.B. kann er auch erkennen wenn man ineffizient programmiert, oder auch einfach unschöne Sachen macht.

Ineffizienter Gebrauch des keySet Iterators anstelle des entrySet Iterators

Ineffizienter Gebrauch des keySet Iterators anstelle des entrySet Iterators

Hardcoded reference

Hardcoded reference

Repeated Conditional Test

Repeated Conditional Test

Dabei sollte man allerdings immer beachten, dass diese Art der Codeanalyse nicht perfekt sein kann und alle Fehler erkennt. Das liegt nicht zuletzt daran, dass versucht wird false positives zu verhindern. In der Realität wird man eher auf false negatives stoßen, also fehlerhafte Situationen die Findbugs nicht erkennt. Aber das ist nicht wirklich schlimm, denn alles was Finbugs findet kann man als Gewinn verbuchen. Den Rest muss man halt weiterhin selbst suchen.

Die gezeigten Sachen sind zwar schon recht cool, aber Findbugs kann mehr. Und das ist richtig wuschig!

Nehmen wir mal folgende Funktion, wie sie eigentlich jeder schonmal geschrieben hat. Wir haben ein Argument und machen was damit. Aber wir  sind faul und prüfen nicht, ob s gleich null ist. Die Folge ist, dass wir eine mögliche NullPointer-Exception haben.

Eine Methode mit potentieller Null-Pointer-Exception

Eine Methode mit potentieller Null-Pointer-Exception

Findbugs bemäkelt das nicht. Warum sollte er auch, schließlich wird der meiste Legacy-Code voll von sowas sein. Ausserdem kann Findbugs garnicht wissen, ob der Programmablauf vielleicht garantieren kann, dass niemals uppercaseA niemals mit null aufgerufen wird.

Das macht aber nichts, denn wir können Findbugs sagen, dass wir die Methode niemals mit null aufrufen möchten. Gemacht wird das mit Annotationen. Ich empfehle die JSR305 Annotationen (link kommt noch), weil bei diesen die Chance relativ hoch ist, dass sie in Java 7 offiziell unterstützt werden könnten. Um die JSR305 Annotationen mit dem Findbugs-Plugin für Eclipse nutzen zu können muss nur das jsr305.jar im Buildpath sein. Der Rest geht automagisch.

In obigem Beispiel könnten wir festschreiben, dass das Argument von uppercaseA nicht null sein darf. Dann kann Findbugs Stellen erkennen, die dagegen verstoßen. Dazu verwenden wir die @Nonnull Annotation.

Nonnull Annotation

Nonnull Annotation

Desweiteren könnten wir garantieren, dass uppercaseA niemals null zurückgibt, indem wir an den Rückgabewert die @Nonnull Annotation dranschreiben.

Weitere Annotation sind @CheckForNull Annotation, die besagt, dass die Methode / der Aufrufer das Argument auf null prüfen muss und die @Nullable Annotation die besagt, dass das Argument bzw. der Rückgabewert null sein können.

Die Annotationen Nullable und CheckForNull

Die Annotationen Nullable und CheckForNull

Das schöne an den Annotationen ist, dass man schon in der Signatur die Informationen hat, die sonst höchstens in der API stünden. Wenn man das konsequent durchzieht kann man so viele Fehler erkennen, bevor man das erstemal den Compiler startet.

Es gibt noch eine ganze Reihe weiterer Annotationen, die meistens selbsterklärend sind, z.B. @Nonnegative, @SuppressWarnings, @CheckReturnValue,


Viele Links für Webentwickler

Eine Seite die viele nützliche Seiten rund um Webentwicklung verlinkt: http://www.elementiks.com/web_resources.php. Und eine rund um Code Analyzer für Java: http://java-source.net/open-source/code-analyzers


Follow

Bekomme jeden neuen Artikel in deinen Posteingang.