Статья незакончена! Опубликована как предварительный материал к лекции по Mock-объектам.
Вольный пересказ статьи Mocks Aren't Stubs by Martin FowlerСм. также XUnit Test Patterns by Gerard MeszarosПример
Для иллюстрации будем использовать следующий пример: выполнение заказа со склада. Заказ (Order) включает всего один товар (для простоты товары будут кодироваться символами типа #article1) с указанием количества (amount). Склад (warehouse) знает про наличие товаров (inventory). Когда мы просим заказ "выполниться", возможны два сценария: (1) если на складе имеется достаточное количество указанного товара, то заказ помечается как выполненный (filled) и со склада списывается соответствующее количество этого товара; (2) если нужного количества нет, заказ остается невыполненным, а со складом ничего не происходит.
Обычные тесты
OrderStateTests >>
setUp
warehouse := Warehouse new.
warehouse
add: 50 of: #article1.
testOrderIsFilledIfEnoughInWarehouse
| order |
order := Order on: 50 of: #article1.
order fillBy: warehouse.
self assert: order isFilled.
self assert: (warehouse inventoryOf: #article1) isZero.
testOrderDoesNotRemoveIfNotEnough
| order |
order := Order on: 51 of: #article1.
order fillBy: warehouse.
self deny: order isFilled.
self assert: (warehouse inventoryOf: #article1) = 50.
Тесты в xUnit (SUnit в данном случае) обычно, как и в данном случае, можно разбить на четыре стадии:
установка,
выполнение,
проверка,
демонтаж.
Установка в данном примере выполняется в методе setUp, а также в самих тестах --- созданием заказа. Выполнение состоит в передаче сообщения #fillBy: созданном заказу. С помощью assert-ов выполняется проверка. А стадия демонтажа явно не представлена (на самом деле, она выполняется "за кадром" сборщиком мусора).
На стадии установки мы конфигурируем два объекта: склад (warehouse) и заказ (order). Можно заметить, что с точки зрения теста эти объекты не равнозначны.
Один из них --- заказ --- собственно и подвергается тестированию (точнее, один из аспектов его функциональности --- метод выполнения). Такие --- центральные для теста --- объекты будем называть
тестируемой системой. А для краткости будем использовать английскую аббревиатуру
SUT (от
System Under Test).
Другой объект --- склад --- нужен в первую очередь для того, чтобы выполнение заказа могло правильно отработать в данном тесте. Назовем такие объекты (их может быть несколько)
сотрудниками (
collaborators в англоязычной литературе). В данном случае мы используем сотрудника еще и на стадии проверки для того, чтобы проконтролировать результат выполнения заказа (произошло ли списание).
Что в этих тестах не так?
В первую очередь можно заметить, что связь второго аспекта условия в каждом тесте (остаток товара на складе) с самой операцией выполнения заказа, мягко говоря, совсем не очевидна. Человеку, читающему данный код, сложно догадаться, что в процессе выполнения заказа может выполняться списание. Этот аспект, как и зависимость успешности списания от наличия необходимого остатка на складе, отмечены только в названиях тестов.
Еще одно замечание: класс Warehouse должен существовать и правильно работать (по крайней мере, в отношении хранения остатков по продуктам и их списания), для того, чтобы тест работал. То есть, тест может сломаться не только потому, что неправильно реализована тестируемая система, но и из-за ошибок в сотрудниках. Как следствие, данный тест не может точно показать, где именно кроется ошибка. И это не единственный недостаток зависимости тестов от используемых в них сотрудников. Но другой --- весьма немаловажный, надо заметить, --- аспект рассмотрим чуть позже...
Тесты с использованием суррогатных объектов
С использованием SMock то же поведение можно описать следующим образом. Рассмотрим сначала "неудачное" списание.
OrderInteractionTests >>
testFillingDoesNotRemoveIfNotEnoughInStock
| order warehouse mock |
order := Order on: 101 of: #article1.
mock := SMock.Mock new.
(mock expect: #has:of:)
with: 101;
with: #article1;
returns: false.
warehouse := mock proxy.
order fillIn: warehouse.
mock verify.
self deny: order isFilled
Теперь вместо настоящего склада мы используем "заместителя" (proxy), полученного из суррогатного объекта ("мок-объекта" или просто "мока" в дальнейшем). Моки позвволяют проверить взаимодействие тестируемой системы с сотрудниками. Для этого сначала моку указывается, какие сообщения он должен будет получить --- описываются ожидания (expectations). А после использования мока (через его представителя) в реальных вычислениях, можно проверить, были ли эти ожидания выполнены.
Ожидания добавляются в мок через посылку сообщения #expect:. В параметре передается селектор сообщения. В ответ мок возвращает специальный объект, представляющий информацию об ожидаемом сообщении. Ему можно указать, например, значения параметров (с помощью последовательных сообщений #with:), возвращаемый результат (#returns:) и др.
В рассмотренном примере: mock ожидает сообщение #has:of: с первым параметром равным 101, и вторым параметром равным #article. В ответ на получение этого сообщения мок должен вернуть значение false.
"Заместитель" переадресует получаемые сообщения моку, который сверяем получаемое сообщение с ожиданиями и помечает их выполнение.
В ответ на сообщение #verify мок проверяет, были ли выполнены все ожидания. Если это не так, то генерируется исключительная ситуация, описывающая несоответствие.
Рассмотрим второй тест:
OrderInteractionTests >>
testFillingRemovesInventoryIfInStock
| order warehouse mock |
order := Order on: 100 of: #article1.
mock := SMock.Mock new.
(mock expect: #has:of:)
with: 100;
with: #article1;
returns: true.
(mock expect: #remove:of:)
with: 100;
with: #article1.
warehouse := mock proxy.
order fillIn: warehouse.
mock verify.
self assert: order isFilled
Здесь мок должен получить два сообщения: #has:of: и #remove:of:. В обоих случаях ожидаются два аргумента: 100 и #article, соответственно. На #has:of: мок должен ответить true.