iPhone Code Schnipsel: Clang Static Analyzer
Gegeben
Man hat die ersten ViewController fertig, einiges an Logik geschrieben, so dass die Applikation eigentlich ganz gut funktionieren sollte. Der erste Start endet aber im Debugger und nach kurzem Prüfen ist man sich sicher, dass man irgendwo einen Bug im Object-Lifecycle hat - also ein Retain/Release/Autorelease zu viel oder zu wenig.
Gesucht
Ein schneller Weg, diese Fehler zu finden, ohne seinen eigenen Code noch einmal komplett durchgehen zu müssen.
Lösung
Der Clang Static Analyzer: http://clang-analyzer.llvm.org/
Einfach den letzten Build downloaden und entpacken. Am komfortabelsten ist es, wenn man das Verzeichnis gleich dem PATH hinzufügt. Dann in Xcode ein "Clean All Targets" ausführen und auf der Konsole in das Projektverzeichnis wechseln. Dort gibt man folgendes Kommando ein:
scan-build -k -V xcodebuild -configuration Debug -sdk iphonesimulator3.0
Wenn alles klappt, sollte Clang nun das Projekt scannen und am Ende eine Seite im Default Browser öffnen. Dort findet man dann eine Übersicht aller gefunden Fehler, Bugs und Verstöße gegen Coding Conventions. Die Punkte geht man einzeln durch und bekommt wunderbar erklärte Problembeschreibungen. Das sieht dann z.B. so aus:
Als Folge stehen die Chancen gut, dass der nächste Start der neuen App problemlos klappt.
Schlüsselwörter: iphone, objective-c, programmierung, schnipsel, xcode
Kurztip: Fehlermeldung "Unrecognized Selector" in Xcode
Von Zeit zu Zeit wird ein Fehler während des Programmierens in Xcode mit der folgenden Fehlermeldung beim Ablauf des Programms bestraft:
2009-03-18 18:51:12.734 PlayGround[51029:20b] *** -[NSCFString someString]: unrecognized selector sent to instance 0xa05b1328
Der Stacktrace des Debuggers beinhaltet dabei nicht die Stelle, an der der Fehler verursacht wird. Abhilfe schafft folgende Zeile in der Datei ~/.gdbinit:
fb objc_exception_throw
Der Befehl definiert einen Breakpoint für die Funktion objc_exception_throw, die bei jeder ausgelösten Exception aufgerufen wird. Dadurch bleibt der Debugger an einer Stelle stehen, an der der Verursacher noch im Stacktrace zu finden ist.
Schlüsselwörter: kurztip, objective-c, xcode
Unit-Testing in Objective-C, Teil 3
Im dritten Teil unserer Serie über Unit-Testing in Objective-C wollen wir das Beispielprojekt aus Teil 1 fortsetzen und dabei die Erstellung von Mock-Objekten mit OCMock kennen lernen. Der Beispielcode steht wieder als Git-Repository zur Verfügung, welches per
git clone git://www.komprovisation.de/objcut1.git
lokal geklont oder auf der Projektseite durchstöbert werden kann. Zudem ist im Artikel an dedizierten Stellen die jeweilige Revision vermerkt.
Gegeben
Aufbauend auf der letzten Revision des ersten Teils haben wir das Modell User bereits um eine einfache Datenbankanbindung erweitert. Als Datenbank wird SQLite zusammen mit der Bibliothek PLDatabase verwendet. Letztere hat den Vorteil, dass die Aufruf an SQLite gekapselt werden und damit ein Wechsel auf ein anderes Datenbanksystem leichter möglich sein sollte. Die Klasse User verwendet die Datenbank über die Hilfsklasse UserDatabase, die im Wesentlichen die folgenden zwei Methoden zur Verfügung stellt:
- (User *)findWithEmail:(NSString *)email {
[database open];
User *user = nil;
NSObject<PLResultSet> *results;
results = [database executeQuery: @"SELECT * FROM users WHERE email = ?", email];
if ([results next]) {
user = [[User alloc] init];
user.email = [results stringForColumn:@"email"];
user.hashedPassword = [results stringForColumn:@"hashed_password"];
}
[results close];
[database close];
return [user autorelease];
}
- (BOOL)saveWithEmail:(NSString *)email hashedPassword:(NSString *)hashedPassword {
[database open];
NSObject<PLPreparedStatement> *statement;
statement = [database prepareStatement: @"INSERT INTO users (email, hashed_password) VALUES (?, ?)"];
[statement bindParameters: [NSArray arrayWithObjects: email, hashedPassword, nil]];
BOOL result = [statement executeUpdate];
[statement close];
[database close];
return result;
}
Die Methode findWithEmail: sucht einen Benutzer anhand der E-Mail-Adresse und liefert eine Instanz des Objektes User zurück. Die Methode saveWithEmail:hashedPassword: speichert einen Benutzer mit seiner E-Mail-Adresse und dem verschlüsselten Password. Die Verschlüsselung des Passwortes ist unabhängig von der Datenbank und erfolgt daher in User mittels der Methode encrypt:.
+ (NSString *)encrypt:(NSString *)password {
unsigned char hash[CC_SHA256_DIGEST_LENGTH];
CC_SHA256([password UTF8String],
[password lengthOfBytesUsingEncoding:NSUTF8StringEncoding],
hash);
NSMutableString *hashedPassword;
hashedPassword = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH];
for (int i = 0; i < CC_SHA256_DIGEST_LENGTH; ++i) {
[hashedPassword appendString:[NSString stringWithFormat:@"%02x", hash[i]]];
}
return hashedPassword;
}
Gesucht
Ziel ist es nun, die Methode loginWithEmail:password:
- (BOOL)loginWithEmail:(NSString *)login password:(NSString *)password {
return ([login isEqualToString:@"thomas@dohmke.de"] &&
[password isEqualToString:@"foobar"]);
}
so abzuändern, dass diese eine Klassenmethode ist und eine Instanz von User zurückliefert, sofern die Authentifizierung mit E-Mail-Adresse und Passwort erfolgreich war. E-Mail und Passwort sollen dabei mit den Werten in der Datenbank verglichen werden, d.h. die Methode muss im Gegensatz zur bisherigen Fake-Version korrekt funktionieren.
Lösung
Im ersten Schritt wandeln wir die beiden Testmethoden in UserTest.m so ab, dass sie der gewünschten Signatur entsprechend. Zur Erinnerung nachfolgend zunächst die bisherige Version:
-(void)testValidLogin {
STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"foobar"],
YES, @"should return YES");
}
-(void)testInvalidLogin {
STAssertEquals([user loginWithEmail:@"thomas@dohmke.de" password:@"wrong"],
NO, @"should return NO");
}
Die Methode soll in eine Klassenmethode umgewandelt werden, daher müssen wir sie statt an die Instanz user an die Klasse User senden. Außerdem soll sie eine Instanz von User zurückliefern, entsprechend sind die Aufrufe von STAssertEquals durch STAssertNotNil bzw. STAssertNil zu ersetzen:
- (void)testValidLogin {
STAssertNotNil([User loginWithEmail:@"thomas@dohmke.de" password:@"foobar"],
@"Should return user object.");
}
- (void)testInvalidLogin {
STAssertNil([User loginWithEmail:@"thomas@dohmke.de" password:@"wrong"],
@"Should return nil.");
}
Bei der Ausführung schlagen die Tests erwartungsgemäß fehl, da die erwartete Signatur der Methode nicht mit der Implementierung übereinstimmt. Entsprechend ändern wir die Deklaration in User.h
+ (User *)loginWithEmail:(NSString *)login password:(NSString *)password;
und passen die Definition in User.m entsprechend an:
+ (User *)loginWithEmail:(NSString *)login password:(NSString *)password {
User *user = [User findWithEmail:login];
if ([[User encrypt:password] isEqualToString:user.hashedPassword]) {
return user;
}
else {
return nil;
}
}
Die Implementierung sucht den Benutzer mittels der Methode findWithEmail:, verschlüsselt den Parameter password und vergleicht das Ergebnis mit dem verschlüsselten Passwort in der von findWithEmail: gefundenen Instanz. [Revision 13222fee]
Die Tests werden nun bestanden, allerdings ergibt sich ein neues Problem: Damit die Methode den Benutzer überhaupt in der Datenbank finden kann, müssen wir diesen dort speichern. Dies erfolgt bereits in der Methode setUp des Testfalls:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
STAssertEquals([user save], YES, @"save failed.");
}
Da das Speichern jedoch in derselben Datenbank erfolgt, die auch vom eigentlichen Programm benutzt wird, würden wir bei einem echten System möglicherweise reale Benutzerdaten überschreiben. Das ist unschön. Zudem wird der Benutzer mit jeder Testmethode neu angelegt wird, wie wir uns leicht auf der Konsole überzeugen können:
$ sqlite3 objcut1.db
SQLite version 3.4.0
Enter ".help" for instructions
sqlite> select * from users;
1|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
2|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
3|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
4|thomas@dohmke.de|c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2
Hier kommt nun OCMock ins Spiel.
Bessere Lösung
OCMock erlaubt uns, die Klasse UserDatabase durch einen Dummy zu ersetzen und in Folge zu verhindern, dass tatsächlich in die Datenbank geschrieben wird. Entsprechend wird UserDatabase selbst nicht mehr getestet, was in Ordnung geht, da wir in UserTest.m nur die Klasse User testen wollen.
Zuerst installieren wir OCMock, in dem wir es von seiner Homepage runterladen (aktuell ist derzeit Version 1.29), das Archiv entpacken und das Verzeichnis OCMock.framework nach /Library/Frameworks kopieren. Dann klicken wir unter Groups & Files rechts auf Frameworks > Linked Frameworks, Menüpunkt Add > Existing Frameworks, suchen das gerade kopierte Verzeichnis und bestätigen mit Add.
Als nächstes fügen wir der Testklasse in UserTest.h eine Instanzvariable für das Mock-Objekt hinzu:
@interface UserTest : SenTestCase {
// ...
id databaseMock;
}
In UserTest.m importieren wir OCMock
#import <OCMock/OCMock.h>
und definieren eine neue Methode, die das Mock-Objekt erstellt:
- (void)setUpMockDatabase {
BOOL yes = YES;
NSValue *wrappedValue = [NSValue valueWithBytes:&yes objCType:@encode(BOOL)];
databaseMock = [OCMockObject mockForClass:[UserDatabase class]];
[[[databaseMock stub] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword];
[[[databaseMock stub] andReturn:user] findWithEmail:user.email];
[User setDatabase:databaseMock];
}
Die ersten zwei Zeilen packen den booleschen Wert YES in eine Instanz der Klasse NSValue ein. In den folgenden drei Zeilen wird das Mock-Objekt für die Klasse UserDatabase mit zwei Stub-Methoden erstellt. Die Signaturen entsprechend dabei den Deklarationen in UserDatabase.h, wobei saveWithEmail:hashedPassword: stets YES und findWithEmail: die in setUp erstellte Instanz von User zurückgeben soll. In der letzten Zeile wird das Mock-Objekt der Klasse User übergeben, die dieses in Folge für alle Datenbankzugriffe verwendet.
Schließlich rufen wir setUpMockDatabase in der setup-Methode auf:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
[self setUpMockDatabase];
STAssertEquals([user save], YES, @"save failed.");
}
Fertig. Die Tests werden weiterhin bestanden, greifen aber nicht mehr auf die Datenbank zu. [Revision aeb659e1]
Erster Nachgang
Neben stub können Stub-Methoden auch mit expect definiert werden. Der Unterschied besteht darin, dass eine mit expect definierte Methode genau einmal aufgerufen werden muss, während die Anzahl der Aufrufe bei "stub" gleichgültig ist, d.h. auch kein Aufruf ist erlaubt. Will man mehr als einen Aufruf kontrollieren, ist das expect-Statement entsprechend oft zu wiederholen.
Als Beispiel verändern wir setUpMockDatabase wie folgt
- (void)setUpMockDatabase {
// ...
[[[databaseMock expect] andReturnValue:wrappedValue] saveWithEmail:user.email hashedPassword:user.hashedPassword];
// ...
}
und verifizieren die Anzahl der Aufruf in tearDown:
- (void)tearDown {
[databaseMock verify];
[user release];
}
Die Methode verify überprüft nach jedem Test, ob die mit expect definierte Stub-Methoden jeweils genau einmal aufgerufen wurde (was im Beispiel der Fall ist, da vor jedem Test in setUp die Instanz user gespeichert wird). [Revision 11065e8d]
Zweiter Nachgang
Wie würde die Lösung aussehen, wenn man kein Mock-Objekt verwenden will, sondern die Anbindung der Datenbank in UserDatabase bewusst mittesten möchte? SQLite erlaubt die Erstellung einer Datenbank im Hauptspeicher, so dass wir eine neue Methode setUpTestDatabase in UserTest.m wie folgt definieren
- (void)setUpTestDatabase {
userDatabase = [[UserDatabase alloc] initWithPath:@":memory:"];
[userDatabase.database executeUpdate: @"DROP TABLE users;"];
STAssertTrue([userDatabase.database executeUpdate: @"CREATE TABLE users (id INTEGER PRIMARY KEY NOT NULL, email VARCHAR(255), hashed_password VARCHAR(255));"], @"");
[User setDatabase:userDatabase];
}
und statt setUpMockDatabase in setUp aufrufen könnten:
- (void)setUp {
user = [User userWithEmail:@"thomas@dohmke.de" password:@"foobar"];
[self setUpTestDatabase];
STAssertEquals([user save], YES, @"save failed.");
}
Es ist zu beachten, dass die SQL-Befehle in setUpTestDatabase hier nur beispielhaft aufgeführt werden, im realen Leben würde man die Klasse UserDatabase um entsprechende Methoden erweitern, die diese kapseln und damit Dopplungen im Code vermeiden.
Schlüsselwörter: objective-c, programmierung, testen, xcode
Unit-Testing in Objective-C, Teil 2
Der zweite Teil unserer Serie zu Unit-Testing in Objective-C beschäftigt sich mit den Assertions. Sie beschreiben die Behauptungen bzw. Erwartungen, die innerhalb einer Testmethode an das Testobjekt gestellt werden. OCUnit stellt dafür verschiedene Makros zur Verfügung:
STAssertEquals
In Teil 1 haben wir bereits das Makro "STAssertEquals" benutzt.
Definition:STAssertEquals(a1, a2, description, ...);
Beispiel:STAssertEquals(4.0, exp2(2), @"2 times 2 shall be 4.");
Es vergleicht die Werte "a1" und "a2" miteinander und gibt eine Fehlermeldung inklusive des Wertes von "description" aus, wenn diese nicht gleich sind. Für "description" kann ein Formatstring (wie für "printf") definiert werden, entsprechend folgen die durch Komma getrennten Parameter.
STAssertEqualsWithAccuracy
Insbesondere bei Verwendung von Fließkommazahlen empfiehlt es sich, statt "STAssertEquals" das Makro "STAssertEqualsWithAccuracy" zu verwenden. Dieses erlaubt die Festlegung einer Abweichung, innerhalb der der erwartete Wert liegen darf.
Definition:STAssertEqualsWithAccuracy(left, right, accuracy, description, ...);
Beispiel:STAssertEqualsWithAccuracy(1.0, cos(0), 0.001,
@"cos(0) shall be 1, but we allow a tolerance of 0.001.");
Der Parameter "accuracy" legt die maximale Differenz zwischen beiden Werten fest, im Beispiel darf der Wert von "cos(0)" also um 0.001 nach oben und unten von 1.0 abweichen.
STAssertEqualObjects
Beim Vergleich zweier Objekte ist häufig nicht die Frage, ob die Instanzen selbst identisch sind, sondern ob ihre Werte bzw. die Werte ihrer Instanzvariablen gleich sind. Ein typischer Fall ist der Vergleich zweier NSString-Objekte. OCUnit bietet dafür das Marko "STAssertEqualObjects", welches auf die "isEqual"-Methode der jeweiligen Klasse zurückgreift:
Definition:STAssertEqualObjects(a1, a2, description, ...);
Beispiel:NSString *string = [NSString stringWithUTF8String:"Just a test"];
STAssertEqualObjects(string, @"Just a test", @"Both strings shall be equal.");
Wer den Unterschied zu "STAssertEquals" sehen will, kann probehalber auch folgenden Aufruf probieren:
STAssertEquals(string, @"Just a test", @"Both strings shall be equal.");
Während die erste Zusicherung erfüllt wird, schlägt die zweite fehl und Xcode zeigt die Meldung "<20dd1900> should be equal to <60610300>: Both strings shall be equal." an. Wie erwartet, werden die Speicheradressen der Objekte und nicht deren Werte verglichen.
STAssertTrue und STAssertFalse
Diese beiden Makros überprüfen boolesche Ausdrücke bzw. Funktionen, die boolesche Werte zurückgeben.
Definition:STAssertTrue(expression, description, ...);
STAssertFalse(expression, description, ...);
Beispiel:STAssertTrue([@"Hesse" isGreaterThan:@"Goethe"], @"Hesse is greater than Goethe");
STAssertFalse([@"Hesse" isGreaterThan:@"Rilke"], @"Hesse is not greater than Rilke");
Man sollte "STAssertTrue" nicht für Vergleiche mit "==" oder "isEqual" verwenden, da hier "STAssertEquals" sowie "STAssertEqualObjects" besser geeignet sind und die verglichenen Werte in der Fehlermeldung aufführen (fügt man diese per entsprechenden Token in den Formatstring von "description" ein, kann man selbiges auch mit "STAssertTrue" erreichen, dies ist aber nicht Sinn der Sache).
STAssertNil und STAssertNotNil
"STAssertNil" vergleicht den übergebenen Wert mit "nil", "STAssertNotNil" sichert zu, dass er nicht "nil" ist. Der vorrangige Einsatzzweck sind Funktionen, die Zeiger auf Objekte zurückgeben.
Definition:STAssertNil(a1, description, ...);
STAssertNotNil(a1, description, ...);
Beispiel:NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:@"TextMate" forKey:@"editor"];
STAssertNil([dict objectForKey:@"viewer"],
@"Shall return nil as key is not set in dictionary.");
STAssertNotNil([dict objectForKey:@"editor"],
@"Shall return not nil as key is set in dictionary.");
Im Beispiel wird ein "NSMutableDictionary" mit einem Eintrag mit dem Key "editor" erstellt, entsprechend liefert es für diesen Key ein Objekt zurück. Für andere Keys wird hingegen "nil" zurückgegeben.
STAssertThrows und STAssertNoThrow
Diese Assertions erwarten, dass eine Exception ausgelöst bzw. nicht ausgelöst wird.
Definition:STAssertThrows(expression, description, ...);
STAssertNoThrow(expression, description, ...);
Beispiel:NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObject:@"Xcode"];
STAssertThrows([array objectAtIndex:1],
@"Exception thrown as index is beyond bounds.");
STAssertNoThrow([array objectAtIndex:0],
@"No exception as index exists.");
Zusätzlich gibt es noch die Varianten "STAssertThrowsSpecific" und "STAssertThrowsSpecificNamed" sowie "STAssertNoThrowSpecific" und "STAssertNoThrowSpecificNamed", mit denen sich Typ und Name der Exception spezifieren lassen. Beispiel:
STAssertThrowsSpecificNamed([array objectAtIndex:1],
NSException, NSRangeException,
@"NSRangeException thrown as index is beyond bounds.");
Außerdem können mit "STAssertTrueNoThrow" und "STAssertFalseNoThrow" boolesche Ausdrücke ausgewertet und gleichzeitig überprüft werden, dass keine Exception erfolgt. Zwar zeigt Xcode auch ohne diese Varianten an, wenn eine Exception ausgelöst wurde, sie ermöglichen jedoch die Darstellung der Fehlermeldung direkt in der betroffenen Zeile.
STFail
Last, but not least, lässt das Makro "STFail" jeden Testfall fehlschlagen. Die Motivation dafür ist typischerweise, dass man die betroffene Methode als fehlerhaft oder unfertig markieren und dadurch deutlich machen will, dass noch Handlungsbedarf besteht.
Definition:STFail(description, ...);
Beispiel:STFail(@"Test not finished yet.");
Damit endet dieser Artikel. Im nächsten Teil der Serie werden wir uns mit Mock-Objekten beschäftigen.
Schlüsselwörter: objective-c, programmierung, testen, xcode
Drei Links zum Sonntag (III)
Diese Woche war es sehr ruhig hier, aber auf die Links zum Sonntag wollen wir nicht verzichten. Dieses Mal ein Link zum Exception-Handling in Mac- und iPhone-Programmen und zwei zum Thema Xcode:
- Open Source Crash Reporter for iPhone
Landon Fuller veröffentlichte im Lauf der Woche ein Framework für das Exception-Handling innerhalb von Mac- und iPhone-Programmen. Crash-Reports werden als Protocol Buffers gespeichert und können beim nächsten Programmstart beispielsweise per E-Mail versendet werden. - 14 Essential Xcode Tips, Tricks and Resources for iPhone Devs
Dan Grigsby fasst bei MobileOrchard die 14 wichtigsten Tips zu Xcode zusammen. Von der Fensterkonfiguration über Shortcuts bis hin zu Skripten ist alles dabei. - Complete Xcode Keyboard Shortcut List
Colin Wheeler stellt in einem Artikel, der schon aus dem Februar 2008 stammt, sämtliche Shortcuts in Xcode in Form einer Übersicht als PNG- oder PDF-Datei zur Verfügung.
Mein persönlicher Lieblingsshortcut ist übrigens ⌘⇧D, die meiner Meinung nach schnellste Möglichkeit, um Dateien in Xcode zu öffnen.
Schlüsselwörter: iphone, linktips, mac, xcode
Auch abrufbar als: Atom
Schlüsselwörter
- berlin (2)
- blog (5)
- browser (2)
- cocoaheads (5)
- dropbox (1)
- git (7)
- idisk (1)
- iphone (28)
- javascript (2)
- kurztip (4)
- linktips (17)
- mac (9)
- macruby (1)
- objective-c (8)
- ortung (1)
- programmierung (22)
- rails (1)
- railsconf (7)
- ruby (6)
- ruby on rails (7)
- schnipsel (14)
- server (2)
- spiele (1)
- statistiken (3)
- stuttgart (3)
- testen (4)
- tidy (1)
- versionskontrolle (5)
- wwdc (1)
- xcode (9)
- xml (1)
