XCode – Nachträgliche Konfiguration von Application Tests

Beim Anlegen eines neuen Projektes kann XCode 4.3 auf Wunsch ein Unit-Test Bundle erzeugen. Die Option dazu heisst Include Unit Tests. Die so angelegten Unit-Tests werden in der Menüleiste unter Product > Test oder alternativ über den Shortcut cmd-U angestossen. XCode startet die zu testende App im Simulator und führt die Unit-Tests aus. Apple nennt diese Form der Tests Application Tests; ich würde sie eher Integration Tests nennen. Um eine ältere App nachträglich mit Application Tests zu versehen, müssen einige manuelle Schritte ausgeführt werden. Als Beispiel soll eine App namens AppWithoutTests dienen, die ich ohne die Include Unit Tests-Option angelegt habe.

1. Erzeugen eines Cocoa Touch Unit Testing Bundles

  • Klick auf File > New > Target ...
  • Auswahl des Cocoa Touch Unit Testing Bundle
  • Eingabe eines Product Names (z.B. Application Tests) und Klick auf Finish

2. Aufräumen der Schemes
XCode hat nun das Testing Bundle erzeugt, jedoch hat es ein weiteres Scheme angelegt. Für die Entwicklung ist das umständlich, da man für die Ausführung der Tests immer das Scheme wechseln muss. Von daher löschen wir das neu angelegte Scheme und erweitern das bisherige Standard-Scheme.

  • Klick auf Product > Manage Schemes ...
  • Löschen des automatisch angelegten Scheme ApplicationTests
  • Hinzufügen des ApplicationTests Bundles im Test-Target des Standard-Schemes (im Beispiel AppWithoutTests).

3. Testing Bundle: Bundle Loader und Testhost
Noch sind wir nicht fertig. Wir müssen die Wirts-Applikation in den Bundle Settings unseres Testing Bundles selbst konfigurieren.

  • Zunächst setzen wir den Pfad des Bundle Loader auf die unsere kompilierte App, also $(BUILT_PRODUCTS_DIR)/<app_name>.app/<app_name>. Für unsere Beispielapp müssen wir den Pfad auf $(BUILT_PRODUCTS_DIR)/AppWithoutTests.app/AppWithoutTests setzen.
  • Anschliessend setzen wir den Test Host auf den soeben konfigurierten Bundle Loaders. Dazu geben verwenden wir die Variable $(BUNDLE_LOADER).

4. Testing Bundle: Setzen der Target Depedencies
Die Abhängigkeit unsers Testing Bundles zu dem Application Target müssen wir selbst hinzufügen, so dass sichergestellt ist, dass immer zuerst die App, und dann die Tests kompiliert werden.

  • Klick auf die Build Phases unseres Testing Bundles
  • Klick auf Plus-Symbol in der Sektion Target Dependencies
  • Auswahl des Application Targets (Im Beispiel AppWithoutTests).

5. Application Target: Symbols hidden by default abschalten
Die zu testende Applikation darf die kompilierten Methoden und Klassen nicht verstecken. Ansonsten scheitert der Linker im Testing Bundle, weil er die importierten Klassen nicht findet.

  • Dazu müssen wir sicherstellen, dass die Option Symbols Hidden by Default in unserem Application Target AppWithoutTests” auf NO gesetzt ist.

Mit diesen (leider sehr umständlichen) Schritten können nun Application Tests für unsere AppWithoutTests ausgeführt werden. Im nächsten Blogeintrag geht es um die so genannten “Logic Tests”, also Tests, die keine Wirts-Applikation benötigen.

Unit-Tests Teil 2: Integrationstests

Im Gegensatz zu Mocktests beschränkt sich ein Integrationstest nicht auf die JVM, sondern involviert auch Fremdsysteme wie Datenbanken, LDAPs oder Webservices. Ein Integrationstest ist auf immer gleichbleibende Testdaten im Fremdsystem angewiesen, muss aber ggf. auch den schreibenden Zugriff testen. Hier bieten sich Transaktionen an: Jede Testmethode läuft in einer eigenen Transaktion, die unabhängig vom Testergebnis immer wieder zurückgerollt wird. Somit bleibt das Fremdsystem in einem definierten Zustand. Dennoch sollten Integrationstests nicht auf den späteren Liveinstanzen ausgeführt werden, sondern immer auf dedizierte Testinstanzen. Allerdings sollte sichergestellt sein, dass die Test- und Liveinstanzen sich gleich verhalten.

Integrationstests bieten sich insbesondere für Dao-Klassen an, da hier der Zugriffscode für Fremdsysteme gekapselt wird. Gute Unterstützung bietet hier das Spring Framework mit den Annotationen @RunWith, @Transactional und @TransactionalConfiguration.

Beispiel-Code:

  1. @ContextConfiguration(locations = { "/applicationContext-test-jdbc-ldap-transactional.xml" })
  2. @RunWith(SpringJUnit4ClassRunner.class)
  3. @Transactional
  4. @TransactionConfiguration(defaultRollback = true)
  5. public class LdapUserDaoImplIntegrationTest
  6. {
  7. ...
  8.         @Test
  9.         public void createUser()
  10.         {
  11.                 // create the user
  12.                 LdapUser ldapUser = createLdapUserPeterMueller();
  13.                 MutableLdapUserIdentity identity = ldapUser.getIdentity();
  14.                 String id = identity.getSamAccountName();
  15.                 // method under test
  16.                 ldapUserDaoImpl.createUser(ldapUser);
  17.  
  18.                 LdapUser testUser = ldapUserDaoImpl.getLdapUserBySamAccountName(id);
  19.                 Assert.assertNotNull("user should now exist in ldap.", testUser);
  20.                 // test more
  21.         }
  22. }

Vorteile:

  • Das konkrete Verhalten eines Fremdsystems (z.B. Active Directory, oder einer MSSQL) kann abgeprüft werden.
  • Fremde Webservices können automatisiert auf ihre Konsistenz geprüft werden.

Mögliche Stolperfallen:

  • Integrationstests laufen nicht offline. Bei Mavenprojekten sollten sie für die Offline-Entwicklung abgeschaltet werden, da sonst der Build scheitert. Dies kann durch ein eigenes “mobile”-Profil in Kombination mit dem maven-sunfire-plugin sichergestellt werden.
  • Die Testausführung dauert bei Integrationstest im Vergleich zu “normalen” JUnit-Tests wesentlich länger. Bei steigender Anzahl von länger laufenden Tests ist ein kompletter Durchlauf der Tests auf den Entwicklungsmaschinen nicht mehr sinnvoll.

Unit-Tests Teil 1: Mocks

Diese neue Artikelserie stellt Variaten von Unit-Tests vor und geht auf die jeweiligen Eigenschaften und Einsatzzwecke ein. Im ersten Teil werden Mock-Tests vorgestellt.

In Mock-Tests wird die Interaktion des zu testenden Objekts mit einem Mock-Objekt geprüft. Konkret werden sogenannte Erwartungen definiert, die das zu testende Objekt in Form von Methodenaufrufen erfüllen muss. Das eigentliche Mock-Objekt wird zur Laufzeit durch Frameworks wie jMock oder EasyMock generiert, eine eigene Implementierung des Interfaces ist dazu nicht notwendig.

Beispiel:
Eine Service-Klasse besitzt zwei Dao (“Data Access Object”) Objekte, die den Zugriff auf eine Datenbank kapseln. Beide Dao-Klassen wurden bereits mit separaten Unit-Tests abgeprüft, nun möchten wir die Service-Klasse selbst testen (ohne dabei die Dao-Klassen nochmals zu testen). Hier greifen wir auf ein Mock-Framework zurück und testen die Service-Klasse mit Mock-Versionen ihrer Daos. Für jede Methode unserer Service-Klasse beschreiben wir nun, welche Dao-Methoden aufgerufen werden sollten (und was sie in diesem Fall zurückgeben sollen).

Vorteile:

  • Mock-Tests erlauben losgelöstes Testen und bringen den Entwickler dazu, sich intensiver mit der Interaktion seiner Objekte zu beschäftigen.
  • Es werden keine Klassen doppelt getestet.
  • Service-Klassen können ohne Abhängigkeit zu Fremdsystemen getestet werden.

Mögliche Stolperfallen:

  • Ein Mock-Test bindet sich stark an die Implementierung. Wenn es alternative Wege zur Zielerreichung gibt (z.B. überladene Methoden), dann scheitert der Test, obwohl das Resultat des Methodenaufrufs weiterhin richtig wäre.
  • Das Mocken von fremden Klassen führt zu falschen Annahmen, da der Entwickler oftmals das genaue Verhalten eines Frameworks nicht immer zu 100% nachbilden kann.
  • Die Erwartungen (sog. Expectations) der Mock-Tests werden zu komplex (Perl-Syndrom: write once – never understand again.)

Empfohlene Artikel zum Vertiefen: