-
Notifications
You must be signed in to change notification settings - Fork 8
/
chapter-09.tex
638 lines (518 loc) · 39.1 KB
/
chapter-09.tex
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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
\chapter{Практикум: каркас для unit-тестирования}
\label{ch:09}
\thispagestyle{empty}
В~этой главе вы вернётесь к написанию кода и разработаете простой каркас для
unit-тестирования Lisp. Это даст вам возможность использовать в реальном коде некоторые
возможности языка, о которых вы узнали после главы~\ref{ch:03}, включая макросы и динамические
переменные.
Вашей главной целью при проектировании каркаса для тестирования будут: лёгкость добавления
новых тестов, запуск различных наборов тестов и отслеживание проваленных тестов. Вы
сосредоточите усилия на проектировании каркаса, который можно использовать при
интерактивной разработке.
Главная особенность автоматизированного тестирования состоит в том, что каркас отвечает за
проверку, все ли тесты выполнились успешно. Вам не требуется тратить время на то, чтобы
пробираться сквозь результаты, сверяя их с ожидаемыми,~-- ком\-пью\-тер может сделать это
гораздо быстрее и аккуратнее вас. Как следствие каждый тест должен быть выражением,
которое вырабатывает логическое значение~-- истина или ложь, тест выполнен успешно или
провалился. К примеру, если вы тестируете встроенную функцию~\lstinline{+}, следующие
выражения являются вполне разумными тестами\footnote{Разумеется, это только для большей
наглядности~-- написание тестов для встроенных функций, таких как~\lstinline{+}, может
выглядеть несколько несуразно. Ведь если даже столь простые вещи не работают, трудно
ожидать, что и тесты отработают так, как было задумано. С другой стороны, большинство
реализаций Common Lisp написано на самом Common Lisp~-- и в этом случае наборы тестов
для функций стандартной библиотеки уже не выглядят нелепостью.}\hspace{\footnotenegspace}:
\begin{myverb}
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)
\end{myverb}
Функции с побочными эффектами необходимо тестировать слегка по-другому~-- вам придётся
вызвать функцию и затем проверить наличие ожидаемых побочных эффектов\pclfootnote{Побочные
эффекты также могут использоваться для сообщения об ошибках; про систему обработки
ошибок в Common Lisp я расскажу в главе~\ref{ch:19}. После прочтения этой главы вы можете
подумать над тем, как объединить оба варианта.}. Но в любом случае каждый тест сводится
к логическому выражению: сработало или не сработало.
\section{Два первых подхода}
Если бы вы тестировали вручную, вы бы вводили эти выражения в REPL и проверяли бы, что они
возвращают \lstinline{T}. Но вам нужен каркас, который позволяет с лёгкостью
организовывать и запускать эти тесты в любое время. Если вы хотите начать с самой простой
работающей версии, то можете просто написать функцию, которая вычисляет все тесты и
возвращает~\lstinline{T} в случае успешного прохождения всех тестов (для этого используя
\lstinline{AND}).
\begin{myverb}
(defun test-+ ()
(and
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))
\end{myverb}
Для запуска тестов просто вызовите \lstinline{test-+}.
\begin{myverb}
CL-USER> (test-+)
T
\end{myverb}
Пока функция возвращает \lstinline{T}, вы знаете, что тесты проходят. Такой способ организации
тестов также весьма выразителен~-- вам не нужно писать много кода, об\-слу\-жи\-ваю\-ще\-го
тестирование. Однако при первом же проваливающемся тесте вы заметите, что отчёт о
тестировании оставляет желать лучшего: если \lstinline{test-+} возвращает \lstinline{NIL}, вы
знаете, что какой-то тест провалился, но не имеете понятия, какой именно.
Давайте попробуем другой простой (можно даже сказать~-- глупый) подход: чтобы проверить,
что случилось с каждым тестом, напишем так:
\begin{myverb}
(defun test-+ ()
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2) 3) '(= (+ 1 2) 3))
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(format t "~:[FAIL~;pass~] ... ~a~%" (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))
\end{myverb}
Теперь каждый тест будет сообщать результат отдельно. Код \lstinline!~:[FAIL~;pass~]! в
строке \lstinline{FORMAT} выводит \lstinline{FAIL} если первый аргумент ложен, и
\lstinline{pass}~-- если истинен\footnote{Более подробно и эта, и другие управляющие
команды \lstinline{FORMAT} будут обсуждаться в
главе~\ref{ch:18}.}\hspace{\footnotenegspace}. Теперь запуск \lstinline{test-+} покажет
подробности происходящего.
\begin{myverb}
CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
NIL
\end{myverb}
В~этот раз отчёт выглядит гораздо лучше, но сам код ужасен. Повторяющиеся вызовы
\lstinline{FORMAT} и утомительное дублирование тестовых выражений напрашиваются на
рефакторинг. Дублирование выражений особо раздражает, потому что если вы опечатаетесь, то
и результаты тестирования будут промаркированы неверно.
Другая проблема состоит в том, что вы не получаете единого ответа, прошли ли все
тесты успешно. Для трёх тестов достаточно легко проверить, что вывод не содержит
строчек \lstinline{FAIL}, но при наличии сотен тестов это начнёт надоедать.
\section{Рефакторинг}
Что вам действительно нужно~-- это способ писать тесты так элегантно, как в первой
функции \lstinline{test-+}, которая возвращает \lstinline{T} или \lstinline{NIL}, но также отчитывается о
результатах индивидуальных тестов, так, как во второй версии. Поскольку вторая версия
близка по функциональности к тому, что вам нужно, лучшее, что вы можете сделать,~--
проверить, можно ли исключить из неё раздражающее дублирование.
Простейший способ избавиться от повторяющихся похожих вызовов \lstinline{FORMAT}~-- создать
новую функцию.
\begin{myverb}
(defun report-result (result form)
(format t "~:[FAIL~;pass~] ... ~a~%" result form))
\end{myverb}
Теперь вы можете писать \lstinline{test-+}, вызывая \lstinline{report-result} вместо
\lstinline{FORMAT}. Не слишком упрощает жизнь, но, по крайней мере, если вы решите изменить вид
выдаваемых результатов, то вам придётся менять код только в одном месте.
\begin{myverb}
(defun test-+ ()
(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4)))
\end{myverb}
Следующее, что нужно сделать,~-- избавиться от дублирования тестового выражения с
присущим дублированию риском неправильной маркировки результата тестирования. Что вам
нужно~-- это возможность обработать тестовое выражение одновременно как код (для
получения результата теста) и как данные (для использования в качестве метки
теста). Использование кода как данных~-- это безошибочный признак того, что вам нужен
макрос. Или, если посмотреть на это с другой стороны, вам нужен способ автоматизировать
подверженное ошибкам написание вызовов \lstinline{report-result}. Неплохо было бы написать
что-то, похожее на
\begin{myverb}
(check (= (+ 1 2) 3))
\end{myverb}
\noindent{}и чтобы это означало следующее:
\begin{myverb}
(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
\end{myverb}
Написание макроса для выполнения этого преобразования тривиально.
\begin{myverb}
(defmacro check (form)
`(report-result ,form ',form))
\end{myverb}
Теперь вы можете изменить \lstinline{test-+}, чтобы использовать \lstinline{check}.
\begin{myverb}
(defun test-+ ()
(check (= (+ 1 2) 3))
(check (= (+ 1 2 3) 6))
(check (= (+ -1 -3) -4)))
\end{myverb}
Раз уж вы устраняете дублирование, почему бы не избавиться от повторяющихся вызовов
\lstinline{check}? Можно заставить \lstinline{check} принимать произвольное количество аргументов и
заворачивать каждый из них в вызов \lstinline{report-result}.
\begin{myverb}
(defmacro check (&body forms)
`(progn
,@(loop for f in forms collect `(report-result ,f ',f))))
\end{myverb}
Это определение использует общепринятую идиому~-- оборачивание набора форм в вызов
\lstinline{PROGN}, чтобы сделать их единой формой. Заметьте, как можно использовать \lstinline{,@}
для вклеивания результата выражения, которое возвращает список выражений, которые сами по
себе созданы с помощью шаблона, созданного обратной кавычкой.
С новой версией \lstinline{check} можно написать новую версию \lstinline{test-+} следующим образом:
\begin{myverb}
(defun test-+ ()
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))
\end{myverb}
\noindent{}что эквивалентно следующему коду:
\begin{myverb}
(defun test-+ ()
(progn
(report-result (= (+ 1 2) 3) '(= (+ 1 2) 3))
(report-result (= (+ 1 2 3) 6) '(= (+ 1 2 3) 6))
(report-result (= (+ -1 -3) -4) '(= (+ -1 -3) -4))))
\end{myverb}
Благодаря макросу \lstinline{check} этот вариант столь же краток, как первая версия
\lstinline{test-+}, но раскрывается в код, который делает то же самое, что вторая версия. Кроме
того, вы можете внести любые изменения в поведение \lstinline{test-+}, изменяя только
\lstinline{check}.
\section{Чиним возвращаемое значение}
Вы можете начать с изменения \lstinline{test-+} таким образом, чтобы его возвращаемое
значение показывало, все ли тесты завершились успешно. Поскольку \lstinline{check}
отвечает за генерацию кода, который запускает тесты, вам нужно изменить его так, чтобы
генерируемый код подсчитывал результаты тестов.
Для начала можно внести небольшое изменение в \lstinline{report-result}, чтобы он возвращал
результат выполняемого им теста.
\begin{myverb}
(defun report-result (result form)
(format t "~:[FAIL~;pass~] ... ~a~%" result form)
result)
\end{myverb}
Теперь, когда \lstinline{report-result} возвращает значение теста, кажется, что вы можете
прос\-то изменить \lstinline{PROGN} на \lstinline{AND}. К сожалению, \lstinline{AND} не
будет работать так, как вам хочется в этом случае, из-за своего прерывания, как только
один из тестов провалится, \lstinline{AND} про\-пус\-тит остальные. С другой стороны, если бы
вы имели конструкцию, которая действует как \lstinline{AND}, но не прерываясь, вы могли бы
её использовать на месте \lstinline{PROGN}. Common Lisp не предоставляет такой
конструкции, но это не помешает вам использовать её: вы с лёгкостью можете написать
макрос, предоставляющий такую конструкцию.
Оставляя тесты в стороне на минуту, вам нужен макрос (назовём его \lstinline{combine-results}),
который позволит вам сказать
\begin{myverb}
(combine-results
(foo)
(bar)
(baz))
\end{myverb}
\noindent{}и это будет значить
\begin{myverb}
(let ((result t))
(unless (foo) (setf result nil))
(unless (bar) (setf result nil))
(unless (baz) (setf result nil))
result)
\end{myverb}
Единственный нетривиальный момент в написании этого макроса~-- это введение переменной
(\lstinline{result} в предыдущем кусочке кода) в раскрытие макроса. Как вы видели в предыдущей
главе, использование обычных имён для переменных в раскрытом макросе может заставить
протекать абстракцию, так что вам нужно будет создать уникальное имя, что делается с
помощью \lstinline{with-gensyms}. Вы можете определить \lstinline{combine-results} так:
\begin{myverb}
(defmacro combine-results (&body forms)
(with-gensyms (result)
`(let ((,result t))
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
,result)))
\end{myverb}
Теперь вы можете исправить \lstinline{check}, просто заменив \lstinline{PROGN} на
\lstinline{combine-results}.
\begin{myverb}
(defmacro check (&body forms)
`(combine-results
,@(loop for f in forms collect `(report-result ,f ',f))))
\end{myverb}
С этой версией \lstinline{check} \lstinline{test-+} должен выдавать результаты своих трёх тестов и
затем возвращать \lstinline{T}, показывая, что все тесты завершились успешно\footnote{Если
функция \lstinline{test-+} была откомпилирована~-- а это могло случиться и неявно в
некоторых реализациях Common Lisp,~-- вам потребуется заново определить её, чтобы
изменения вступили в силу. В~интерпретируемом же коде макросы обычно раскрываются каждый
раз заново~-- при каждом выполнении функции,~-- позволяя пронаблюдать эффект от
изменения макроса сразу.}\hspace{\footnotenegspace}.
\begin{myverb}
CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
T
\end{myverb}
Если вы измените один из тестов так, чтобы он проваливался\footnote{Просто измените один
их тестов таким образом, чтобы он проваливался,~-- это проще, чем изменить поведение
функции \lstinline{+}.}\hspace{\footnotenegspace}, возвращаемое значение изменится на \lstinline{NIL}.
\begin{myverb}
CL-USER> (test-+)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
FAIL ... (= (+ -1 -3) -5)
NIL
\end{myverb}
\section{Улучшение отчёта}
Пока вы тестируете только одну функцию, результаты тестирования обозримы. Если какой-то
тест проваливается, всё, что вам нужно сделать,~-- это найти его в конструкции \lstinline{check}
и понять, почему он не срабатывает. Но если вы пишете много тестов, вы, возможно, захотите
структурировать их, а не запихивать все больше и больше тестов в одну функцию. Например,
предположим, что вы хотите добавить несколько тестов для функции \lstinline{*}. Вы могли бы
написать новую функцию тестирования.
\begin{myverb}
(defun test-* ()
(check
(= (* 2 2) 4)
(= (* 3 5) 15)))
\end{myverb}
Теперь у вас есть две тестовые функции, так что вы, возможно, захотите написать ещё одну
функцию, которая запускает все тесты. Это достаточно легко.
\begin{myverb}
(defun test-arithmetic ()
(combine-results
(test-+)
(test-*)))
\end{myverb}
В~этой функции вы используете \lstinline{combine-results} вместо \lstinline{check}, потому что и
\lstinline{test-+}, и \lstinline{test-*} сами позаботятся о выводе результатов своих тестов. Когда
вы запустите \lstinline{test-arithmetic}, то получите следующий результат:
\begin{myverb}
CL-USER> (test-arithmetic)
pass ... (= (+ 1 2) 3)
pass ... (= (+ 1 2 3) 6)
pass ... (= (+ -1 -3) -4)
pass ... (= (* 2 2) 4)
pass ... (= (* 3 5) 15)
T
\end{myverb}
Теперь представьте, что один из тестов провалился, и вам нужно найти проблему. Для пяти
тестов и двух тестовых функций это будет не так сложно. Но представьте себе, что у вас 500
тестов, разнесённых по 20 функциям. Неплохо было бы, чтобы результаты сообщали вам, в
какой функции находится каждый тест.
Поскольку код, который печатает результаты тестов, собран в \lstinline{report-result}, вам
нужен способ передать в неё информацию о том, в какой тестовой функции вы находитесь. Вы
можете добавить параметр, сообщающий это, в \lstinline{report-result}, но \lstinline{check}, который
генерирует вызовы \lstinline{report-result}, не знает, из какой функции он вызван, что
означает, что вам придётся изменить вызовы \lstinline{check}, передавая аргумент, который он
будет передавать дальше, в \lstinline{report-result}.
Это в точности та проблема, для решения которой были придуманы динамические
переменные. Если вы создадите динамическую переменную, которая привязывается к имени
тестовой функции, то \lstinline{report-result} сможет использовать её, а \lstinline{check} может
ничего о ней не знать.
Для начала определим переменную на верхнем уровне.
\begin{myverb}
(defvar *test-name* nil)
\end{myverb}
Теперь слегка изменим \lstinline{report-result}, чтобы включить \lstinline{*test-name*} в вывод
\lstinline{FORMAT}.
\begin{myverb}
(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
\end{myverb}
После этих изменений тестовые функции всё ещё работают, но выдают следующие результаты
из-за того, что \lstinline{*test-name*} нигде не привязывается к значению, отличному от
начального:
\begin{myverb}
CL-USER> (test-arithmetic)
pass ... NIL: (= (+ 1 2) 3)
pass ... NIL: (= (+ 1 2 3) 6)
pass ... NIL: (= (+ -1 -3) -4)
pass ... NIL: (= (* 2 2) 4)
pass ... NIL: (= (* 3 5) 15)
T
\end{myverb}
Для того чтобы правильно выдавать имена тестовых функций в выводе, вам нужно изменить их.
\begin{myverb}
(defun test-+ ()
(let ((*test-name* 'test-+))
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4))))
(defun test-* ()
(let ((*test-name* 'test-*))
(check
(= (* 2 2) 4)
(= (* 3 5) 15))))
\end{myverb}
Теперь результаты правильно помечены именами тестовых функций.
\begin{myverb}
CL-USER> (test-arithmetic)
pass ... TEST-+: (= (+ 1 2) 3)
pass ... TEST-+: (= (+ 1 2 3) 6)
pass ... TEST-+: (= (+ -1 -3) -4)
pass ... TEST-*: (= (* 2 2) 4)
pass ... TEST-*: (= (* 3 5) 15)
T
\end{myverb}
\section{Выявление абстракций}
При внесении изменений в тестовые функции вы снова получили дублирующийся код. Тестовые
функции не только дважды включают своё имя~-- первый раз при определении, второй раз при
связывании с глобальной переменной \lstinline{*test-name*},~-- но обе они начинаются совершенно
одинаково (вся разница~-- имя функции). Вы могли бы попытаться избавиться от
дублирования просто потому, что это некрасиво. Но если рассмотреть причину, вызвавшую
дублирование, более подробно, то можно извлечь довольно важный урок по использованию
макросов.
Причина, по которой обе функции начинаются одинаково, в том, что они обе предназначены для
тестирования. Дублирование возникает из-за того, что тестовая функция~-- это только одна
половина абстракции. Эта абстракция существует в вашей голове, но в коде нет возможности
сказать <<это~-- тестовая функция>> другим способом, кроме как написанием соответствующего
паттерна.
К сожалению, неполные абстракции~-- плохие помошники при написании
программ. Полуабстракция, описанная в коде соответствующим паттерном, гарантирует вам
массовое дублирование кода со всеми сопутствующими проблемами поддержки этого кода в
дальнейшем. Более того, так как подобные абстракции целиком существуют только в наших
мыслях, у нас нет никакой возможности убедиться, что разные программисты (или даже один и
тот же~-- но в разное время) одинаково понимают одну и ту же абстракцию. Дабы полностью
абстрагировать идею, вам нужно как-то выразить фразу <<это~-- тестовая функция>>
соответствующим паттерном. Другими словами, вам нужен макрос.
Так как паттерн, который вы пытаетесь написать, представляет собой вызов \lstinline{DEFUN}
и ещё немного кода~-- вам нужен макрос, раскрывающийся в вызов \lstinline{DEFUN}. Вы будете
использовать этот макрос вместо \lstinline{DEFUN} для определения тестовых функций, так что
имеет смысл назвать его \lstinline{deftest}.
\begin{myverb}
(defmacro deftest (name parameters &body body)
`(defun ,name ,parameters
(let ((*test-name* ',name))
,@body)))
\end{myverb}
Используя этот макрос, вы можете переписать \lstinline{test-+} следующим образом:
\begin{myverb}
(deftest test-+ ()
(check
(= (+ 1 2) 3)
(= (+ 1 2 3) 6)
(= (+ -1 -3) -4)))
\end{myverb}
\section{Иерархия тестов}
Теперь, когда у вас есть полноценные тестовые функции, может возникнуть вопрос: должна
ли функция \lstinline{test-arithmetic} также быть тестовой? Казалось бы, если вы определите
её с помощью \lstinline{deftest}, то её связывание с \lstinline{*test-name*} скроет связывания
\lstinline{test-+} и \lstinline{test-*}~-- и это отразится на выводе результатов тестов.
Но представьте, что у вас есть тысяча (или даже больше) тестов, которые нужно как-то
упорядочить. На самом нижнем уровне находятся такие функции, как \lstinline{test-+} и
\lstinline{test-*}, непосредственно выполняющие проверку. При наличии тысяч тестов их
потребуется каким-либо образом упорядочить. Такие функции, как \lstinline{test-arithmetic}, могут
группировать схожие тестовые функции в наборы тестов. Допустим, что некоторые
низкоуровневые тестовые функции могут использоваться в разных наборах тестов. Тогда вполне
возможна такая ситуация, что тест будет пройден в одном контексте и провалится в
другом. Если это случится, вам наверняка захочется узнать несколько больше, чем просто имя
провалившегося теста.
Если вы определите \lstinline{test-arithmetic} посредством \lstinline{deftest}, сделав небольшие
изменения при связывании с \lstinline{*test-name*}, то сможете получить отчёты с более
подробным описанием контекста выполнившегося теста:
\begin{myverb}
pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)
\end{myverb}
Поскольку процесс определения тестовых функций описан отдельным паттерном, изменить отчёт
можно, и не меняя кода самих тестовых функций\pclfootnote{В~любом случае~-- если наши
тестовые функции были скомпилированы, вам нужно будет перекомпилировать их после
внесения изменений в макрос.}. Сделать так, чтобы \lstinline{*test-name*} хранил список имён
тестовых функций вместо имени последней вызванной функции, очень просто. Вам нужно всего
лишь изменить связывание
\begin{myverb}
(let ((*test-name* ',name))
\end{myverb}
\noindent{}на такое:
\begin{myverb}
(let ((*test-name* (append *test-name* (list ',name))))
\end{myverb}
Так как \lstinline{APPEND} возвращает новый список, составленный из его аргументов, эта
версия будет связывать \lstinline{*test-name*} со списком, содержащим старое значение
\lstinline{*test-name*}, с новым именем, добавленным в конец списка\footnote{Как вы
увидите в главе~\ref{ch:12}, добавление в конец списка с помощью \lstinline{APPEND}~--
не самый эффективный способ построения списка. Но пока нам достаточно и этого~-- пока
глубина вложенности структуры тестов не слишком велика, это смотрится не так уж и
плохо. А при необходимости всегда можно просто чуть изменить определение
\lstinline{deftest}.}\hspace{\footnotenegspace}. После выхода из функции старое значение \lstinline{*test-name*}
восстанавливается.
Теперь вы можете переопределить \lstinline{test-arithmetic}, используя \lstinline{deftest} вместо
\lstinline{DEFUN}.
\begin{myverb}
(deftest test-arithmetic ()
(combine-results
(test-+)
(test-*)))
\end{myverb}
В~результате вы получите именно то, что хотели:
\begin{myverb}
CL-USER> (test-arithmetic)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)
pass ... (TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)
pass ... (TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)
pass ... (TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)
T
\end{myverb}
С ростом количества тестов вы можете добавлять новые уровни~-- и пока они будут
определяться через \lstinline{deftest}, вывод результата будет корректен. Так, если вы
определите таким образом \lstinline{test-math}:
\begin{myverb}
(deftest test-math ()
(test-arithmetic))
\end{myverb}
то получите вот такой результат:
\begin{myverb}
CL-USER> (test-math)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2) 3)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ 1 2 3) 6)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-+): (= (+ -1 -3) -4)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 2 2) 4)
pass ... (TEST-MATH TEST-ARITHMETIC TEST-*): (= (* 3 5) 15)
T
\end{myverb}
\section{Подведение итогов}
Вы могли бы продолжить работу над этим каркасом, добавляя новые возможности~-- но как
каркас для написания тестов без особого напряжения и с возможностью использовать
\lstinline{REPL} это очень неплохое начало. Ниже код приведён полностью, все 26 строк:
\begin{myverb}
(defvar *test-name* nil)
(defmacro deftest (name parameters &body body)
"Define a test function. Within a test function we can call
other test functions or use 'check' to run individual test
cases."
`(defun ,name ,parameters
(let ((*test-name* (append *test-name* (list ',name))))
,@body)))
(defmacro check (&body forms)
"Run each expression in 'forms' as a test case."
`(combine-results
,@(loop for f in forms collect `(report-result ,f ',f))))
(defmacro combine-results (&body forms)
"Combine the results (as booleans) of evaluating 'forms' in order."
(with-gensyms (result)
`(let ((,result t))
,@(loop for f in forms collect `(unless ,f (setf ,result nil)))
,result)))
(defun report-result (result form)
"Report the results of a single test case. Called by 'check'."
(format t "~:[FAIL~;pass~] ... ~a: ~a~%" result *test-name* form)
result)
\end{myverb}
Этот пример прекрасно иллюстрирует обычный ход программирования на языке \lstinline{Lisp}, так
что давайте рассмотрим процесс его написания ещё раз.
Вы начали с постановки задачи~-- вычислить совокупность булевых выражений и узнать,
все ли они возвращают \lstinline{true}. Простое \lstinline{AND} работало и синтаксически было
абсолютно верно, но вывод результатов оставлял желать лучшего. Тогда вы написали
немного действительно глуповатого кода, битком набитого повторениями и спо\-соб\-ствую\-щи\-ми
ошибкам выражениями, чтобы всё работало так, как вы хотели.
Естественно, что вы решили попробовать привести вторую версию программы к более ясному и
красивому виду. Вы начали со стандартного приёма~-- выделения части кода в отдельную
функцию~-- \lstinline{report-result}. Увы, но использование \lstinline{report-result} утомительно и
чревато ошибками~-- тестовое выражение приходится писать дважды. Тогда вы написали макрос
\lstinline{check} для автоматически корректного вызова \lstinline{report-result}.
В~процессе написания макроса \lstinline{check} вы добавили возможность оборачивать несколько
вызовов \lstinline{report-result} в один вызов \lstinline{check}, сделав новую версию \lstinline{test-+}
столь же краткой, как и первоначальную с \lstinline{AND}.
Следующей задачей было исправить \lstinline{check} таким образом, чтобы генерируемый этим
макросом код возвращал \lstinline{t} или \lstinline{nil} в зависимости от того, все ли тесты прошли
удачно. Прежде чем переиначивать \lstinline{check}, вы предположили, что у вас есть
непрерываемая \lstinline{AND} конструкция. В~этом случае правка \lstinline{check}~-- тривиальное
дело. Вы обнаружили, что хотя такой конструкции и нет, написать её самим совсем не
трудно. После написания \lstinline{combine-results} исправить \lstinline{check} было элементарным
делом.
Затем всё, что оставалось,~-- это сделать более удобным отчёт. Начав с исправления тестовых
функций, вы представили их как особый вид функций~-- и в результате написали макрос
\lstinline{deftest}, выделив паттерн, отличающий тестовые функции от всех прочих.
Наконец, с помощью макроса \lstinline{deftest}, разделившего определение тестовой функции от
лежащей в её основе структуры, вы получили возможность улучшить вывод результатов, не меняя
при этом самих тестовых функций.
Теперь, имея представление об основах~-- функциях, переменных и макросах, получив немного
опыта по их практическому применению, вы готовы начать изучение богатой стандартной
библиотеки функций и типов данных языка Common Lisp.
%%% Local Variables:
%%% mode: latex
%%% TeX-master: "pcl-ru"
%%% TeX-open-quote: "<<"
%%% TeX-close-quote: ">>"
%%% End: