Как перейти с TDD в стиле .NET на Ruby? - PullRequest
9 голосов
/ 10 сентября 2010

Я изо всех сил пытался адаптировать мой стандартный подход для тестирования кода .NET к Ruby.

В качестве примера я пишу класс, который будет:

grab all *.markdown files from a directory
  foreach file:
    extract code samples from file
    save code to file.cs in output directory

Обычно для .NET я получаю что-то вроде:

class ExamplesToCode {
  public ExamplesToCode(IFileFinder finder, IExampleToCodeConverter converter) { ... }
  public void Convert(string exampleDir, string targetDir) { ... }
}

В моем тесте (написанном первым) я бы высмеивал искатель и конвертер.Тогда я бы заглушил finder.FindFiles("*.markdown"), чтобы вернуть скажем ["file1", "file2"], и проверил, что converter.Convert("file1", targetDir) и converter.Convert("file2", targetDir) были вызваны.

Когда я пытаюсь применить это к Ruby, это то, что Ruby имеет тенденцию использовать блоки и внутренниеитераторы (например, array.each { |x| puts x }), включая модули над инжекцией конструктора.Я не уверен в том, как выполнить модульное тестирование кода в этих случаях (без настройки полного интеграционного теста), и подход .NET кажется просто невероятным;похоже, что он борется так, как работает Руби.

Есть предложения о том, как сделать это в Ruby?Примером теста Ruby для этого примера будет отличный.

Ответы [ 5 ]

2 голосов
/ 10 сентября 2010

Прежде чем ответить на вопрос о том, как это сделать в Ruby, я бы хотел устранить некоторые недоразумения.

Во-первых, я бы не сказал, что существует такой "метод Ruby" для тестирования подобных вещей, как строгий способ тестирования чего-то подобного в .NET (который, по общему признанию, я не использовал годами). Можно использовать подход, основанный на взаимодействии (насмешливый), или, как вы сказали, больше подход, основанный на состоянии, путем создания интеграционного теста, в котором одновременно выполняются все три класса. Я полагаю, что компромисс между двумя подходами не зависит от языка. Ruby имеет много насмешливых фреймворков , которые позволили бы вам использовать интерактивный подход, если это то, что вам наиболее удобно. (Я обычно использую тот, который поставляется с RSpec.)

Во-вторых, я не думаю, что «включение модулей поверх конструктора» является точным утверждением. Модули - это дополнительный инструмент, доступный вам в Ruby, но они ни в коем случае не заменяют хороший ОО-дизайн композицией объектов. Я все время передаю зависимости своим инициализаторам в Ruby, поскольку это облегчает их тестирование и позволяет использовать их повторно. Обычно я по умолчанию устанавливаю зависимость в списке аргументов, однако, вот так def initialize(converter=CodeConverter.new).

Теперь, чтобы ответить на ваш вопрос. То, что Лиамкленнан сказал об использовании Dir::[], правильно: finder не требуется. Итак, вопрос в том, как вы пишете тесты для методов, которые вызывают Dir::[]? У вас есть три варианта: 1) использовать одну из вышеупомянутых библиотек насмешек и заглушку Dir::[] (это простой и легкий подход), 2) записать файлы на диск и убедиться, что они прочитаны (ick), или 3) использовать библиотека типа FakeFS для предотвращения дискового ввода-вывода, но все же позволяет писать естественный тест. В следующем примере один возможный способ написания / тестирования этого (с использованием RSpec и FakeFS), который несколько параллелен вашему первоначальному проекту:

class CodeExtractor

  def self.extract_dir(example_dir, target_dir)
    Dir[example_dir + "/*.md"].each do |filename|
      self.extract(filename, target_dir)
    end
  end

  def self.extract(*args)
    self.new(*args).extract
  end

  def extract(filename, target_dir)
    # ...
  end
end

# The spec...
require 'fakefs/spec_helpers'
describe CodeExtractor do
  include FakeFS::SpecHelpers

  describe '::extract_dir' do
    it "extracts each markdown file in the provided example dir" do
      FileUtils.touch(["foo.md", "bar.md"])
      CodeExtractor.should_receive(:extract).with(Dir.pwd + "/foo.md","/target")
      CodeExtractor.should_receive(:extract).with(Dir.pwd + "/bar.md","/target")
      CodeExtractor.extract_dir(Dir.pwd, "/target")
    end
  end

  describe '#extract' do
    it "blah blah blah" do
      # ...
    end
  end
end

Конечно, возникает вопрос, добавляет ли такой тест достаточную ценность, чтобы заслужить его существование. Я не думаю, что буду вдаваться в это, хотя .... Если вы решите использовать FakeFS, имейте в виду, что трассировки стека от ошибок могут быть бесполезны, так как FS фальсифицируется, когда RSpec пытается получить номер строки несуществующая ФС. :) По совпадению, у меня есть некоторый код, который читает и анализирует слайды уценки на github. спецификации могут служить еще примерами того, как вы можете подходить к тестированию подобных вещей в Ruby. HTH, и удачи.

2 голосов
/ 10 сентября 2010

У вас может быть очень тестовый курс, который будет выглядеть примерно так:

class ExamplesToCodeTest < Test::Unit::TestCase
  def test_convert
    # have some example markdown files in a fixtures directory
    ExamplesToCode.convert("test/fixtures/*.markdown")
    assert_equal expected_output_1, File.read("test/output/file_1.cs")
    assert_equal expected_output_2, File.read("test/output/file_2.cs")
    assert_equal expected_output_3, File.read("test/output/file_3.cs")
  end
  private
    def expected_output_1
      "... expected stuff here ..."
    end
    def expected_output_2
      "... expected stuff here ..."
    end
    def expected_output_3
      "... expected stuff here ..."
    end
end

Полагаю, это даст достойный интеграционный тест, но это не то, что мне действительно нравится, мне нравится иметь свой код вкуски размера прикуса

Сначала я создал бы класс, который может обрабатывать файл уценки, например:

class MarkdownReaderTest < Test::Unit::TestCase
  def test_read_code_sample_1
    reader = MarkdownReader.new
    code_sample = reader.read("fixtures/code_sample_1.markdown")
    # or maybe something like this:
    # code_sample = reader.parse(File.read("fixtures/code_sample_1.markdown"))
    # if you want the reader to just be a parser...
    assert_equal code_sample_1, code_sample
  end
  # ... repeat for other types of code samples ...
  private
    def code_sample_1
      "text of code sample 1 here..."
    end
end

Теперь весь код для чтения и анализа файлов уценки находится в MarkdownReaderучебный класс.Теперь, если мы не хотим на самом деле писать файлы, вы можете получить фантазию и поэкспериментировать с RR или Mocha или чем-то еще (я использую rr здесь):

class CodeSampleWriter < Test::Unit::TestCase
  include RR::Adapters::TestUnit
  def test_write_code_sample
    # assuming CodeSampleWriter class is using the File.write()...
    any_instance_of(File) do |f|
      mock(f).write(code_sample_text) { true }
    end
    writer = CodeSampleWriter.new
    writer.write(code_sample_text)
  end
  private
    def code_sample_text
      "... code sample text here ..."
    end
end

Теперь предположим класс examplesToCodeиспользует классы MarkdownReader и CodeSampleWriter, вы снова можете использовать фиктивные объекты с RR следующим образом:

class ExamplesToCodeTest < Test::Unit::TestCase
  include RR::Adapters::TestUnit
  def test_convert
    # mock the dir, so we don't have to have an actual dir with files...
    mock(Dir).glob("*.markdown") { markdown_file_paths }
    # mock the reader, so we don't actually read files...
    any_instance_of(MarkdownReader) do |reader|
      mock(reader).read("file1.markdown") { code_sample_1 }
      mock(reader).read("file2.markdown") { code_sample_1 }
      mock(reader).read("file3.markdown") { code_sample_1 }
    end
    # mock the writer, so we don't actually write files...
    any_instance_of(CodeSampleWriter) do |writer|
      mock(writer).write_code_sample(code_sample_1) { true }
      mock(writer).write_code_sample(code_sample_2) { true }
      mock(writer).write_code_sample(code_sample_3) { true }
    end
    # now that the mocks are mocked, it's go time!
    ExamplesToCode.new.convert("*.markdown")
  end
  private
    def markdown_file_paths
      ["file1.markdown", "file2.markdown", "file3.markdown"]
    end
    def code_sample_1; "... contents of file 1 ..."; end
    def code_sample_2; "... contents of file 2 ..."; end
    def code_sample_3; "... contents of file 3 ..."; end
end

Надеемся, что это даст вам некоторые идеи о том, как подходить к тестированию в Ruby.Не будьте подстрекательским, но по большей части внедрение зависимостей не является чем-то, что можно увидеть или использовать в мире Ruby - оно обычно добавляет много накладных расходов.Пересмешливание / удвоение, как правило, гораздо лучший вариант для тестирования.

1 голос
/ 11 сентября 2010

Из всего этого псевдокода единственное, что меня действительно беспокоит, это «извлечение примеров кода из файла». Чтение файлов из каталога тривиально, сохранение файла тривиально. Независимо от тестовой среды, я бы потратил большую часть своего времени, сконцентрировавшись на бите разбора.

Для прямого тестирования я бы вставил фрагменты прямо в тестовый набор:

# RSPec
describe "simple snippet" do
  before(:each) do
    snippet =<<SNIPPET
increment a variable
= code
x = x + 1
SNIPPET
    @snippets = ExamplesToCode.parse(snippet)
  end
  it "should capture the snippet" do
    @snippets.should include("x = x + 1\n")
  end
  it "should ignore the comment" do
    @snippets.any? {|snip| snip =~ /increment a variable}.should be_nil
  end
end

Ах, я вижу еще одно изменение, которое я незаметно сделал при написании теста: мой examplesToCode.parse () возвращает массив (или другой итерируемый контейнер), так что его можно тестировать отдельно от самой итерации.

0 голосов
/ 11 сентября 2010

Интересно, что Дерик Бэйли из LosTechies.com только что опубликовал пост в блоге о коде для литья, чтобы его было легче тестировать:

http://www.lostechies.com/blogs/derickbailey/archive/2010/09/10/design-and-testability.aspx

Дерик упоминает, что в Ruby вам не нужно так стараться, как в других языках, таких как C #, чтобы ваш код был тестируемым.

Так что, возможно, ответ заключается в том, что ваш нисходящий рабочий процесс, похожий на BDD, который вы взяли из «Ничего, кроме .NET bootcamp» http://jpboodhoo.com/training.oo, не применяется так же, как в C #. То же самое можно сказать и о моем маленьком эксперименте с ката кода анаграммы, который я проводил в своем блоге несколько месяцев назад, где я изучал похожие методы в C # http://murrayon.net/2009/11/anagram-code-kata-bdd-mspec.html. Я пытаюсь выяснить, что это будет означать ... может быть, вам нужно отказаться от идеи интерфейсов, потому что в Ruby вы должны делать полиморфизм с помощью композиции, а не наследования.

0 голосов
/ 10 сентября 2010

Даже в ruby ​​есть только два способа развязать этот код: DI или сервисный локатор.Из двух я все еще предпочитаю DI, как вы описали.

Я не уверен в идиомах ruby, но я подозреваю, что они не будут беспокоиться об абстракции IFileFinder, вместо этого непосредственно вызывая Dir ["*. Makrkdown"] и затем переписывая это в тесте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...