Давайте посмотрим на упрощенный пример здесь:
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" do
it "parentl" do
puts "parent test 1: #{ENV["GLOBAL_VAR"]?}
- #{ENV["SUB_VAR"]?}"
end
describe "sub tests" do
before_each do
ENV["SUB_VAR"] = "bar"
end
after_each do
ENV.delete "SUB_VAR"
end
it "child1" do
puts "child test: #{ENV["GLOBAL_VAR"]?}
- #{ENV["SUB_VAR"]?}"
end
end
it "parent2" do
puts "parent test 2: #{ENV["GLOBAL_VAR"]?}
- #{ENV["SUB_VAR"]?}"