Episode Script
When I last left the gem-love project, I had a failing acceptance test and a skeleton command object with no implementation. Today I’m going to start on that implementation.
Here’s the #execute
method as it stands now:
def execute
puts "Under construction..."
end
I think for a little while about what code I want to see in here. Since this is a class that adapts my code into the RubyGems command framework, I don’t want to see detailed, low-level code in here. I just want to see some very high-level, coarse-grained code that acts as an entry point into the code that’s fully mine to control.
One things for sure, I’ll need to pull a gem name from the command’s arguments in order to determine which gem is being loved. So I assign a variable for it, using the get_one_gem_name
method provided by RubyGems
. I know about this method from poking around in the source code of some of the standard RubyGems
commands earlier.
Now what? I think about what message I want to send in order to get the ball rolling, and the answer comes back pretty quickly: gem-love is all about people endorsing gems, so the message I want to send here is #endorse_gem
, with the gem_name
as an argument.
def execute
gem_name = get_one_gem_name
.endorse_gem(gem_name)
end
The next question is: what role makes sense to receive the #endorse_gem
message? gem_user
seems to fit the bill here, since it’s the users of gems who will be making endorsements.
def execute
gem_name = get_one_gem_name
gem_user.endorse_gem(gem_name)
end
OK, now where will I get an object to play the role of gem_user
? The obvious answer seems to be to instantiate one right here. I re-use the name of the role in naming the class that I instantiate, and I namespace inside the as-yet-nonexistent GemLove
module.
def execute
gem_name = get_one_gem_name
gem_user = GemLove::GemUser.new
gem_user.endorse_gem(gem_name)
end
By the way, if you’re unfamiliar with this approach of starting with a message and working backwards, I highly recommend reading Sandi Metz’ book, “Practical Object Oriented Development in Ruby”.
Running the single test shows that I’ve taken a step back: instead of a failure, I now have an error because of the unrecognized class name.
$ rspec F Failures: 1) gem love command endorsing a gem Failure/Error: command.invoke(*args) NameError: uninitialized constant GemLove::GemUser # ./spec/acceptance_spec.rb:50:in `run' # ./spec/acceptance_spec.rb:43:in `block (2 levels) in <top (required)>' Finished in 0.04126 seconds 1 example, 1 failure
I proceed to add the GemLove
namespace module and GemUser
class directly to the love_command.rb
file. I don’t think they’ll live here forever, but as I’m feeling out the design I don’t want to commit to a file hierarchy just yet.
module GemLove
class GemUser
end
end
I run the test again, and it complains about a missing method this time.
$ rspec F Failures: 1) gem love command endorsing a gem Failure/Error: command.invoke(*args) NoMethodError: undefined method `endorse_gem' for #<GemLove::GemUser:0x00000001755070> # ./spec/acceptance_spec.rb:50:in `run' # ./spec/acceptance_spec.rb:43:in `block (2 levels) in <top (required)>' Finished in 0.04367 seconds 1 example, 1 failure
Adding an empty method gets me back to my original test failure.
module GemLove
class GemUser
def endorse_gem(gem_name)
end
end
end
$ rspec F Failures: 1) gem love command endorsing a gem Failure/Error: gem_named('fattr').should have(1).endorsements expected 1 endorsements, got 0 # ./spec/acceptance_spec.rb:44:in `block (2 levels) in <top (required)>' Finished in 0.06332 seconds 1 example, 1 failure
This is where I want to be, since this is a test that should only go green once the command is working on both the client and server side.
Now, what should this #endorse_gem
method do? To answer that question, I make a new spec file:
require_relative '../../lib/rubygems/commands/love_command.rb'
module GemLove
describe GemUser do
end
end
I also need to add a require to the love_command.rb
file in order to make it loadable in isolation.
require 'rubygems/command'
Now I write an example for the #endorse_gem
method. This is going to be an integration test—that is, it will test how the method interacts with system code. Specifically, it will test that it makes the appropriate HTTP request. In order to verify this, I require a library called “WebMock”. WebMock worms its way into HTTP
libraries and makes it easy to fake out HTTP requests.
The test begins by instantiating a GemUser
. Then it prepares for a faked server interaction by telling WebMock to stub out any requests to www.gemlove.org/endorsements/fattr
. fattr
is the name of the gem I’ll be making a test endorsement of.
It then tells the gem_user
object to endorse the gem. Finally, it asserts that an HTTP POST
was made to the URL that was stubbed earlier.
require_relative '../../lib/rubygems/commands/love_command.rb'
require 'webmock/rspec'
module GemLove
describe GemUser do
describe '#endorse_gem' do
it 'registers a gem endorsement with the gem-love server' do
gem_user = GemUser.new
stub_request(:any, 'www.gemlove.org/endorsements/fattr')
gem_user.endorse_gem("fattr")
a_request(:post, 'www.gemlove.org/endorsements/fattr').
should have_been_made
end
end
end
end
When I run the test, it fails, letting me know that the expected POST
never came.
$ rspec spec/gem_love/gem_user_spec.rb F Failures: 1) GemLove::GemUser#endorse_gem registers a gem endorsement with the gem-love server Failure/Error: a_request(:post, 'www.gemlove.org/endorsements/fattr'). The request POST http://www.gemlove.org/endorsements/fattr was expected to execute 1 time but it executed 0 tim es The following requests were made: No requests were made. ============================================================ # ./spec/gem_love/gem_user_spec.rb:11:in `block (3 levels) in <module:GemLove>' Finished in 0.00941 seconds 1 example, 1 failure
I now switch over to the code under test. I add a require
for Net::HTTP
, and then fill in the blank method. It will construct a URL using the given gem_name
, and then POST
to that URL.
require 'net/http'
module GemLove
class GemUser
def endorse_gem(gem_name)
url = URI("http://www.gemlove.org/endorsements/#{gem_name}")
Net::HTTP.post_form(url, {})
end
end
end
Running the test again, it passes!
$ rspec spec/gem_love/gem_user_spec.rb . Finished in 0.00204 seconds 1 example, 0 failures
When I run all specs, however, I get failures.
$ rspec FF Failures: 1) gem love command endorsing a gem Failure/Error: command.invoke(*args) WebMock::NetConnectNotAllowedError: Real HTTP connections are disabled. Unregistered request: POST http://www.gemlove.org/endorsements/fattr with h eaders {'Accept'=>'*/*', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Ruby'} You can stub this request with the following snippet: stub_request(:post, "http://www.gemlove.org/endorsements/fattr"). with(:headers => {'Accept'=>'*/*', 'Content-Type'=>'application/x-www-form-urlencoded', 'User-Agent'=>'Ruby'} ). to_return(:status => 200, :body => "", :headers => {}) ============================================================ # ./spec/acceptance_spec.rb:50:in `run' # ./spec/acceptance_spec.rb:43:in `block (2 levels) in <top (required)>' 2) GemLove::GemUser#endorse_gem registers a gem endorsement with the gem-love server Failure/Error: a_request(:post, 'www.gemlove.org/endorsements/fattr'). The request POST http://www.gemlove.org/endorsements/fattr was expected to execute 1 time but it executed 2 tim es The following requests were made: POST http://www.gemlove.org/endorsements/fattr with headers {'Accept'=>'*/*', 'Content-Type'=>'application/x-ww w-form-urlencoded', 'User-Agent'=>'Ruby'} was made 2 times ============================================================ # ./spec/gem_love/gem_user_spec.rb:11:in `block (3 levels) in <module:GemLove>' Finished in 0.04752 seconds 2 examples, 2 failures
This is to be expected. Now that the LoveCommand
actually tries to make an HTTP request, the acceptance test is going to fail until it has a server to talk to. For the time being I just pend out the acceptance test.
specify 'endorsing a gem' do
pending "completion of the server side"
run 'gem love fattr'
gem_named('fattr').should have(1).endorsements
end
Now I have one pending spec and one passing one:
$ rspec *. Pending: gem love command endorsing a gem # completion of the server side # ./spec/acceptance_spec.rb:42 Finished in 0.05101 seconds 2 examples, 0 failures, 1 pending
I now have a first cut at a client-side implementation. It is obviously incomplete – for instance, I haven’t begun to think about identifying who the gem user is, let alone authenticating them with some kind of server-side account. But I want to get a “walking skeleton” working end-to-end before I begin to flesh out more functionality.
That’s enough for now. Happy hacking!