JAdventure - Kurs zur Softwareentwicklung
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

1.4.4 Unit Test

Wenn wir irgend welchen Code schreiben, dann müssen wir diesen auch testen. Dabei haben wir mehrere Möglichkeiten:

Manuelle Tests Es ist natürlich immer möglich, dass man manuell ein paar Tests durchführt. Dazu wird z.B. die Anwendung gestartet und werden die Funktionalitäten manuell durchgetestet.

Dies bringt aber einige Nachteile mit sich:

  • Es ist nur möglich, wenn ich etwas starten kann zum testen.
  • Es ist sehr zeitaufwendig, manuell alles zu testen. Und es ist auch sehr schwer, alle Möglichkeiten durchzutesten.

Unit Tests Unit Tests sind die einfachste Art von automatisierten Tests. Hier werden die kleinsten Einheiten (Units) getestet. Dabei wird darauf geachtet, dass alle Möglichkeiten, wie z.B. eine Methode durchlaufen werden kann, auch wirklich durchlaufen werden. So kann sicher gestellt werden, dass es keinen ungetesteten Code gibt.

Im Folgenden werden wir die Unit Tests betrachten, die im Rahmen des bisherigen Codes geschrieben wurden.

Testumfang

Generell sollten wir jeden Code, den wir schreiben, auch mit Unit Tests testen. In der Praxis wird aber oft auch Code generiert. Dies ist z.B. bei unseren Entities der Fall.

Wenn wir uns z.B. die folgende KLasse ansehen:

/**
 * Rectangle Size
 */
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
@EqualsAndHashCode(callSuper=true)
public class RectangleSize extends Size {

    /**
     * Width of the object.
     */
    int width;

    /**
     * Height of the object.
     */
    int height;
}

dann sehen wir nur zwei Instanzvariablen. Durch Lombok wird aber einiges an Code generiert:

  • Konstruktoren
  • Getter
  • Setter
  • toString
  • hashCode und equals

Somit ist in der Klasse auch einiges an Code. Ich vertrete die Auffassung, dass generierter Code nicht explizit getestet werden muss. Es wird sich darauf verlassen, dass der Code korrekt funktioniert. Bei der Beispielklasse macht es keinen Sinn zu Prüfen, ob getWidth() wirklich den Inhalt der Variable width zurück gibt und dass man diesen Wert mit setWidth neu setzen kann. Kritischer kann es sein, dass man die Funktionalität von equals auch testet. Hier wäre ggf. durch falsche oder fehlende Implementationen ein falsches Verhalten bekommt. Hier vertrete ich die Meinung: Sobald ich ein Verhalten benötige, das von dem üblichen Verhalten abweicht, dann schreibe ich den Code selbst und teste diesen auch.

Damit haben wir aber die Erklärung, wieso die meisten Entity Klassen keinen Unit-Test haben. Und wieso bei einem “Run all Unit Tests with Coverage” der Namespace mit den Entities eher schlecht aussieht:

Die 68% der Methoden und nur 74% der Zeilen in den Entities in ord.jadv.model sind getestet worden. Das ist eine Quote unterhalb der oft anvisierten 80% - aber in diesem Bereich akzeptabel.

Level

In der Klasse Level haben wir Methoden, die wir testen können:

/**
 * JAdventure Level
 */
@Getter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper=true)
public class Level extends SavedObject implements Container {

    ...
		
    public void removeObject(@NonNull GameObject obj) {
        objects.remove(obj);
    }

    public void addObject(@NonNull GameObject obj, int x, int y) {
        objects.add(obj);
        obj.setPosition(new Position(x, y, this));
    }

    @Override
    public List<GameObject> getChildren() {
        return objects;
    }
}

Wir haben hier also ein klares Verhalten, welches getestet werden muss:

/**
 * Tests the logic of the Level class.
 */
public class LevelTest {
    /**
     * Tests the adding of an object to the level
     * <remark>
     *     After adding an object to the level at a specified point, the position of the object must be correct.
     * </remark>
     */
    @Test
    public void testAddObject() {
        Level level = new Level("TestLevel", 1000, 1000);
        GameObject obj = GameObject.builder().name("TestObject").size(new RectangleSize(10, 10)).build();
        level.addObject(obj, 1, 2);
        assertAll(
                () -> assertEquals(level, level.getObjects().get(0).getPosition().getParent()),
                () -> assertEquals(1, level.getObjects().get(0).getPosition().getX()),
                () -> assertEquals(2, level.getObjects().get(0).getPosition().getY()));

    }

    /**
     * Tests that objects can be removed from a level.
     */
    @Test
    public void testRemoveObject() {
        Level level = new Level("TestLevel", 1000, 1000);
        GameObject obj1 = GameObject.builder().name("TestObject").size(new RectangleSize(10, 10)).build();
        level.addObject(obj1, 1, 2);
        GameObject obj2 = GameObject.builder().name("TestObject2").size(new RectangleSize(10, 10)).build();
        level.addObject(obj2, 3, 4);

        level.removeObject(obj2);
        assertEquals(1, level.getObjects().size());
    }
}

Wir testen also die beiden Methoden addObject und RemoveObject.

Was fehlt ist ein Unit Test für die Methode getChildren. Diesen Test könntest Du im Rahmen einer Übung schreiben. Ab Lektion 2 wird sich ein Unit Tests für diese Methode auch im Source finden.

SavedObjectAdapter

Wir haben einen SavedObjectAdapter und wir müssen nun noch testen, dass dieser wirklich das macht, was wir erwarten.

Dazu brauchen wir mehrere Tests:

Test der Serialisierung

Wir sollen sicher stellen, dass sich ein Objekt ganz normal mit seiner Klasse serialisieren lässt. Wir erstellen also einfach eine Instanz einer Klasse, die von SavedObject erbt und serialisieren diese.

Eine einfache kleine Klasse wäre z.B. RectangleSize.

    /**
     * Test serialization of a SavedObject instance.
     */
    @Test
    public void testSerialization() {
        String jsonRSize = "{\"width\":123,\"height\":50,\"type\":\"org.jadv.model.size.RectangleSize\"}";
        RectangleSize rSize = new RectangleSize(123, 50);
        Gson gson = new Gson();
        assertEquals(jsonRSize, gson.toJson(rSize, RectangleSize.class));
    }

Test der Serialisierung mit SavedObject

Statt der konkreten Klasse können wir auch immer sagen, dass ein SavedObject serialisiert wird. Und die Erwartung ist, dass dann die Klasse korrekt serialisiert wird. Dazu schreiben wir einen weiteren Unit Test.

    /**
     * Test serialization of a SavedObject instance using SavedObject.class.
     */
    @Test
    public void testSerializationWithSavedObject() {
        String jsonRSize = "{\"width\":123,\"height\":50,\"type\":\"org.jadv.model.size.RectangleSize\"}";
        RectangleSize rSize = new RectangleSize(123, 50);
        Gson gson = new Gson();
        assertEquals(jsonRSize, gson.toJson(rSize, SavedObject.class));
    }

Test der Deserialisierung

Nun gehen wir noch den umgekehrten Weg. Wir haben ein Json String und wissen nur, dass es ein SavedObject ist. Und bei der Deserialisierung erwarten wir, dass wir eine Instanz von RectangleSize zurück bekommen mit den richtigen Werten.

    /**
     * Test deserialization of a SavedObject instance.
     */
    @Test
    public void testDeserialization() {
        String jsonRSize = "{\"width\":123,\"height\":50,\"type\":\"org.jadv.model.size.RectangleSize\"}";
        Gson gson = new Gson();
        RectangleSize rSize = (RectangleSize) gson.fromJson(jsonRSize, SavedObject.class);
        assertAll(
                () -> assertEquals(123, rSize.getWidth()),
                () -> assertEquals(50, rSize.getHeight())
        );
    }

Test des Fehlers: Klasse nicht vorhanden

Wenn die Klasse über den Namen nicht gefunden werden kann, dann werfen wir eine JsonParseException. Dass dies funktioniert müssen wir nun auch noch testen. Dazu können wir uns ein json Strign bauen mit einem type Wert, den es als Klasse nicht gibt.

    /**
     * Test thrown exception if given type is unknown.
     */
    @Test
    public void testExceptionWhenClassUnknown() {
        String json = "{\"type\":\"does.not.exist.NonExistingClass\"}";
        Gson gson = new Gson();
        assertThrows(JsonParseException.class, () -> gson.fromJson(json, SavedObject.class));
    }

Damit hätten wir Unit Tests geschrieben. Und wenn wir uns die Coverage ansehen (Run all Tests with Coverage und dann im Coverage Toolfenster), dann sehen wir: 100% Coverage bei SavedObjectAdapter.

ListOfSavedObjectAdapter

Unser Adapter für eine List%lt;SavedObject> muss nun auch noch getestet werden.

Dazu nehmen wir eine Klasse, die so eine List enthält und serialisieren/deserialisieren diese.

An Klassen haben wir bisher nur Level, daher nutzen wir diese Klasse.

Der eigentliche Aufbau ist ansonsten ähnlich wie die bisherigen Tests. Daher dürften tiefere Erklärungen unnötig sein.

Serialisierung

    /**
     * Tests the serialization of an Object (using class Level) that has a list of SavedObject.
     */
    @Test
    public void testSerialization() {
        String jsonLevel = "{\"name\":\"TestLevel\",\"width\":1000,\"height\":1000,\"objects\":[{\"name\":\"TestObject\",\"size\":{\"width\":10,\"height\":10,\"type\":\"org.jadv.model.size.RectangleSize\"},\"position\":{\"x\":1,\"y\":2},\"type\":\"org.jadv.model.objects.GameObject\"},{\"name\":\"TestObject2\",\"size\":{\"width\":10,\"height\":10,\"type\":\"org.jadv.model.size.RectangleSize\"},\"position\":{\"x\":2,\"y\":3},\"type\":\"org.jadv.model.objects.GameObject\"}],\"type\":\"org.jadv.model.level.Level\"}";
        Level level = new Level("TestLevel", 1000, 1000);
        GameObject obj = GameObject.builder()
                .name("TestObject")
                .size(new RectangleSize(10,10))
                .build();
        level.addObject(obj, 1, 2);
        obj = GameObject.builder()
                .name("TestObject2")
                .size(new RectangleSize(10,10))
                .build();
        level.addObject(obj, 2, 3);

        Gson gson = new Gson();
        String json = gson.toJson(level, Level.class);

        assertEquals(jsonLevel, json);
    }

Deserialisierung

    /**
     * Tests the deserialization of an Object (using class Level) that has a list of SavedObject.
     */
    @Test
    public void testDeserialization() {

        String jsonLevel = "{\"name\":\"TestLevel\",\"width\":1000,\"height\":1000,\"objects\":[{\"name\":\"TestObject\",\"size\":{\"width\":10,\"height\":10,\"type\":\"org.jadv.model.size.RectangleSize\"},\"position\":{\"x\":1,\"y\":2},\"type\":\"org.jadv.model.objects.GameObject\"},{\"name\":\"TestObject2\",\"size\":{\"width\":10,\"height\":10,\"type\":\"org.jadv.model.size.RectangleSize\"},\"position\":{\"x\":2,\"y\":3},\"type\":\"org.jadv.model.objects.GameObject\"}],\"type\":\"org.jadv.model.level.Level\"}";

        Gson gson = new Gson();
        Level level2 = (Level) gson.fromJson(jsonLevel, SavedObject.class);

        assertAll(
                () -> assertEquals("TestLevel", level2.getName()),
                () -> assertEquals(2, level2.getObjects().size()),
                () -> assertEquals("TestObject", level2.getObjects().get(0).getName()),
                () -> assertEquals(1, level2.getObjects().get(0).getPosition().getX()),
                () -> assertEquals(2, level2.getObjects().get(0).getPosition().getY()),
                () -> assertEquals(level2, level2.getObjects().get(0).getPosition().getParent()),
                () -> assertEquals("TestObject2", level2.getObjects().get(1).getName()),
                () -> assertEquals(2, level2.getObjects().get(1).getPosition().getX()),
                () -> assertEquals(3, level2.getObjects().get(1).getPosition().getY()),
                () -> assertEquals(level2, level2.getObjects().get(1).getPosition().getParent())
        );
    }

Und auch hier erhalten wir dann eine Coverage von 100% und können damit an dieser Stelle erst einmal zufrieden sein.

Diese Unit Tests werden uns in zukünftigen Lektionen aber auch noch weiter beschäftigen.

Test Objekte

Ein erster Punkt war, dass erste Objekte erzeugt werden. Wir haben im Rahmen der Unit Tests nun auch Level mit ersten GameObject erstellt. Das sind natürlich noch kein wirklich sinnvollen oder brauchbaren Elemente.

Dies ist natürlich klar, da wir ja noch nicht wirklich viel implementiert haben. Daher haben wir die Test Objekte bisher auch rein als String Literale in den Tests. Die Json Strings werden aber zukünftig in Ressourcen wandern und somit tatsächlich als Dateien auch zur Verfügung stehen.