On Jan 11, 2008 5:11 PM, Jay D. [email protected] wrote:
The thing is that, ideally, you don’t want to have to make changes to
the tests for object A when you’re refactoring B.
WDYT?
Yeah, I buy that. Not everyone does though. Or at least not everyone
feels that it’s a particularly important goal.
I think the fear many of us classicists have is that if A uses B (and Bs interface changes) then A will pass but it shouldn’t. We also have to go around updating all the mocks for tests that use B. With a state based approach we wouldn’t have to change any tests assuming we’ve fixed the outer methods already. If we haven’t then our tests will fail and tell us we need to.
As David has mentioned before, that’s a matter of tool support.
Consider how this works if you’re using Java. You might have some
interface that looks like:
public interface Account {
void deposit(int amount);
void withdraw(int amount);
}
You’ve got a BankAccount class that implements it, and in tests that
depend on the Account role, you use mock objects.
If you were to rename deposit to credit, and withdraw to debit, the
first thing that would happen is that the compiler would complain that
BankAccount doesn’t implement the Account interface anymore. So you
can lean on the compiler to fix it.
Even better, you would use a refactoring tool, and that’d look for any
classes that implement Account and change their method names.
Conceptually, the same exact thing is possible and desirable in Ruby.
There are some extra hoops to jump through because we can’t lean on
the compiler like we can in Java. Otoh, we write Ruby a lot faster
than Java, and we don’t want to stab ourselves. So at this point it’s
still a worthwhile tradeoff (to me).
One problem is that in a language with interfaces, roles are made
explicit. In Ruby, any object can play any role, as long as it
implements the necessary protocol. Maybe we could do something at the
framework level to make the roles more explicit. Something like
describe AccountService, " transferring between two accounts" do
before(:each) do
@source = mock_role(:account)
@target = mock_role(:account)
@service = AccountService.new
end
it “should debit the source account” do
@source.should_receive(:debit).with(50)
@service.transfer 50, @source, @target
end
it “should credit the target account” do
@target.should_receive(:credit).with(50)
@service.transfer 50, @source, @target
end
end
Then you have a spec somewhere that verifies that the Account class
fulfills the account role.
It’s an interesting idea, and I remember it being bounced around a
while back, as something like “mock integration mode.”
There are plenty of obstacles though…you can’t just use respond_to?
on an instance of a class, because objects could use method_missing,
be decorated with behavior at runtime, etc. It also doesn’t seem
practical to try to define instances manually and then swap them in to
tests. You probably have to create so many objects for different edge
cases that it makes more sense just to use state-based verification in
the first place. This latter set of issues is just conjecture though,
as I’ve never given it a shot myself.
At this point, we can just use integration tests to catch the things
we miss, and be thankful that even large Rails projects seldom pass
25k lines of code
If demand increases for solutions to the mock
maintainability problem, one will arise.
I know, our integration tests should catch this. I’m curious what you guys mean by “integration tests” in this case. For example, in a rails app are the ‘integration tests’ always or usually testing the full rails stack by sending web requests and verifying against the response? I.e. simulating a user interaction?
In Rails apps, yes, a lot of the stories would go through the full
Rails stack. They don’t have to though.
User stories represent some facet of behavior which, once completed,
provides value to the customer. In short, working software. By
definition, then, stories must use real production implementations of
objects instead of mocks. This means they work quite nicely as
integration tests as well.
Pat