Tom M. wrote:
On Jun 14, 2006, at 7:25 AM, Eric D. Nielsen wrote:
How have other people apprached this issue? Do you mock with
abandon in
the controller tests, or only when absolutely required, or have you
found some sweet-spot between the two extremes that you can share?
I use mocks to replace methods that cause problems during testing due to
their activity being inappropriate for testing, or won’t force the code
to journey through the code path I’d like to test (think error
handling).
A good example of this is credit card processing. You probably don’t
want to hit you processing each time you test, and you’d probably like
to test error handling if they’re unavailable. With Mocks you can
replace the method that actually calls out to them, and have the new
one respect an attribute that you can set in the test that causes the
mock method to return error codes.
I agree that both your examples above (forcing hard to force error
conditions or tests depending on third party network services) are
places where mocks are absolutely required.
As for testing other layers via functional tests, how could that be
avoided? Even in unit tests, I don’t have very many methods that don’t
depend on other methods I’ve written, so it’s very rare when tests
only run through a single method.
Well in some sense it comes back to the “Humble Dialog” approach used
for testing user-interfaces in either web or desktop applications.
Assuming you’ve pushed functionality to “lowest” level (Model <
Controller < View) the tests required at the higher levels become
successively more trivial.
Tightly focused unit tests will often only exercise a single method, but
even if they don’t they shouldn’t be exercising code “too far” removed
from the object under test. I often use the “Law of Demeter” as a
guideline for determining the appropriate “reach” of testing.
As to how this can be acheived, lets take the example of a simple
“login” action in some controller:
If following TDD, we’d normally drive the development with two tests:
- If the login fails, did we do the appropriate thing
- If the login succeeds, did we do the appropriate thing.
Lets assume that the User model will handle logins. As a client of the
User we don’t care how they authenticate, only that they do so. As a
result a mock of User with configurable return values (Mock as Actor)
can easily sub-in for the actual User without a need for hitting the
database.
So we could use a Mock like (hypthetical code, not tested for actual
run-ability)
class User #note does not extend ActiveRecord::Base
def self.set_expected_return_for_login(value)
@@return_val = value
end
def login(username, password)
@@return_val
end
end
Now we only use the “published” api relevant to us at our controller
test. (Using class variables to avoid issues with figuring out a
good/simple way to handle the mock injection in this case.)
With test code like
def successful_login
User.set_expected_return_for_login(true)
post :action, {:username=>‘foo’,:password=>‘bar’)
assert_redirected_to :where_ever
assert …
end
As a result you force the path you want to test; you don’t rely on the
details of how the lower layer generates the response. Of course, most
mock frameworks have improved support to automate the process of setting
return values/expectations on methods and do some checking to make sure
that the method being knocked out exists in the production code and that
the parameter lists match, etc. Thus the mock can detect if you api
changes in many cases.
Of course you also have your integration/acceptence tests that also help
to catch places where you mocks masked an api change (say if you flipped
the order of two arguements in a function call)
Taken to an extreme this would lead to the controller tests being
de-coupled from the database and never needing to use the DB, fixtures,
etc…