Выбрать главу

Давайте посмотрим на упрощенный пример здесь:

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"]?}"