开发者

Is there any way to avoid combining many tests into one example, when using mocks?

Partly following on from this question. Hopefully the example speaks for itself: there's a WishlistReporter class which asks one object for data and outputs to another object.

The problem is that with the double for DB, I'm actually testing a whole bunch of things in a single example. Which is not ideal.

I can split the report() method into gather_data() and output() methods. But that doesn't help: in order to test the output() method I would still need to create the mock db and run gather_data() again.

Is there a way around this?

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

(In reference to the previous question: I'm perfectly happy to mock the Db class because I consider its interface to be external: part of the "what" not the "how".)


In this sort of situation, I wouldn't verify the calls to @db because those are read-only calls, so I really don't care if they happen or not. Yes, of course they do have to happen (otherwise where is the data coming from), but I don't think of it as an explicit requirement on the behavior of WishlistReporter... If WishlistReporter could produce the report without talking to the database that'd be perfectly fine by me.

I would use db.stub! instead of db.should_receive and be perfectly happy with that.

But for cases where the calls to the object being mocked have side-effects and are explicit requirements, I do something like this. (In this example, for whatever reason we require that the db object be instructed to reload its data before we query it.) Again, the methods that are returning the data don't need to be explicitly verified, since if the report output is correct, then the data must have been pulled from @db correctly:

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


I always add this kind of expectations to a before block. I'd write your spec like this:

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


Answering my own question, again! Here's one way to solve the problem, but frankly I think that for me, the cure is worse than the disease.

describe WishlistReporter do
  before(:each) do
    @db = double("database")
    @wr = WishlistReporter.new(db)
  end

  describe "#gather_data" do        
    it "should talk to the DB" do
      @db.should_receive(:categories).and_return(["C1"])
      @db.should_receive(:items).with("C1").and_return(["I1", "I2"])
      @db.should_receive(:subitems).with("I1")
      @db.should_receive(:subitems).with("I2")
      @wr.gather_data
    end
  end

  describe "#report" do
    it "should output a report" do
      @wr.should_receive(:gather_data).and_return( {"C1"=>{"I1"=>["S1", "S2"], "I2"=> etc etc}})
      file = StringIO.new

      @wr.report(:text, file)

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

end

I think the point here is I've just introduced a massive complexity in the code to make the example a bit simpler. That's not a trade-off I feel comfortable with.


Perhaps this is besides the point, but I think your code is structured in a strange way. Instead of expecting the code to call

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

I would expect it to call:

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

A category object has items. You shouldn't need to pass a category object to a db object to get at its items. Either you're not using ActiveRecord (or DataMapper, or ...) or you're using it in a strange way.

Then you could do:

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

which sidesteps having to name all the stuff and shares the stubs between all specs. These are stubs, not expectations, but as this is not the core functionality you're testing, I agree with Aaron V. that stubs are more appropriate.


Edit after responding to comments and re-reading the question:

Your main complaint in the question is that

with the double for DB, I'm actually testing a whole bunch of things in a single example

To me that indicated that your problem was with the double, because that is presented as the reason you are 'testing a while bunch of things'. Perhaps you don't like the expectations (which can be a way of testing things that binds the test very closely to the implementation), the chained list to get it working or a perceived need to repeat them. All of that makes sense and your own answer, using chained expectations, reflects the wish for a simplification in the way the double is being used.

However, when reading it carefully once more, the actual problem turns out to be about splitting some method into two other methods and testing them separately. Unfortunately, that line of reasoning is not supported by the example at all. It has nothing to do with the double or the expectations and you could have left them out without a problem. You are very familiar with your code and your problem and this may all seem obvious to you, but to me, as a casual reader of your question, it isn't.

Now as to your problem: if your problem is indeed that you feel that splitting report() into two functions is pointless, because you can't test output() separately from gather_data(), because gather_data() needs to be called to produce the input for output(), then you are forgetting two things:

  1. Splitting code into separate functions is always a good idea, if the code can be logically divided into those functions . It makes the code easier to understand, easier to maintain and more effectively testable. The last, because:

  2. Even if you can't test output() separately from gather_data(), you can still test gather_data() separately. That test will give you earlier warnings about a smaller piece of code, which makes it easier to identify and solve problems.

The related problem, that you feel you can't test output() separately from gather_data(), is not a problem that is generally solvable. In every case, you've got three choices:

  1. You mock the literal input to output() and test it in an isolated fashion, based on that input (and usually some variations, to test multiple code paths).

  2. You write a mock version of gather_data() that partly duplicates it's logic, but is much simpler to reason about and call that one to produce input for output().

  3. You test gather_data() and output() together, because pragmatically it's too much trouble to test output() in isolation.

gather_data() needs input and you either supply it by hand, by a script or by the code that produces it anyway. The last one is a perfectly acceptable pragmatic solution, assuming you have a separate test of gather_data() that already tells you whether the combined test failed because gather_data() failed or because output() failed.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜