-
Notifications
You must be signed in to change notification settings - Fork 0
/
testing-vortrag.txt
358 lines (265 loc) · 8.92 KB
/
testing-vortrag.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
Warum sollte ein Entwickler Unit Tests für seinen Code erstellen? Lohnt sich der Aufwand oder ist das nur Zeitverschwendung? Im Vortrag erfahrt Ihr die Antworten und lernt einfache Techniken kennen, wie man seinen Code _testbarer_ und dadurch _wartbarer_ machen kann.
Wozu testen?
Meine "best practices" bei Unit Tests
Ausblick: weitere Testarten
Typische Gedanken bei umfangreichem Code:
Ich will die Methode nicht anfassen, dann geht bestimmt etwas kaputt. (-> 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 so etwas
-> Wartung und Debugging kann zu einem "Fass ohne Boden" werden
Das Problem:
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
- 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
- 2. System wird in Zukunft funktionieren
- imo DER Grund für Testen
- Änderungen und Refactorings ohne Angst: Sicherheitsnetz
-> man wird mutiger bei Änderungen
- sofortiges Feedback bei neuen Bugs
-> "Zum Glück hab ich diesen Test"
- für jeden gefundenen Bug einen Test schreiben, der den Bug auslöst
-> alte Bugs kommen nicht wieder
- statt "testen" besser "sicherstellen"
- 3. Man wird ein besserer Entwickler
- subjektives Ziel
- leicht zu testender Code ist automatisch einfacher und leichter wartbar
- 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
- 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
-> man kann test-süchtig werden!
Übersicht: (Pyramide)
Unit Tests (für einzelne Klassen)
Integrationstests (für mehrere Klassen)
Benutzertests (für das komplette System)
Unit Tests
Warnung: Beispiele sind sehr trivial, es geht um das Vorgehen!
Eine Klasse ohne Abhängigkeiten:
public class Person {
private String name;
public void setName(String newName) {
name = newName.trim();
}
public String getName() {
return name;
}
}
@Test
public void shouldRemoveWhitespace() {
Person personSut = new Person();
personSut.setName(" Alex ");
String storedName = personSut.getName();
assertEquals("Alex", storedName);
}
- benutzt keine anderen (selbstgeschriebenen) Klassen
- nur eine Zustandsüberprüfung
- Ziel 2: System wird in Zukunft funktionieren
-> wenn trim() gelöscht wird, scheitert der Test
komplexeres Beispiel mit zwei Klassen
Grafik: (Client) -(start)-> (Service)
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.
- Warum unabhängig?
1. Gemeinsames Testen nicht immer möglich
- der Service läuft nicht immer oder ist noch nicht implementiert
2. Leichte Fehlerlokalisierung
- wenn ein Test fehlschlägt, weiß man genau in welcher Klasse der Fehler ist
3. Performance
- Jede Klasse wird nur einmal getestet
- ALLE Unit Tests sind auf Knopfdruck wiederholbar
-> man kann alle Unit Tests sehr häufig ausführen, z. B. nach jeder Änderung, jedem Bugfix oder Refactoring
4. Tests sind automatisch die aktuellste Dokumentation für andere Entwickler
- leichtere Einarbeitung in den Produktivcode
5. Nebeneffekt: besseres Design durch lose Kopplung
Verhindern von "new" in Methode:
- Klasse von oben links
- rechts:
public class Client {
private Service srv = new Service();
// for unit tests
void setService(Service newSrv) {
srv = newSrv;
}
public void useService() {
srv.sendMessage("start");
//...
}
}
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 können wir "testen", bzw. "sicherstellen"?
- wenn useService() aufgerufen wird, dann muss die Nachricht "start" gesendet werden
-> Ziel 2: System wird in Zukunft funktionieren
- Wenn jemand die Nachricht zu "startService" ändert, wird der Test scheitern
Mock: Ersatz für ein Objekt (to mock = nachahmen).
Grafik: (Client) -(start) -x-> (Service)
|-> (ServiceMock)
Java-Framework zum Erstellen von Mocks: Mockito
@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");
}
Man hat Macht über den Mock, z. B.:
1. Prüfen, welche Methoden im Test aufgerufen wurden -> verify(serviceMock).sendMessage("start")
2. Beliebige Werte zurückgeben -> when(serviceMock.getCurrentYear()).thenReturn(3000)
3. 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 (check)
- 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
links: Original
- Konstruktor für Tests
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");
//...
}
}
im Produktivcode:
...
Client c = new Client();
c.useService();
...
- Fabrikmethode
links: Original
public class Client {
// keine Instanzvariable!
// for unit tests
Service createService() {
return new Service();
}
public void useService() {
Service srv = createService();
srv.sendMessage("start");
//...
}
}
im Produktivcode:
...
Client c = new Client();
c.useService();
...
+ Thread-sicher, kein Feld nötig, zB bei Web Service, Servlet
+ auch für nicht-default Konstruktor des Service
- schwerer verständliches Testen mit spy()
- Objektfabrik/Provider
links: Original
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");
//...
}
}
im Produktivcode:
...
Client c = new Client();
c.useService();
...
+ flexibel
- zwei Mocks im Test nötig: providerMock, serviceMock
- direkte Übergabe
links: Original
public class Client {
public void useService(Service srv) {
srv.sendMessage("start");
//...
}
}
//Aufruf im Produktivcode:
Client c = new Client();
c.useService(new Service());
+ Client-Code einfacher
- API komplexer
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
public static void main(String[] args) {
new Main().execute(args);
}
void execute(String[] args) {
...
}
Ausnahme: kleine Utility-Methoden, die nur in-memory arbeiten
- Zugriffe auf Dateien und Netzwerk: in eigene Klassen auslagern
-> nicht einfach new FileInputStream("config.txt")
MyClass ---> FileAccess
- InputStream readFile()
- void writeFile()
InputStream config = new ByteArrayInputStream("user = admin".getBytes());
when(fileAccessMock.readFile()).thenReturn(config);
- keine "train wrecks": getServiceLocator().getService().getDescription().asXml()
-> viele Mocks nötig
Fazit: Wenn man erstmal "drin" ist, will man nicht mehr zurück zum "normalen" Programmieren.
Ausblick:
- noch wenig bis keine Erfahrung bei GUI, Web, Datenbanken
- Integration Tests mit Spring -> Einschleusen von Mocks durch Dependency Injection
- User Tests z. B. mit Selenium