Это будет динамически, а не статически типизировано. Утиная типизация будет тогда выполнять ту же работу, что и интерфейсы на статически типизированных языках. Кроме того, его классы могут быть модифицируемыми во время выполнения, чтобы тестовая среда могла легко заглушать или имитировать методы в существующих классах. Руби является одним из таких языков; rspec - это главная тестовая среда для TDD.
Как динамическая типизация помогает при тестировании
С помощью динамической типизации вы можете создавать фиктивные объекты, просто создавая класс с тем же интерфейсом (сигнатурами методов), что и объект коллектора, который вам нужно макетировать. Например, предположим, у вас был какой-то класс, который отправлял сообщения:
class MessageSender
def send
# Do something with a side effect
end
end
Допустим, у нас есть MessageSenderUser, который использует экземпляр MessageSender:
class MessageSenderUser
def initialize(message_sender)
@message_sender = message_sender
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
Обратите внимание на использование здесь внедрения зависимостей , основного элемента модульного тестирования. Мы вернемся к этому.
Вы хотите проверить, что MessageSenderUser#do_stuff
звонки отправляются дважды. Как и в случае статически типизированного языка, вы можете создать фиктивный MessageSender, который подсчитывает, сколько раз send
был вызван. Но в отличие от статически типизированного языка, вам не нужен класс интерфейса. Вы просто идете вперед и создаете это:
class MockMessageSender
attr_accessor :send_count
def initialize
@send_count = 0
end
def send
@send_count += 1
end
end
И используйте это в своем тесте:
mock_sender = MockMessageSender.new
MessageSenderUser.new(mock_sender).do_stuff
assert_equal(mock_sender.send_count, 2)
Само по себе «типизирование по типу утки» динамически типизированного языка не так уж много добавляет к тестированию по сравнению со статически типизированным языком. Но что, если классы не закрыты, но могут быть изменены во время выполнения? Это изменит правила игры. Посмотрим как.
Что если вам не нужно использовать внедрение зависимостей, чтобы сделать класс тестируемым?
Предположим, что MessageSenderUser будет когда-либо использовать MessageSender только для отправки сообщений, и вам нет необходимости разрешать замену MessageSender каким-либо другим классом. В рамках одной программы это часто имеет место. Давайте перепишем MessageSenderUser, чтобы он просто создавал и использовал MessageSender без внедрения зависимостей.
class MessageSenderUser
def initialize
@message_sender = MessageSender.new
end
def do_stuff
...
@message_sender.send
...
@message_sender.send
...
end
end
MessageSenderUser теперь проще в использовании: никому не нужно его создавать, чтобы создать MessageSender для его использования. В этом простом примере это не выглядит большим улучшением, но теперь представьте, что MessageSenderUser создается более одного раза или имеет три зависимости. Теперь у системы есть множество случайных примеров просто для того, чтобы порадовать модульные тесты, а не потому, что это вообще обязательно улучшает дизайн.
Открытые классы позволяют тестировать без внедрения зависимостей
Тестовая среда на языке с динамической типизацией и открытыми классами может сделать TDD довольно привлекательным. Вот фрагмент кода из теста rspec для MessageSenderUser:
mock_message_sender = mock MessageSender
MessageSender.should_receive(:new).and_return(mock_message_sender)
mock_message_sender.should_receive(:send).twice.with(no_arguments)
MessageSenderUser.new.do_stuff
Вот и весь тест. Если MessageSenderUser#do_stuff
не вызвать MessageSender#send
ровно дважды, этот тест не пройден. Настоящий класс MessageSender никогда не вызывается: мы сказали тесту, что всякий раз, когда кто-то пытается создать MessageSender, он должен вместо этого получить наш поддельный MessageSender. Внедрение зависимостей не требуется.
Приятно сделать так много в таком простом тесте. Всегда приятнее не использовать внедрение зависимостей, если это не имеет смысла для вашего дизайна.
Но какое это имеет отношение к открытым классам? Обратите внимание на призыв к MessageSender.should_receive
. Мы не определили #should_receive при написании MessageSender, так кто же это сделал? Ответ заключается в том, что тестовая среда, внося некоторые осторожные изменения в системные классы, способна заставить ее выглядеть так, как через #should_receive определен для каждого объекта. Если вы думаете, что изменение системных классов, таких как это, требует некоторой осторожности, вы правы. Но это идеальная вещь для того, что делает здесь тестовая библиотека, и открытые классы делают это возможным.