This repository has been archived by the owner on Apr 7, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 20
/
07-errors_and_testing.slide
514 lines (336 loc) · 23 KB
/
07-errors_and_testing.slide
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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
Грешки, тестове и документация
21.11.2018
http://fmi.golang.bg/
@fmi_golang
* В този епизод:
- грешки
- паника
- (припомняне за) отлагания
- тестове
- документация
- и бумащина
* Но преди това...
* Въпрос за мъфин #1
Какво прави `select`?
- Позволява multiplexing на четене/писане от канали
- "switch" за канали
* Error handling
* Имало едно време чисто С
- Неконсистентен `error`handling`
- Понякога се приема аргумент по указател, в който се записва евентуална грешка
- Понякога се ползва връщаната стойност
- Понякога това е комбинирано с `errno`
* Пример в C
.code code/errors_and_testing/c_err_example.c
* Имало едно време един език Go
Има грубо-казано 2 начина
- 1) Връщане на грешка като (част от) резултата от функция
- 2) Изпадане в паника (носете си хавлия)
* Връщане на грешка
- Има конвенция обектите, които се връщат, да отговарят на следния глобално-достъпен интерфейс:
type error interface {
Error() string
}
- Разбира се, всеки може да връща "по-сложни" обекти, даващи повече информация за грешката. Например, `os.Open` връща `os.PathError`:
type PathError struct {
Op string // "open", "unlink", etc.
Path string // Файлът с грешката
Err error // Грешката, върната от system call-a
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
* Стандартна употреба
func ReadFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
//...
}
или малко по-сложно:
func CreateFile(filename string) (*os.File, error) {
var file, err = os.Create(filename)
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Free some space
return os.Create(filename)
}
return file, err
}
* Errors are values
Често оплакване на Go програмисти е количеството проверки за грешки:
if err != nil {
return err
}
- Това е донякъде вярно, особено ако даден код се обръща често към "външния свят" (`os`, `io`, `net`, etc.)
- За разлика от други езици, които може да имат exceptions и try-catch, в Go грешките се третират като нормални стойности
- Това е умишлено, защото помага за обработката на всички грешки на правилното място
- Така нямаме глобални "try-catch" блокове и изненадващи exceptions, които идват от 20 нива навътре в call stack-а
- Повече подробности на [[https://blog.golang.org/errors-are-values]]
* Пример
if _, err := fd.Write(p0[a:b]); err != nil {
return err
}
if _, err := fd.Write(p1[c:d]); err != nil {
return err
}
if _, err := fd.Write(p2[e:f]); err != nil {
return err
}
Може да стане:
var err error
write := func(buf []byte) {
if err == nil {
_, err = w.Write(buf)
}
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
if err != nil {
return err
}
* Създаване на грешки
- може да връщате собствен тип, който имплементира `error` интерфейса
- може да използвате функцията `New(string)` от пакета `errors`:
func someFunc(a int) (someResult, error) {
if a <= 0 {
return nil, errors.New("a must be positive!")
}
// ...
}
- може да използвате `fmt.Errorf`:
func someFunc(a int) (someResult, error) {
if a <= 0 {
return nil, fmt.Errorf("a is %d, but it must be positive!", a)
}
// ...
}
- може да си направите и собствен "конструктор"
* Припомняне на defer
- `defer` е специален механизъм на езика
- `defer` добавя *извикване* на функция в един списък (стек)
- Когато обграждащата функция приключи, тези извиквания се изпълняват в обратен ред
.play code/errors_and_testing/defer_example.go /^func main/,/^}/
- defer се използва за сигурно и лесно почистване на ресурси (отворени файлове, заключени mutex-и, etc.) ... и справяне с панирани програми!
* Паника!
- Нещо като изключенията
- Ползваме ги само в крайни случаи (не като изключенията)
- Изпадайки в паника, подавате стринг с грешката
- Добрата новина е, че можете да се съвземате от тях... пак буквално
* Уточнения
- `panic` е вградена функция
- Тя спира нормалното изпълнение на програмата
- Когато функция F изпълни `panic`, изпълнението на F спира, всички `defer`-нати функции на F се изпълняват нормално, след което изпълнението се връща във функцията, извикала F
- За извикващия на F, F е все едно извикване на `panic`
- Това продължава, докато всички функции в текущата горутина (`thread`) не свършат, когато програмата гърми
- Паники се случват след директното извикване на функцията `panic`, както и след разни runtime грешки, като `out-of-bounds`array`access`
* Избягвайте ненужното изпадане в паника
.image assets/panic.jpg 550 500
* recover
- Съвзема от паника
- `recover` е безполезен без `defer` ( може да се съвземете само в defer )
- `recover` не прави нищо (връща `nil`), ако текущата горутина не е в паника
- Ако е, `recover` връща аргумента, подаден на `panic`
* Example
.code code/errors_and_testing/panic.go /^func f/,/END OMIT/
.play code/errors_and_testing/panic.go /^func main/,/^}/
* Грешки в "Go 2"
Go 2?!
До сега не сме ви говорили за Go2, така че настанете се удобно...
* Go 2
- За сега изглежда, че все някога гаранцията за [[https://golang.org/doc/go1compat][go1compat]] ще бъде нарушена
- Всички такива неща са наречени "Go 2"
- На практика може никога да излезе втора версия
- А езика малко по малко да еволюира като добавки в Go1
* Грешки в "Go 2"
Има две интересни секции в [[https://go.googlesource.com/proposal/+/master/design/go2draft.md][черновата за Go2 предложение]]:
- Спряване с грешки (Error Handling)
- Стойности на грешките (Error Values)
* Тестове и документация
* Disclamer
Днес няма да си говорим за acceptance testing, quality assurance или нещо, което се прави от QA отдела във фирмата.
Всичко тук е дело на програмиста.
* Митът
Проектът идва с готово, подробно задание.
Прави се дизайн.
С него работата се разбива на малки задачи.
Те се извършват последователно.
За всяка от тях пишете кода и приключвате.
Изискванията не се променят, нито се добавя нова функционалност.
* Митът v2.0
Щом съм написал един код, значи ми остава единствено да го разцъкам - няколко print-а, малко пробване в main функцията и толкова.
Така или иначе няма да се променя.
А ако (не дай си боже) това се случи - аз съм го писал, знам го, няма как да допусна грешка.
Най-много да го поразцъкам още малко.
* Тежката действителност
Заданията *винаги* се променят.
Често се налага един код да се преработва.
Писането на код е сложна задача - допускат се грешки.
Програмистите са хора - допускат грешки.
Промяната на модул в единия край на системата като нищо може да счупи модул в другия край на системата.
Идва по-добра идея за реализация на кода, по ред причини.
* Искаме да автоматизираме нещата
За всичко съмнително ще пишем сценарий, който да "цъка".
Всеки сценарий ще изпълнява кода и ще прави няколко твърдения за резултатите.
Сценариите ще бъдат обединени в групи.
Пускате всички тестове с едно бутонче.
Резултатът е "Всичко мина успешно" или "Твърдения X, Y и Z в сценарии A, B и C се оказаха неверни".
Искаме да тестваме и производителността на нашия код.
* Видове тестове
- *Unit*tests* - проверяват дали дадено парче код/пакет работи правилно в изолация
- *Integration*tests* - проверяват дали няколко модула си общуват правилно
- *Functional*tests* - проверяват дали крайната функционалност е както се очаква
- *Benchmark*tests* - извикват една и съща операция `n` пъти и записват времето, отнело за изпълнение
* За какво ни помагат тестовете
- Откриват грешки по-рано
- Позволяват ни уверено да правим промени в системата
- Улесняват работата в екип и приемствеността на проекта
- Дават сигурност на клиенти, колеги, шефове и на самите нас
- Представляват пример как се работи с кода
- Помагат разделянето на интерфейс от имплементация
- Служат като документация и спецификация
- Посочват ни слабите от към производителност части
* За какво не служат тестовете
- Не доказват, че приложението работи
- Не доказват, че приложението е с достатъчно добра производителност
- Не са Quality Assurance
* testing
Разбрахме се, че тестовете са ни супер важни.
Очевидно в стандартната библиотека на Go, има пакет за това.
За да тестваме `foo.go`, създаваме `foo_test.go` в същата директория, който тества `foo.go`
Ако тестваме пакета `foo` можем:
- Тестовите файлове също да са в пакета `foo`
- Така имаме достъп до всичко от пакета. Публично или не
- Тестовите файлове да са в пакет `foo_test`
- Така имаме достъп само до публичните неща от пакета
- Или да ги смесваме
С `go`test`./...` пускаме тестовете на един цял проект :)
* Тестовете в testing
- Дефинират се като функции, които приемат указател към `testing.T`
- Функциите трябва да започват с `Test` и слеващата буква да е главна
- Един тест минава успешно, ако не се изпълни `t.Error[f]?()`, `t.Fail()`, `t.Fatal()`...
.code code/errors_and_testing/testing.go /func Test/,/END TEST/
* Оркестрация на тестове
- За тестове които искаме да пропуснем викаме `t.Skip()`
- Тестовете вървят последователно освен ако не бъде извикано `t.Parallel()`
- Препоръчва се да се извика в началото на функцията
- Сигнализира че тест може да бъде изпълняван парелно с и само с други тестове извикали `t.Parallel()`
* Benchmark тестове
- Дефинират се като функции, които приемат указател към `testing.B`
- Функциите трябва да започват с `Benchmark` и слеващата буква да е главна
- Тя се състои от `for` цикъл, извикващ `b.N` пъти тестваната функция
- `go` е достатъчно умен да реши колко пъти да я извика, за да получи адекватни резултати
- Стъпките са 1, 100, 10,000, 1,000,000 50,000,000.
.link https://github.com/ChristianSiegert/go-testing-example
.code code/errors_and_testing/testing.go /func Bench/,/END BENCH/
* Demo
* Малко почивка с тестовете...
* Документиране на кода
`go` генерира автоматична документация на нашия код, вземайки под внимание:
- всеки коментар, в началото на файл
.code code/errors_and_testing/testing.go /\/\*/,/package fibonacci/
- всеки коментар, дефиниран над функция, метод, тип
.code code/errors_and_testing/testing.go /Fastest Fibonacci/,/func Fibonacci/
- всеки коментар до име в тип, var, const
.code code/errors_and_testing/testing.go /lookupTable stores/,/var lookupTable/
* Виждане на документацията
На всички локално инсталирани пакети
godoc -http=:6060
Документация на (почти) всички go пакети качени в BitBucket, GitHub, Launchpad и Google Project Hosting
.link http://godoc.org/ godoc.org
* The testing must go on...
* Сечението на testing.B и testing.T
- често се налага да има общ код между тестове и бенчмаркове
- с цел да не се повтаряме има testing.TB
- това е общия интерфейс между testing.B и Testing.T
- включва всичките вариации на Error, Fail, Fatal, Log и Skip
.code code/errors_and_testing/table_test.go /func testingFunc/,/END testingFunc OMIT/
* Таблично базирани тестове
- Често се налага да тестсваме един и същи сценариѝ с различни параметри
.code code/errors_and_testing/table_test.go /func TestFibonacci/,/TABLE TEST OMIT/
- От 1.7 има и подтестове които позволяват това да изглежда малко по добре в терминал
* SubTests
.code code/errors_and_testing/table_test.go /func TestSubFibonacci/,/SUB TEST OMIT/
* SubTests continues
--- FAIL: TestFibonacci (0.00s)
table_test.go:68: Expected 1 for Fiboncci(2) but got 21
--- FAIL: TestSubFibonacci (0.00s)
--- FAIL: TestSubFibonacci/Fibonacci(2) (0.00s)
table_test.go:68: Expected 1 for Fiboncci(2) but got 21
--- FAIL: TestSubFibonacci/Fibonacci(10) (0.00s)
table_test.go:68: Expected 55 for Fiboncci(10) but got 21
FAIL
- Важно е да се отбележи че подтестовете са независими един от друг. Дори един да се провали, другите ще се изпълнят
- Тъй като са именувани, то не се налага да идентифицираме в кой случай се е случила грешката
* SubBenchmarks
- В случая на Benchmark-овете това е дори още по полезно
- Понеже Benchmark-овете се измерват от началото на Benchmark функцията до нейното завършване, правенето на таблични Benchmark-ове е невъзможно
- Но в 1.7 можем да ползвам под Benchmark-ове
.code code/errors_and_testing/table_test.go /func BenchmarkSubFibonacci/,/SUB BENCHMARK OMIT/
* SubBenchmarks output:
--- FAIL: BenchmarkSubFibonacci/BFibonacci(2)
table_test.go:68: Expected 1 for Fiboncci(2) but got 21
BenchmarkSubFibonacci/BFibonacci(8)-16 1000000000 2.65 ns/op
--- FAIL: BenchmarkSubFibonacci/BFibonacci(10)
table_test.go:68: Expected 55 for Fiboncci(10) but got 21
--- FAIL: BenchmarkSubFibonacci
FAIL
* All together
- Под тестовете разбира се може да бъдат рънвани паралелно или не
- Може да има под тестове на под тестовете и т.н.
.code code/errors_and_testing/table_test.go /func TestGroupSubFibonacci/,/SUB GROUP OMIT/
* All together output
--- FAIL: TestGroupSubFibonacci (1.00s)
--- FAIL: TestGroupSubFibonacci/group1 (0.00s)
--- FAIL: TestGroupSubFibonacci/group1/NonParallel (0.00s)
table_test.go:96: Just cause
table_test.go:98: Oops
--- FAIL: TestGroupSubFibonacci/group1/Fibonacci(2) (1.00s)
table_test.go:69: Expected 1 for Fiboncci(2) but got 21
--- FAIL: TestGroupSubFibonacci/group1/Fibonacci(10) (1.00s)
table_test.go:69: Expected 55 for Fiboncci(10) but got 21
FAIL
* Example тестове - шантавата част
- Документация и тест в едно вътре в тестов файл
- Функцията започва с `Example`, последвана от името на типа или функцията
Foo -> ExampleFoo
- За метод `Bar` го слагаме с подчертавка след типа `ExampleFoo_Bar`
- Пишем няколко реда, в които използваме нашия тип
- Можем да завършим с коментар започващ с `Output:` и ще бъде тествано че изхода на кода съвпада с останалата част от коментара
- Влиза в документацията на пакета като пример
.code code/errors_and_testing/testing.go /func Example/,/END EXAMPLE/
* Имитации
* Иматации или Mock-ове
- Често се случва да искаме да тестваме код който си говори с друг код или тотално отделна система
- Примерно paypal
- Искаме да сме сигурно че работим правилно с api-то на Paypal във всичките случаи които имаме
- Но не искаме да плащаме на paypal пари всеки път като си пуснем тестовете
- Също важи и когато не искаме да ни фейлват тестовете за една част от кода, само защото ползва друга, която в момента е бъгава
- Тогава правим имитации или Mock-ове - код, който подръжава на друг, но без да ни изпрезва кредитните карти
* В Go
- Ако искам да можем да mock-нем нещо в тестовете си, ще е необходимо да можем да го подменим в тях
- За разлика от някои други по - динамични езици, в които може да подменим time.Sleep или os.Read с наши имплементации, в Go трябва да сме малко по - имплицитни
- Обикновенно трябва да се приемат интерфейс(и), които имплементират функционалността, която искаме, примерно Sleeper
type Sleeper interface {
Sleep(time.Duration)
}
- След което просто се подава нашата mock-ната имплементация
* Библиотеки за имитации
- Някои библиотеки ви дават "по - лесен" начин
- Препоръчваме да се обръщате към тях, чак когато наистина ви потрябват
- На нас до сега не са ни трябвали
- Въпреки това ще ги спомнем
* testify
.link https://github.com/stretchr/testify
- Цяла библиотека за писане на тестове
- Но има `mock` пакет
* mockery
.link https://github.com/vektra/mockery
- Библиотека занимаваща се специално с... mockery
* mock
.link https://github.com/golang/mock
- Както забелязвате, в github организацията на golang е
- Подсказва някаква връзка с авторите на езика
- Често от подобни места пакети стигат до стандартната библиотека