Thư viện tri thức trực tuyến
Kho tài liệu với 50,000+ tài liệu học thuật
© 2023 Siêu thị PDF - Kho tài liệu học thuật hàng đầu Việt Nam

Rails for Java Developers phần 8 ppsx
Nội dung xem thử
Mô tả chi tiết
TESTING INTERACTIONS WITH MOCK OBJECTS 227
Like Java, Ruby does not ship with a mock object library. We will use
FlexMock.9 You can install FlexMock via RubyGems:
gem install flexmock
To make FlexMock available to all tests in a Rails application, require it
in test_helper.rb:
Download code/rails_xt/test/test_helper.rb
require 'flexmock'
Now for a test. The Rails XT sample application does not have a manager layer, so we will introduce a new feature in the controller layer.
Instead of simply accessing all quips, users should be allowed to filter
quips based on their preferences. Our application will store user preferences in the session and use a third-party API to filter content. The
third-party API will be implemented through a @filter_service instance on
the controller.
It is possible to call the FlexMock API via freestanding classes. It is
much simpler, however, to just begin our test case by including FlexMock::TestCase:
Download code/rails_xt/test/functional/quips_controller_test.rb
include FlexMock::TestCase
Adding FlexMock::TestCase gives us helper methods for creating mocks,
and it automatically validates the mocks during teardown.
The QuipsController should provide a new method, list_with_user_filter. This
method should return all quips, minus any that are rejected by the
FilterService. Here is the test:
Download code/rails_xt/test/functional/quips_controller_test.rb
def test_list_with_user_filter
filter = flexmock("filter")
filter.should_expect do |m|
m.filter(Array,nil).returns([quips(:quip_1)])
end
@controller.instance_variable_set('@filter_service', filter)
get :list_with_user_filter
assert_equal [quips(:quip_1)], assigns(:quips)
assert_response :success
assert_template 'list_with_user_filter'
end
9. http://onestepback.org/software/flexmock/
TESTING INTERACTIONS WITH MOCK OBJECTS 228
On line 2, the flexmock method creates a mock object. The argument
is a name that will be used in error messages. In the Java version,
the mock had to have a specific interface so jMock could know what
methods the mock should simulate. Since Ruby is dynamically typed,
we do not specify any specific module or class for the mock.
On line 3, we set the expectations for the mock. FlexMock takes advantage of Ruby’s blocks to set expectations through a recorder object. On
line 4, the block parameter m is a recorder. Instead of saying m.should_
expect.filter, we can simply say m.filter; the should_expect is implicit. FlexMock’s matching of parameters takes advantage of Ruby’s case equality
operator (===). So, the first argument to filter must be an instance of
Array. This array will be the result of Quip.find(:all), and we could have
chosen to match it exactly by instantiating the entire collection in the
test. The second argument nil matches the user’s filtering preferences,
which are initially nil.
On line 6, we set the controller’s @filter_serviceto our mock filter. By
calling instance_variable_set, we avoid the requirement that the controller
provide a setter for @filter_service. There is no call to verify at the end of
the method; FlexMock mocks verify automatically at the end of the test.
Ruby’s blocks and case equality make it easy to define flexible argument matching. Imagine that we wanted to verify that none of the quips
passed to the @filter_service has non-nil text. FlexMock would handle
this with FlexMock.on:
Download code/rails_xt/test/functional/quips_controller_test.rb
matcher = FlexMock.on {|args| Array === args && args.all? {|a| a.text}}
filter.should_expect do |m|
m.filter(matcher,nil).returns([quips(:quip_1)])
end
The previous tests demonstrates another advantage of mock objects.
Mock objects allow you to test interactions with code that does not exist
yet. In testing the QuipsController, we never create a real filter service. At
the time of this writing, there is no real filter service. This decoupling
lets teams of developers work on related subsystems without having to
wait for completed implementations of every object.
The mock objects in this section replace objects not under test and verify that those objects are called in an appropriate fashion. Sometimes
you want to replace objects not under test, but you don’t care how they
are called. This subset of mock object capability is provided by stub
objects.
REDUCING DEPENDENCIES WITH STUB OBJECTS 229
7.8 Reducing Dependencies with Stub Objects
It is all too easy to write fragile tests that depend on other classes. Think
about how you might test this simple controller method:
Download code/people/app/controllers/people_controller.rb
def create
@person = Person.new(params[:person])
if @person.save
flash[:notice] = 'Person was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
To test both branches of the code, you will need a valid Person and
an invalid Person. The problem is that you are supposed to be testing
PersonController, not Person. If you pick valid and invalid arguments for
the real Person class, you introduce a dependency on Person. This is a
maintenance headache. When you change Person, you will break the
PersonTest (OK), but you will also break the PersonControllerTest (aargh).
To avoid this problem, we can test a stub version of Person. The stub stub
replaces Person with behavior that we define locally, breaking the external dependency. This probably sounds similar to the mock objects from
the previous section, and it is. In fact, we will use the same library for
stubs, FlexMock. Here is a stub-based test for creating a Person:
Download code/people/test/functional/people_controller_test.rb
def test_create_succeeds
flexstub(Person).should_receive(:new).and_return {
flexmock('person') do |m|
m.should_receive(:save).and_return(true)
end
}
post :create
assert_response :redirect
assert_redirected_to :action => 'list'
end
On line 2, flexstub temporarily modifies the behavior of Person. For the
remainder of this test, calls to Person.new will invoke this block of code
instead. On line 3 we mock an instance of Person, and on line 4 we cause
save to always succeed. This test method will test how the controller
handles a successful Person create, regardless of how the real Person
class works.