Библиотека функций к Script-fu
Введение
Написание кода на Лисп это тестирование, я не знаю(это не значит что их нет, просто я их действительно не знаю) ни одного языка программирования в котором цикл: написание код - проверка(тестирование) был бы таким коротким. Кстати в Script-fu я работаю через буфер обмена, это не удобно! Там есть возможность работать из Емакс, через сервер Scrip-fu, но я эту возможность не использую(приятно видеть консоль), а с обычной схемой или лиспом, работа в передаче кода заключается в нажатии пары клавиш. Лисперы не пишут многостраничные листинги кода, а затем его тестируют, они пишут функцию, выполняют его в интерпретаторе и сразу тестируют. Всё это благодаря наличию в системе REPL. И всё таки не смотря на это настаёт момент, когда требуются отдельные тесты, которые удобно запустить и проверить консистентное состояние программной системы, а то в процессе такого интенсивного создания-тестирования программы всё равно можно что либо опустить, и какая нибудь функциональность да отвалится.
В нашем случае(в связи со сложностью функционирования комбинаций методов) потребуются тесты проверяющие корректность функционирования обобщённых функций в объектной системе и, по мимо них, правильность обращения к полям объекта, т.е правильность создания самих объектов и т.д. Так что эти тесты будут скорее интеграционными, чем юнит-тестами.
Функции тестирования
Тестирование функциональности всегда сопряжено с риском возникновения какого либо отказа, исключительной ситуации, поэтому для тестирования работы обобщённых функций я написал следующую функцию:
(define *test-rezult* '())
(define-m (push-rez x)
(push *test-rezult* x))
;;выполняет запуск функции с аргументами и сравнивает результат в функции сравнения.
(define (test-func func args comp-func)
(set! *test-rezult* '())
(let ((rez (catch (begin '())
(cons (apply func args) '()))))
(set! *test-rezult* (reverse (cons rez *test-rezult*)))
(if (not (null? rez))
(if (apply comp-func (list *test-rezult*))
(cons #t *test-rezult*)
(cons #f *test-rezult*))
(cons rez *test-rezult*))))
основное назначение которой: перехват внезапного исключения в тестируемой функции и сигнализация об этом(путем возврата пустого списка) вышестоящей функции. Например можно протестировать подобную функцию
тест обычной функции.
(define (t1 x1)
(/ 10 x1))
(t1 10)
;;1
(t1 0)
;;Error: /: division by zero
(test-func t1 '(2) (lambda (x) (if (not (null? (car x)))
(begin (prn "args: " x "\n") (= (caar x) 5))
#f)))
;;(#t (5)) результат 5 и это правильно
(test-func t1 '(2) (lambda (x) (if (not (null? (car x)))
(begin (prn "args: " x "\n") (= (caar x) 4))
#f)))
;;(#f (5)) результат 5 и это не правильно(мы ожидали 4).
(test-func t1 '(0) (lambda (x) (if (not (null? (car x)))
(begin (prn "args: " x "\n") (= (caar x) 4))
#f)))
;;(() ())# возврат первым элементом пустого списка свидетельствует о произошедшей ошибке
Таким образом наша тестирующая функция различает три возможных ситуации правильно, неправильно и произошедшая ошибка. По мимо этого, по скольку я тестирую вызовы методов которые не возвращают никаких результатов и мне важно знать порядок вызова этих методов, я ввёл глобальную переменную test-rezult
, в которую из тестирующих функция я буду производить запись push-rez
произвольных данных, и на их основе отслеживать надлежащий порядок вызова методов. Для функции тестирования сделаем обёртку сообщающую нам о результатах тестирования.
(define-m (named-test name func args comp-func)
(let ((rez (test-func func args comp-func)))
(cond
((null? (car rez))
(prn "FAIL TEST! (executed error!) `" name "` rez: " (cdr rez) "\n")
(throw (join-to-str "FAIL TEST! (executed error!) `" name "`")))
((car rez)
(prn "PASS TEST!: `" name "` rez: " (cdr rez) "\n"))
((not (car rez))
(prn "FAIL TEST! (unexpected rezult) `" name "` rez: " (cdr rez) "\n")
(throw (join-to-str "FAIL TEST! (unexpected rezult) `" name "`")))
(#t
(prn "UNKNOWN TEST RESULT `" name "` args: " args ", rez: " rez "\n")))
))
(define-macro (check-equal param)
(let ((var (gensym)))
`(lambda (,var) (equal? ,param ,var))))
тесты могут располагаться в загружаемом файле, и создавать длинную цепочку. Чтобы не пропустить возникшую ошибку просто будем выкидывать исключение при неудовлетворительном результате тестирования.
(define (test-t1 x)
(push-rez "Start test")
(let ((rez (t1 x)))
(push-rez "End test")
rez))
(named-test "test1 t1" test-t1 '(5) (check-equal '("Start test" "End test" (2))))
;;PASS TEST!: `test1 t1` rez: (Start test End test (2))
(named-test "test1 t1" test-t1 '(0) (check-equal '("Start test" "End test" (2))))
;;FAIL TEST! (executed error!) `test1 t1` rez: (Start test ())
;;Error: FAIL TEST! (executed error!) `test1 t1`
(named-test "test1 t1" test-t1 '(4) (check-equal '("Start test" "End test" (2))))
;;FAIL TEST! (unexpected rezult) `test1 t1` rez: (Start test End test (2,5.0))
;;Error: FAIL TEST! (unexpected rezult) `test1 t1`
Проверив работоспособность нашей тестирующей системы можно приступать к написанию интеграционных тестов проверяющих функционирование нашей объектной системы.
Класс Object и циклические(взаимнорекурсивные) структуры
В тестах мы проверим правильность создания объектов, правильность работы функций доступа к полям и правильность работы обобщённых функций. Для вывода состояний объекта я использую базовый класс, определённый в файле: obj/object.scm
(defclass Object () ())
(defmethod (to-s (o Object))
(inspect o nil))
(struct cycle-detect
(again first-encounter num-enc))
Основное назначение этого класса, это распечатка значений полей объектов, пользуясь интроспекцией. И казалось бы в чём тут может быть проблема? Но суть в том, что объекты могут иметь не только стандартные примитивные поля, но и поля содержащие объекты(или их списки) и иногда эти объекты могут иметь взаимно рекурсивные ссылки. И вот такие объекты уже стандартыным обходом полей не распечатать, нужно использовать алгоритм определения циклов в структурах данных, что и делает функция inspect
.(в статье я не использую такие структуры, но может быть как нибудь при случае расскажу и о них).
inspect
(defmethod (inspect (obj Object) cycle)
;;(prn "inspect Object!\n" "cycle: " cycle "\n")
(when (not (cycle-detect? cycle))
(set! cycle (cycle-detect! (build-storage-points-cycle obj)
(make-storage 16 (lambda (x el) (eq? (car x) el)))
0)))
;;(prn "inspect Object!\n" "cycle: " cycle "\n")
(let* ((again (cycle-detect-again cycle))
(first-encounter (cycle-detect-first-encounter cycle))
(type (type-obj obj))
(f1 (get-class-fields-all type))
(fields (if (not (null? f1))
(sort-symb< (map (lambda (x) (sym2key (if (pair? x)
(car x)
x)))
f1))
f1))
(to-str-rec
(lambda (cur)
(cond
((string? cur) cur)
((null? cur) "()")
((boolean? cur) (if cur "#t" "#f"))
((closure? cur) (string-append "#CLOSURE" (to-str-rec (get-closure-code cur))) )
((and (atom? cur)
(not (vector? cur))) (atom->string cur))
((or (list? cur) (vector? cur) (pair? cur))
;;(prn "find list or vector or pair\n")
(let ((f-enc (storage-ref first-encounter cur)))
;;(prn "element again used: " (car f-enc) "\n")
(if (car f-enc) ;;уже встречался!!!
(string-append "#" (atom->string (cdr (cdr f-enc))) "#") ;;вместо элемента будет ссылка на него!
(let ((cur-again (storage-ref again cur))) ;;может элемент из циклич. ссылок?
;;(prn "element may by again: " (car cur-again) "\n")
(when (car cur-again) ;;да? а ведь это первое наше вхождение!
(cycle-detect-num-enc! cycle (+ 1 (cycle-detect-num-enc cycle))) ;;установим его номер
(storage-add first-encounter (cons cur (cycle-detect-num-enc cycle)))) ;;и запомним что первое вхождение уже было!
(let ((ret
(cond
((list? cur)
(string-append "(" (apply string-append (insert-between-elements (map to-str-rec cur) " ")) ")"))
((hash? cur)
;;(prn "Cur is hash: " cur "\n")
(string-append "#{" (apply string-append (insert-between-elements (map to-str-rec (hash2pairs cur)) " ")) "}"))
((object? cur)
(inspect cur cycle))
((vector? cur)
(string-append "#(" (apply string-append (insert-between-elements (map to-str-rec (vector->list cur)) " ")) ")"))
((pair? cur)
(let ((splt (split-pairs-to-list cur)))
(string-append "(" (apply string-append (insert-between-elements (map to-str-rec (car splt)) " "))
(if (null? (cdr splt))
""
(string-append " . " (to-str-rec (cadr splt))))
")")))
)))
(if (car cur-again)
(string-append "#" (atom->string (cdr (cdr (storage-ref first-encounter cur)))) "=" ret) ;;поставим метку!
ret))) ;;обычный не циклический список
)))
(#t "-bad element-")
))))
(let ((rez '()))
(for-list (f fields)
(push rez (string-append (to-str f) ": " (to-str-rec (vfield obj f)))))
(join-to-str "#" type "[" (apply join-to-str (insert-between-elements (reverse rez) ", ")) "]"))
))
Ну а что такое циклическая структура? Структура которая может содержать ссылку на саму себя, прямо или косвенно.
(define end-list (cons 13 '()))
(define cyclic-list (append '(1 2 3 4) end-list))
cyclic-list
;;(1 2 3 4 13)
(begin
(set-car! end-list cyclic-list) ;;не выполняйте без окружения begin, иначе репл попытается вывести это в консоль и за
#t)
(to-str* cyclic-list)
;;"#1=(1 2 3 4 #1#)"
;;простейшая циклическая структура:
(define simple-cycle (cons 13 '()))
(begin
(set-car! simple-cycle simple-cycle) ;;не выполняйте без окружения begin, иначе репл попытается вывести это в консоль
#t)
(prn* simple-cycle)
;;#1=(#1#)
важно отметить данным алгоритмом обрабатываются не все циклические структуры, а только те в которых ссылки содержат значащие элементы структуры(т.е те в которых должны содержаться значения). т.е для списков это car
элементы, если цикл содержит cdr
элементы алгоритм работать не будет(зависнет просто и вывалится из за нехватки памяти).
Интеграционные тесты
Подготовка к тестированию
;;(define path-home "D:")
(define path-home (getenv "HOME"))
(define path-lib (string-append path-home "/work/gimp/lib/"))
(define path-work (string-append path-home "/work/gimp/"))
(load (string-append path-lib "util.scm"))
(load (string-append path-lib "defun.scm"))
(load (string-append path-lib "struct2.scm"))
(load (string-append path-lib "storage.scm"))
(load (string-append path-lib "cyclic.scm"))
(load (string-append path-lib "hashtable3.scm")) ;;хеш который может работать с объектами в качестве ключей!!!
(load (string-append path-lib "sort2.scm"))
(load (string-append path-lib "tsort.scm"))
;;(load (string-append path-lib "cpl-sbcl.scm")) ;;можно выбрать любую из функций упорядочения иерархии классов.
(load (string-append path-lib "cpl-mro.scm"))
;;(load (string-append path-lib "cpl-topext.scm"))
(load (string-append path-lib "struct2ext.scm"))
(load (string-append path-lib "queue.scm"))
(load (string-append path-lib "obj4.scm"))
(load (string-append path-lib "obj/object.scm"))
(load (string-append path-lib "tests.scm"))
Что представляют из себя тесты для объектной системы? Это определение иерархии, определение методов обобщённых функций и собственно их вызов и сверка результатов, что получилось и что должно быть.
;;чтобы не вводить одну и туже иерархию десять раз напишу макрос определяющий иерархию
(define-macro (deftest-obj1)
`(begin
(defclass a (Object)
(a))
(defclass b (a)
(b))
(defclass c (a)
(c))
(defclass d (b c)
(d))))
;;TEST before
(deftest-obj1) ;; определение иерархии
(defgeneric test-a x)
(defmethod (:before test-a (x a))
(push-rez (join-to-str ":before test-a (x a), x: " (to-s x))))
(named-test "test :before" test-a (list (make-a :a 12)) (check-equal '(":before test-a (x a), x: #a[:a: 12]" (#f))))
(named-test "test2 :before" test-a (list (make-d :a 1 :b 2 :c 3 :d 4)) (check-equal '(":before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]" (#f))))
;;PASS TEST!: `test :before` rez: (:before test-a (x a), x: #a[:a: 12] (#f))
;;PASS TEST!: `test2 :before` rez: (:before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] (#f))
;;TEST before before
(defmethod (:before test-a (x d))
(push-rez (join-to-str ":before test-a (x d)")))
(named-test "test3 :before" test-a (list (make-a :a 12)) (check-equal '(":before test-a (x a), x: #a[:a: 12]" (#f))))
(named-test "test4 :before" test-a (list (make-d :a 1 :b 2 :c 3 :d 4))
(check-equal '(":before test-a (x d)" ":before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]" (#f))))
;;PASS TEST!: `test3 :before` rez: (:before test-a (x a), x: #a[:a: 12])
;;PASS TEST!: `test4 :before` rez: (:before test-a (x d) :before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] (#f))
все тесты приводить не буду, но вот последний тест тестирующий сложную комбинацию методов обобщённой функции
;подготовка к большому тесту с различными комбинациями методов с различными квалификаторами и более простые тесты
;;around around primary primary after after after before before before
(deftest-obj1)
(defgeneric test-a x)
(defmethod (:before test-a (x a))
(push-rez (join-to-str ":before test-a (x a), x: " (to-s x))))
(defmethod (:after test-a (x a))
(push-rez (join-to-str ":after test-a (x a), x: " (to-s x))))
(defmethod (:around test-a (x a))
(push-rez (join-to-str ":around test-a (x a), x: " (to-s x)))
(if (next-method-p)
(call-next-method)
(vfield x :a)))
(defmethod (:around test-a (x d))
(push-rez (join-to-str ":around test-a (x d), x: " (to-s x)))
(if (next-method-p)
(call-next-method)
(vfield x :d)))
(named-test "test comb" test-a (list (make-a :a 12))
(check-equal '(":around test-a (x a), x: #a[:a: 12]" ":before test-a (x a), x: #a[:a: 12]"
":after test-a (x a), x: #a[:a: 12]" (#f))))
(named-test "test comb" test-a (list (make-c :a 12 :c 13))
(check-equal '(":around test-a (x a), x: #c[:a: 12, :c: 13]" ":before test-a (x a), x: #c[:a: 12, :c: 13]"
":after test-a (x a), x: #c[:a: 12, :c: 13]" (#f))))
(defmethod (test-a (x a))
(push-rez (join-to-str ":primary test-a (x a), x: " (to-s x)))
(if (next-method-p)
(call-next-method)
(vfield x :a)))
(defmethod (test-a (x d))
(push-rez (join-to-str ":primary test-a (x d), x: " (to-s x)))
(if (next-method-p)
(call-next-method)
(vfield x :d)))
(named-test "test comb3" test-a (list (make-a :a 12))
(check-equal '(":around test-a (x a), x: #a[:a: 12]" ":before test-a (x a), x: #a[:a: 12]"
":primary test-a (x a), x: #a[:a: 12]" ":after test-a (x a), x: #a[:a: 12]" (12))))
(named-test "test comb4" test-a (list (make-d :a 1 :b 2 :c 3 :d 4))
(check-equal '(":around test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":around test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":primary test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":primary test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":after test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]" (1))))
(defmethod (:before test-a (x b))
(push-rez (join-to-str ":before test-a (x b), x: " (to-s x))))
(defmethod (:after test-a (x b))
(push-rez (join-to-str ":after test-a (x b), x: " (to-s x))))
(defmethod (:before test-a (x d))
(push-rez (join-to-str ":before test-a (x d), x: " (to-s x))))
(defmethod (:after test-a (x d))
(push-rez (join-to-str ":after test-a (x d), x: " (to-s x))))
(named-test "test comb5" test-a (list (make-b :a 1 :b 2))
(check-equal '(":around test-a (x a), x: #b[:a: 1, :b: 2]"
":before test-a (x b), x: #b[:a: 1, :b: 2]"
":before test-a (x a), x: #b[:a: 1, :b: 2]"
":primary test-a (x a), x: #b[:a: 1, :b: 2]"
":after test-a (x a), x: #b[:a: 1, :b: 2]"
":after test-a (x b), x: #b[:a: 1, :b: 2]" (1))))
(named-test "test comb6" test-a (list (make-c :a 1 :c 3))
(check-equal '(":around test-a (x a), x: #c[:a: 1, :c: 3]"
":before test-a (x a), x: #c[:a: 1, :c: 3]"
":primary test-a (x a), x: #c[:a: 1, :c: 3]"
":after test-a (x a), x: #c[:a: 1, :c: 3]" (1))))
результаты запуска предварительных тестов.
;;PASS TEST!: `test comb` rez: (:around test-a (x a), x: #a[:a: 12] :before test-a (x a), x: #a[:a: 12] :after test-a (x a), x: #a[:a: 12] (#f))
;;PASS TEST!: `test comb` rez: (:around test-a (x a), x: #c[:a: 12, :c: 13]
;; :before test-a (x a), x: #c[:a: 12, :c: 13] :after test-a (x a), x: #c[:a: 12, :c: 13] (#f))
;; PASS TEST!: `test comb3` rez: (:around test-a (x a), x: #a[:a: 12]
;; :before test-a (x a), x: #a[:a: 12]
;; :primary test-a (x a), x: #a[:a: 12]
;; :after test-a (x a), x: #a[:a: 12] (12))
;; PASS TEST!: `test comb4` rez: (:around test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :around test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :primary test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :primary test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :after test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] (1))
;; PASS TEST!: `test comb5` rez: (:around test-a (x a), x: #b[:a: 1, :b: 2]
;; :before test-a (x b), x: #b[:a: 1, :b: 2]
;; :before test-a (x a), x: #b[:a: 1, :b: 2]
;; :primary test-a (x a), x: #b[:a: 1, :b: 2]
;; :after test-a (x a), x: #b[:a: 1, :b: 2]
;; :after test-a (x b), x: #b[:a: 1, :b: 2] (1))
;; PASS TEST!: `test comb6` rez: (:around test-a (x a), x: #c[:a: 1, :c: 3]
;; :before test-a (x a), x: #c[:a: 1, :c: 3]
;; :primary test-a (x a), x: #c[:a: 1, :c: 3]
;; :after test-a (x a), x: #c[:a: 1, :c: 3] (1))
А вот и сам большой тест и его результат. Как видите вызов одной функции test-a с параметором объектом класс d приводит к вызову целой цепочки методов.
(named-test "test comb7" test-a (list (make-d :a 1 :b 2 :c 3 :d 4))
(check-equal '(":around test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":around test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":before test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":before test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":primary test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":primary test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":after test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":after test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]"
":after test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]" (1))))
;; PASS TEST!: `test comb7` rez: (:around test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :around test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :before test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :before test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :primary test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :primary test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :after test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :after test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4]
;; :after test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] (1))
и вот сводим все тесты в один файл:
(load (string-append path-work "test-obj4.scm"))
deftest-obj1#tPASS TEST!: `test :before` rez: (:before test-a (x a), x: #a[:a: 12] (#f))
...
...
#tPASS TEST!: `test comb7` rez: (:around test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :around test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :before test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :before test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :before test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :primary test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :primary test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :after test-a (x a), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :after test-a (x b), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] :after test-a (x d), x: #d[:a: 1, :b: 2, :c: 3, :d: 4] (1))
#t##t#
И если мы где-то, для теста, поставим какое то некорректное значение или вызовем некорректную операцию, например:
(defmethod (test-a (x a))
(push-rez (join-to-str ":primary test-a (x a), x: " (to-s x)))
(/ 10 0)
;;(push-rez (join-to-str "ops!"))
(if (next-method-p)
(call-next-method)
(vfield x :a)))
То цепочка тестов прервётся приблизительно с таким вот сообщением:
#t#tFAIL TEST! (unexpected rezult) `test comb3` rez: (:around test-a (x a), x: #a[:a: 12] :before test-a (x a), x: #a[:a: 12] :primary test-a (x a), x: #a[:a: 12] ops! :after test-a (x a), x: #a[:a: 12] (12))
Error: FAIL TEST! (unexpected rezult) `test comb3`
Выводы
Без создания и использования тестирующей системы большой проект развиваться, да и существовать, просто не может. Создание набора тестов создаёт тестовый ИНВАРИАНТ программной системы, применение которого даст ответ при изменениях в проекте, это всё ещё наша система или уже нет. Если обнаруживаются ошибки или случаи, которые не отслеживает наш инвариант, то его расширяют соответствующим набором тестов и приводят код в состояние удовлетворяющему новому тестовому инварианту системы. И мы СМОГЛИ внедрить тестирующую систему в наш проект, так что за правильность функционирования диспетчеризации, в условиях изменения проекта, можно быть спокойным. Ну а без тестов, как говориться: «то лапы ломит, то хвост отваливается, а недавно я ещё линять начал... и т. д.».