Есть ли способ избежать объединения многих тестов в одном примере при использовании макетов? - PullRequest
3 голосов
/ 08 июля 2011

Частично следует из этого вопроса. Надеемся, что пример говорит сам за себя: есть класс WishlistReporter, который запрашивает один объект для данных и выводит их для другого объекта.

Проблема в том, что с двойным для БД я на самом деле тестирую целую кучу вещей в одном примере. Что не идеально.

Я могу разделить метод report () на методы collect_data () и output (). Но это не помогает: для того, чтобы протестировать метод output (), мне все равно нужно создать фиктивную базу данных и снова запустить collect_data ().

Есть ли способ обойти это?

describe WishlistReporter do

  it "should talk to the DB and output a report" do
    db = double("database")
    db.should_receive(:categories).and_return(["C1"])
    db.should_receive(:items).with("C1").and_return(["I1", "I2"])
    db.should_receive(:subitems).with("I1").and_return(["S1", "S2"])
    db.should_receive(:subitems).with("I2").and_return(["S3", "S4"])

    wr = StringIO.new

    r = WishlistReporter.new(db)
    r.report(db, :text, wr)

    wr.seek(0)
    wr.read.should =~ /stuff/
  end
end

(Что касается предыдущего вопроса: я совершенно счастлив издеваться над классом Db, потому что считаю его интерфейс внешним: часть «что», а не «как».)

Ответы [ 4 ]

1 голос
/ 24 июля 2011

В такой ситуации я бы не проверял вызовы @db, потому что это вызовы только для чтения, поэтому мне действительно все равно, происходят они или нет. Да, конечно, они должны произойти (в противном случае откуда берутся данные), но я не считаю это явным требованием к поведению WishlistReporter ... Если бы WishlistReporter мог создать отчет, не обращаясь к базе данных это было бы прекрасно для меня.

Я бы использовал db.stub! вместо db.should_receive и был бы совершенно доволен этим.

Но в тех случаях, когда вызовы к объекту, подвергающемуся насмешке, имеют побочные эффекты и являются явными требованиями, я делаю что-то вроде этого. (В этом примере по какой-либо причине мы требуем, чтобы объект db получил инструкцию перезагрузить свои данные перед тем, как мы их запросим.) Опять же, методы, которые возвращают данные, не требуют явной проверки, так как если вывод отчета правильно, тогда данные должны быть правильно извлечены из @db:

describe WishlistReporter do

  before(:each) do
    @db = double("database")
    @db.stub!(:reload_data_from_server)
    @db.stub!(:categories).and_return(["C1"])
    @db.stub!(:items).with("C1").and_return(["I1", "I2"])
    @db.stub!(:subitems).with("I1").and_return(["S1", "S2"])
    @db.stub!(:subitems).with("I2").and_return(["S3", "S4"])
    @output = StringIO.new
    @subject = WishlistReporter.new(@db)
  end

  it "should reload data before generating a report" do
    @db.should_receive(:reload_data_from_server)

    @subject.report(:text, @output)
  end

  it "should output a report" do
    @subject.report(:text, @output)

    @output.seek(0)
    @output.read.should =~ /stuff/
  end
end
1 голос
/ 08 июля 2011

Я всегда добавляю такие ожидания к блоку before.Я бы написал вашу спецификацию так:

describe WishlistReporter do
  let(:db) { double('database') }
  let(:wf) { StringIO.new }

  subject { WishListReporter.new(db) }

  describe '#read' do
    before do
      db.should_receive(:categories).and_return(["C1"])
      db.should_receive(:items).with("C1").and_return(["I1", "I2"])
      db.should_receive(:subitems).with("I1").and_return(["S1", "S2"])
      db.should_receive(:subitems).with("I2").and_return(["S3", "S4"])

      subject.report(db, :text, wr)
      subject.seek(0)
    end

    it 'talks to the DB and outputs a report' do
      subject.read.should =~ /stuff/
    end
  end
end
0 голосов
/ 24 июля 2011

Возможно, это не главное, но я думаю, что ваш код структурирован странным образом.Вместо ожидания вызова кода

categories = db.categories
categories.each do |category| 
  items = db.items(category) 
  items.each do |item|
    db.subitems(item)
  end
end

я бы ожидал, что он вызовет:

categories = db.categories
categories.each do |category| 
  items = category.items
  items.each do |item|
    item.subitems
  end
end

У объекта категории есть элементы.Вам не нужно передавать объект категории объекту БД, чтобы получить его элементы.Либо вы не используете ActiveRecord (или DataMapper, либо ...), либо используете его странным образом.

Тогда вы можете сделать:

let(:items) {[mock('item1', :subitems => ["S1", "S2"]), 
              mock('item2', :subitems => ["S3", "S4"])]}
let(:categories) {[mock('category', :items => items)]}
let(:db) {double('database', :categories => categories)}

, что обходится безчтобы назвать все вещи и разделяет заглушки между всеми спецификациями.Это заглушки, а не ожидания, но поскольку это не основная функциональность, которую вы тестируете, я согласен с Аароном В. в том, что заглушки являются более подходящими.


Редактировать после ответа на комментарии и перечитываниявопрос:

Ваша основная жалоба в этом вопросе заключается в том, что

с двойным для БД, я на самом деле тестирую целую кучу вещей в одном примере

Для меня это указывало на то, что ваша проблема была с двойником, потому что это представляется причиной, по которой вы «тестируете кучу вещей».Возможно, вам не нравятся ожидания (которые могут быть способом тестирования вещей, которые очень тесно связывают тест с реализацией), цепочечный список для его работы или осознанная необходимость их повторения.Все это имеет смысл, и ваш собственный ответ, использующий цепочечные ожидания, отражает стремление к упрощению использования двойного.

Однако, при повторном прочтении, реальная проблема оказываетсяо разделении какого-либо метода на два других метода и тестировании их по отдельности.К сожалению, эта аргументация вообще не поддерживается примером.Это не имеет ничего общего с двойником или ожиданиями, и вы могли бы оставить их без проблем.Вы хорошо знакомы с вашим кодом и своей проблемой, и это может показаться вам очевидным, но мне, как обычному читателю вашего вопроса, это не так.

Теперь о вашей проблеме: если вашапроблема действительно в том, что вы чувствуете, что разбивать report() на две функции бессмысленно, потому что вы не можете протестировать output() отдельно от gather_data(), потому что gather_data() необходимо вызвать для получения ввода для output(), тогдавы забываете две вещи:

  1. Разделение кода на отдельные функции - это всегда хорошая идея, если код можно логически разделить на эти функции.Это делает код более легким для понимания, более легким в обслуживании и более эффективным для тестирования.Последнее, потому что:

  2. Даже если вы не можете проверить output() отдельно от gather_data(), вы все равно можете проверить gather_data() отдельно.Этот тест даст вам более ранние предупреждения о меньшем куске кода, что облегчит идентификацию и решение проблем.

Связанная проблема, которую вы чувствуете, что не можете протестировать output() отдельно от gather_data(), не является проблемой, которая обычно решаема.В каждом случае у вас есть три варианта:

  1. Вы высмеиваете буквальный ввод для output() и тестируете его изолированным способом, основываясь на этом вводе (и обычно некоторых вариациях,для проверки нескольких путей кода).

  2. Вы пишете фиктивную версию collect_data (), которая частично дублирует его логику, но гораздо проще рассуждать и вызывать ее для создания ввода для output().

  3. Вы тестируете gather_data() и output() вместе, потому что прагматически слишком сложно тестировать output() в изоляции.

gather_data() требует ввода, и вы либо предоставляете его вручную, с помощью скрипта или кода, который все равно его производит.Последнее является вполне приемлемым прагматическим решением, при условии, что у вас есть отдельный тест gather_data(), который уже сообщает вам, был ли сбой комбинированного теста из-за сбоя gather_data() или из-за сбоя output().

0 голосов
/ 09 июля 2011

Отвечая на мой собственный вопрос, снова!Вот один из способов решения проблемы, но, честно говоря, я думаю, что для меня лекарство хуже болезни.чтобы сделать пример немного проще.Это не компромисс, с которым я чувствую себя комфортно.

...