UnitTesting



UnitTesting

0 0


unit-testing-vortrag


On Github dneumann / unit-testing-vortrag

UnitTesting

Durch einfache Techniken zu besserer Wartbarkeit des Codes

Dennis Neumann Digitale Bibliothek Software- und Serviceentwicklung

Übersicht

Wozu testen? Meine "best practices" bei Unit Tests Ausblick: weitere Testarten

1. Wozu testen?

Typische Gedanken bei umfangreichem Code:

  • Ich will die Methode nicht anfassen, dann geht bestimmt etwas kaputt. (→ Änderungen, Refactoring)
  • Ich habe nur ein kleines Feature hinzugefügt, wieso ist jetzt diese komplett andere Stelle kaputt? (→ neuer Bug)
  • Diesen Bug habe ich doch schon vor zwei Wochen gefixt, wieso ist der wieder da? (→ alter Bug)
  • Wenn ich einen Bug fixe, tauchen jedesmal zwei neue auf. (→ neue Bugs)
  • → je größer das Programm wird, desto öfter kommt es zu solchen Problemen
  • → Wartung und Debugging kann zu einem "Fass ohne Boden" werden

Das "Änderungsproblem":

  • Ideale Welt:
  • - Anforderungen
  • - Implementieren
  • - Nutzen
  •  
  •  
  •  
  • Realität:
  • - Anforderungen
  • - Implementieren
  • - while (true) {
  •       Nutzen
  •       Verändern
  •    }
→ wir müssen darauf vorbereitet sein, dass ständig Änderungen und Erweiterungen gewünscht werden → der Code muss wartbar sein und es auch bleiben

Entschärfung des "Änderungsproblems":

Automatisiertes Testen

leicht zu testender Code ist leichter zu warten

Ziele beim Testen

  • 1. System funktioniert jetzt
  •  
  • Flüchtigkeitsfehler sofort erkennen
  • verhindern von neuen Bugs von Anfang an
  • wäre in der "idealen Welt" ausreichend

Ziele beim Testen

  • 2. System wird in Zukunft funktionieren
  •  
  • imo DER Grund für Testen
  • Änderungen und Refactorings ohne Angst
  • sofortiges Feedback bei neuen Bugs
  • Bug-auslösenden Test schreiben, danach Bug fixen
  •  → alte Bugs kommen nicht wieder
  • statt "testen" besser "sicherstellen"

Ziele beim Testen

  • 3. Man wird ein besserer Entwickler
  •  
  • subjektives Ziel
  • komplexe Methoden und Klassen lassen sich nur schwer testen
  • man kann seinen Code immer weiter verbessern durch Refactorings

Voraussetzungen für gute Tests

  • man muss auf Testbarkeit hin programmieren
  • → man muss sich umstellen
  • bereits Programmiertes ist meistens "verloren", da untestbar und keine umfassenden Refactorings möglich

Nachteile

  • Lernaufwand
  • Mehraufwand für Erstellen von Tests
  • Tests müssen auch ständig geändert und erweitert werden

Zwischenfazit

  • automatisiertes Testen macht uns das (Entwickler-)Leben leichter
  • ABER
  • erst nach einer Verzögerung, spätestens wenn Änderungswünsche kommen
  • Testbarkeit → Wartbarkeit
  • man kann test-süchtig werden!

2. Best Practices

Test-Pyramide

Minimalbeispiel

public class Person {

  private String name;

  public void setName(String newName) {
    name = newName.trim();
  }

  public String getName() {
    return name;
  }

}

Unit Test


@Test
public void shouldRemoveWhitespace() {
  Person personSut = new Person();
  personSut.setName(" Alex ");
  String storedName = personSut.getName();
  
  assertEquals("Alex", storedName);
}
  • - benutzt keine anderen (selbstgeschriebenen) Klassen
  • - Ziel 2: System wird in Zukunft funktionieren
  •   → wenn trim() gelöscht wird, scheitert der Test

Komplexeres Beispiel mit zwei Klassen

Clientcode v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}

  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();


  • - Client kann nicht isoliert getestet werden, es wird immer ein Service als lokale Variable miterzeugt
  • - Ziel bei Unit Tests: Testen einzelner Klassen unabhängig von anderen

v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}
  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

v2 (mit Setter)

public class Client {
  private Service srv = new Service();
  // for unit tests
  void setService(Service newSrv) {
    srv = newSrv;
  }
  public void useService() {
    srv.sendMessage("start");
    //...
  }
}
  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();
  • - kein "new" in Methode
  • - Service kann im Test ersetzt werden (durch einen Mock), da eine Instanzvariable
  • - man muss ein bisschen "tricksen", damit der Code testbar wird

Was ist ein Mock?

  • - Mock: Ersatz für ein Objekt (to mock = nachahmen)
  • - Java-Framework zum Erstellen von Mocks: Mockito

v2 (mit Setter)

public class Client {
  private Service srv = new Service();
  // for unit tests
  void setService(Service newSrv) {
    srv = newSrv;
  }
  public void useService() {
    srv.sendMessage("start");
    //...
  }
}

  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

Unit Test


@Test
public void shouldSendStartMessage() {
  // wie Produktivcode
  Client clientSut = new Client();

  // echten Service ersetzen
  Service serviceMock = mock(Service.class);
  clientSut.setService(serviceMock);
  // wie Produktivcode
  clientSut.useService();

  // der eigentliche Test, "Sicherstellen"
  verify(serviceMock).sendMessage("start");
}

Macht über den Mock

  • z. B.:
  • - prüfen, welche Methoden im Test aufgerufen wurden
  • verify(serviceMock).sendMessage("start")
  • - beliebige Werte zurückgeben
  • when(serviceMock.getCurrentYear()).thenReturn(3000)
  • - beliebige Exceptions werfen
  • when(serviceMock.sendMessage("start")).thenThrow(new ServiceIsRunningException())

Wie kriegt man einen Mock in die zu testende Klasse hinein?

  • - Setter-Methode für Tests ✔
  • - Konstruktor für Tests
  • - Fabrikmethode
  • - Objektfabrik
  • - direkte Übergabe an die Methode als Parameter
  •  
  •  → muss man je nach Anwendungsfall auswählen
  •  → mehr Infos: howitest.wordpress.com

v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}


  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

mit extraKonstruktor

public class Client {

  private Service srv;

  public Client() {
    srv = new Service();
  }

  // for unit tests
  Client(Service testSrv) {
    srv = testSrv;
  }

  public void useService() {
    srv.sendMessage("start");
    //...
  }

}

v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}


  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

mit Fabrikmethode

public class Client {

  // keine Instanzvariable!

  // for unit tests
  Service createService() {
    return new Service();
  }

  public void useService() {
    Service srv = createService();
    srv.sendMessage("start");
    //...
  }

}

v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}


  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

mit Objektfabrik("Provider")

public class Client {

  private Provider serviceProvider = new Provider();

  // for unit tests
  void setProvider(Provider newProvider) {
    serviceProvider = newProvider;
  }

  public void useService() {
    Service srv = serviceProvider.getService("local");
    srv.sendMessage("start");
    //...
  }

}

v1

public class Client {

  public void useService() {
    Service srv = new Service();
    srv.sendMessage("start");
    //...
  }

}


  // Aufruf im Produktivcode: 
  Client c = new Client();
  c.useService();

mitParameterübergabe

public class Client {

  public void useService(Service srv) {
    srv.sendMessage("start");
    //...
  }

}



  // ABER: Aufruf im Produktivcode:
  Client c = new Client();
  c.useService(new Service());


Hinweise für bessere Testbarkeit

  • kein "new MyClass()" in Methoden
  • Konstruktor idealerweise ohne Parameter und ohne Logik
  • so wenig static/global/singletons wie möglich
  • Zugriffe auf Dateien und Netzwerk: in eigene Klassen auslagern
  • keine "train wrecks": getServiceLocator().getService().getDescription().asXml()
  • → viele Mocks nötig

3. Ausblick

  • - noch wenig bis keine Erfahrung bei GUI, Web, DB
  • - Integration Tests mit Spring
  •   → Einschleusen von Mocks durch Dependency Injection
  • - User Tests z. B. mit Selenium