Crystal Programming. Введение на основе проекта в создание эффективных, безопасных и читаемых веб-приложений и приложений CLI - Джордж Дитрих
Шрифт:
Интервал:
Закладка:
По мере роста количества кода в приложении будет расти и количество тестов. Это может затруднить отладку конкретных тестовых случаев. В этом случае аргумент focus: true можно добавить в блок #describe или #it. При этом будет выполнена только одна спецификация, как в следующем примере:
it "does something", focus: true do
1.should eq 1
end
Только не забудьте удалить его перед совершением!
Модуль Spec также предоставляет некоторые дополнительные методы, которые можно использовать для более точного контроля выполнения ваших тестовых случаев. Некоторые из них перечислены здесь:
• #pending: этот метод используется для определения тестового примера для чего-то, что еще не полностью реализовано, но будет реализовано в будущем, например, ожидающий "check cat" { cat.alive? }. Блок метода никогда не выполняется, но может использоваться для описания того, что должен делать тест.
• #pending!: Метод #pending! аналогичен предыдущему методу, но может использоваться для динамического пропуска тестового примера. Это может быть полезно для обеспечения выполнения зависимостей/требований системного уровня перед запуском тестового примера.
• #fail: Наконец, этот метод можно использовать для ручного провала тестового примера. Это можно использовать в сочетании с пользовательской условной логикой для создания более сложных утверждений, с которыми не могут справиться встроенные утверждения.
Маркировка (Tagging) тестов
Теги — это способ организовать спецификации в группы, чтобы можно было выполнить их подмножество. Подобно фокусировке спецификации, теги применяются к блокам #describe или #it через аргумент tags следующим образом:
require "spec"
describe "tags" do
it "tag a", tags: "a" do
end
it "tag b", tags: "b" do
end
end
Отсюда вы можете использовать опцию --tag через crystal spec, чтобы контролировать, какие из них будут выполняться, как описано здесь:
• --tag 'a' --tag 'b' будет включать спецификации, отмеченные ИЛИ b.
• --tag '~a' --tag '~b' будет включать спецификации, не помеченные знаком И, не помеченные знаком b.
• --tag 'a' --tag '~b' будет включать спецификации, отмеченные тегом a, но не отмеченные тегом b.
Последняя команда может выглядеть так: crystal spec --tag 'a'. Далее мы рассмотрим, как обрабатывать зависимости внутренних объектов путем создания макетов.
Осмеяние (Mocking)
Предыдущий пример с методом #add не имел никаких внешних зависимостей, но помните в Главе 4 «Изучение Crystal посредством написания интерфейса командной строки», как мы сделали NotificationEmitter типом аргумента конструктора, а не использовали его непосредственно в методе #process? Тип NotificationEmitter является зависимостью типа Processor.
Причина, по которой мы сделали его аргументом конструктора, заключается в том, что он следует нашим принципам проектирования SOLID (где SOLID означает принцип единой ответственности, принцип открытости-закрытости, принцип замены Лискова, принцип сегрегации интерфейса и принцип инверсии зависимостей), что, в свою очередь, делает тип легче для тестирования, позволяя использовать фиктивную реализацию вместо этого аргумента. Макет позволяет вам подтвердить, что он вызывается правильно, и настроить его на возврат значений так, чтобы тестовые примеры каждый раз были одинаковыми.
Давайте посмотрим на упрощенный пример здесь:
module TransformerInterface
abstract def transform(value : String) : String
end
struct ShoutTransformer
include Transformerinterface
def transform(value : String) : String
value.upcase
end
end
class Processor
def initialize(@transformer : Transformerinterface =
ShoutTransformer.new); end
def process(value : String) : String
@transformer.transform value
end
end
puts Processor.new.process "foo"
Здесь у нас есть тип интерфейса Transformer, который определяет требуемый метод, который должен реализовать каждый преобразователь. У нас есть единственная его реализация, ShoutTransformer, которая преобразует значение в верхний регистр. Затем у нас есть тип Processor, который использует тип интерфейса Transformer как часть своего метода #process, по умолчанию использующий преобразователь крика. Запуск этой программы приведет к выводу FOO на ваш терминал.
Поскольку мы хотим протестировать наш тип Processor изолированно, мы собираемся создать имитацию реализации преобразователя для использования в нашем тесте. Это гарантирует, что мы не тестируем больше, чем требуется. Взгляните на следующий пример:
class MockTransformer
include Transformerinterface
getter transform_arg_value : String? = nil
def transform(value : String) : String
@transform_arg_value = value
end
end
Он реализует тот же API, что и другие, но фактически не преобразует значение, а просто предоставляет его через переменную экземпляра. Затем мы могли бы использовать это в тесте следующим образом, обязательно потребовав также Processor и MockTransformer, если они не определены в одном файле:
require "spec"
describe Processor do
describe "#process" do
it "processes" do
transformer = MockTransformer.new
Processor.new(transformer).process "bar"
transformer.transform_arg_value.should eq "bar"
end
end
end
Поскольку фиктивный преобразователь хранит значение, мы можем использовать его, чтобы гарантировать, что он был вызван с ожидаемым значением. Это позволит выявить случаи, когда он не вызывается или вызывается с неожиданным значением, что является ошибкой. Макетная реализация также не обязательно должна быть частной. Его можно было бы представить как часть самого проекта, чтобы конечный пользователь мог использовать его и в своих тестах.
Хуки
Основной принцип тестирования заключается в том, что каждый тестовый пример независим от других, например, не полагаясь на состояние предыдущего теста. Однако для нескольких тестов может потребоваться одно и то же состояние для проверки того, на чем они сосредоточены. Crystal предоставляет несколько методов как часть модуля Spec, которые можно использовать для определения обратных вызовов в определенных точках жизненного цикла теста.
Эти методы могут быть полезны для централизации настройки/удаления необходимого состояния для тестов. Например, предположим, что вы хотите убедиться, что глобальная переменная среды установлена перед запуском любого теста, и в нескольких тестовых случаях есть другая переменная, но нет других тестов. Для этого вы можете использовать методы .before_suite, #before_each и #after_each. Пример этого вы можете увидеть в следующем фрагменте кода:
require "spec"
Spec.before_suite do
ENV["GLOBAL_VAR"] = "foo"
end
describe "My tests"