why spy?
I did learn the concept of spy from Ash, (sorry that I am
too lazy to find the exact tweet right now) after realizing
what it is trying to accomplish, I did implement something
similar in muack, as I feel it might be useful in some cases.
At first I didn't use it personally, because mostly I am
using bacon to test my codes, which I could define nested
context and shared context, making setup quite easy.
*
Before jumping into details, let me clarify what I think
spies could bring us:
* We could always stub the stuffs and then only examine
it later if we're interested. We would not be forced to
put assertion right into setup method/block.
* We could put all assertions in the end of operations,
not the begin of operations. So all the assertions
could be put in one place.
In the first point, that means we don't have to care what
we're going to test, we can simply put all stubs into the
setup block.
The advantage of this is that we don't have to think too
much whenever we're writing the setup block, and we could
share the setup block between tests, as we don't make
assertions there.
If we're using mocks instead, then we're putting assertions
right into setup block, so that this block can't really be
shared between tests.
The disadvantage is, slower tests, as we're running some
codes which we might never be interested.
This is more an implementation advantage than design advantage.
*
The second point is, we would be very clear about what
we're really testing as we put all assertions in one place
instead of scattering around due to the way we define mocks.
This would be more a design (or readability) advantage.
*
In bacon, since we have nested and shared context, the first
advantage does not really matter because bacon is flexible
enough to do whatever we want to setup.
But in an test/unit based testing framework, or to be more
precise, a testing framework without nested and shared context,
it would be a good advantage to workaround this shortness.
*
ok, let's see what I meant by nested context in bacon:
require 'bacon'
Bacon.summary_on_exit
describe Bacon do
before do
@a = 0
end
should 'have @a' do
@a.should == 0
end
describe '@b' do
before do
@b = 0
end
should 'have @a and @b' do
@a.should == 0
@b.should == 0
end
end
should 'have no @b' do
instance_variable_defined?(:@b).should == false
end
end
By running this, bacon would print:Bacon - should have @a @b - should have @a and @b - should have no @b 3 specifications (4 requirements), 0 failures, 0 errorsThat means, I could easily reuse the
before
block to setupthe stuffs I *exactly* need. This is somehow hard in a test/unit
based testing framework, e.g. the one Rails uses by default.
If we want to do that kind of stuffs in a testing framework
without nested context, we might need to extract those setup
methods into normal methods, and call them accordingly in the
setup method (in pure test/unit) or in a setup block (with Rails
extension), splitting one test case into multiple test cases.
Something like this:
require 'test/unit'
class TestA < Test::Unit::TestCase
def setup
@a = 0
end
def test_have_a
assert_equal 0, @a
end
end
class TestB < TestA
def setup
super
@b = 0
end
def test_have_a_and_b
assert_equal 0, @a
assert_equal 0, @b
end
end
By running this in Ruby 2.0, the result is:Loaded suite - Started .. Finished in 0.001353 seconds. 2 tests, 3 assertions, 0 failures, 0 errors, 0 pendings, 0 omissions, 0 notifications 100% passed 1478.20 tests/s, 2217.29 assertions/sYou might argue that this is not actually harder to do,
and it's even more clear what's really happening, as
we don't even have to show that there's no
@b
definedin
TestA
since it's obvious.However this has some disadvantages. First we're forced to
split related tests from a class to another class. We can't
easily tell from this example, but I think we tend to put
tests together because of functionality similarity instead
how we setup the stubs.
Of course they are related, so sometimes they are the same
thing. But suppose we're testing a function with correct
and incorrect fake input, we might really need different
setup/before method/block for testing the same function.
And, this doesn't work in Rails' controller tests. I am
not too surprised for this... Rails always has their weird
hacks so that normal Ruby can't really work.
On the other hand, if we want to do something like this in
bacon, we have shared context. Here's an example:
require 'bacon'
Bacon.summary_on_exit
shared :a do
before do
@a = 0
end
end
shared :b do
before do
@b = 0
end
end
describe '@a' do
behaves_like :a
should 'have @a' do
@a.should == 0
end
end
describe '@b' do
behaves_like :a, :b
should 'have @a and @b' do
@a.should == 0
@b.should == 0
end
end
It's more like mixin instead of inheritance, making reusemuch more easier.
What My points are...
Since bacon is much more flexible (and also much faster) than
test/unit, some advantages of using spies are gone. i.e. The
first point I was referring about, that we could simply stub
everything without thinking too much.
If we have flexible setup methods, we could still manage to
only setup what we want in each test cases without splitting
them into different places.
*
I only realized this whenever I was writing a bunch of controller
tests in Rails. Now I wonder if I should try to run bacon in
Rails, then I don't have to suffer from this pain.
However I might not want to do this by myself, since integrating
with Rails is really a PITA as we would definitely touch the
internal details of eternal weirdness of Rails...
In the last, let me show you my current muack spies in Rails
controller tests: (slightly edited though)
# I have no interested in adding test directory into $LOAD_PATH
require_relative '../test_helper'
class UsersControllerTest < ActionController::TestCase
setup do
any_instance_of(User) do |u|
# we don't care if the user processes or not
stub(u).process.proxy
end
end
def spy_user_process
any_instance_of(User) do |u|
# now we care if the user has processed
spy(u).process
end
end
def test_process
sign_in(user)
post :process, :id => user.to_param
assert_redirected_to dashboard_path
spy_user_process
end
# other tests for process which should call User#process
# they need to insert `spy_user_process` in the end
def test_show
sign_in(user)
get :show, :id => user.to_param
assert_response :success
# as we don't care if the user has processed or not,
# we don't spy in the user stub.
end
end
If I were writing in bacon, maybe I'll write it this way:require_relative '../test_helper'
describe UserController do
should 'show' do
sign_in(user)
get :show, :id => user.to_param
response.status.should == 200
end
describe 'process' do
before do
any_instance_of(User) do |u|
# process should be called once
mock(u).process.proxy
end
end
should 'process successfully' do
sign_in(user)
post :process, :id => user.to_param
response.redirected_to.should == dashboard_path
# we don't have to spy here because mock would
# check for us. however we could still do it the
# spy way if we want.
end
# other tests for process which should call User#process
end
end
Actually even if we have spies, the bacon one is still
better, as we don't have to duplicate the line we're
dispatching spies in other tests! (i.e. spy_user_process)
Or you might think it's still better to explicitly say
what we're testing for... well, at least we have choices
in bacon :P
0 retries:
Post a Comment
Note: Only a member of this blog may post a comment.