Nick left a question on my many-to-many associations post. He wanted to know more about sorting by date and querying…
So I decided to (over achieve and) show how I would approach that as if I were adding a new feature:
- Adding a “date” key field and an index
- Using Cucumber to drive the new feature from the desired behavior à la BDD.
- Querying with a date sort tacked on…
You can follow my progress from the earlier version to this one by examining the commit history:
Commit History for Adding Dates to the Event Class
Looking at the “added initial event date functionality” commit, you can see how I added some new files to allow for Cucumber and MongoMapper:
Commits for Event Dates
For Cucumber, I added the “/features” stuff.
And since I wanted to start testing the code, I had to make the database functional, so I added “mongo_db.rb” – and I assume you have MongoDB installed and running locally.
##### MONGODB SETTINGS #####
MongoMapper.connection = Mongo::Connection.new('localhost', 27017, :pool_size => 5)
MongoMapper.database = "event-development"
I am never quite sure if there is a “perfect” way to wire up small, non-Rails apps like this one to use MongoDB. But what I have done works good enough to allow for a simple example to run.
BDD Cycle
So I began the BDD cycle by creating a “feature branch” and switching to a new branch from the current master branch:
git checkout -b event_dates master
Next I wrote the feature for the new behavior:
Scenario: Sort Events by Date
Given A set of events
When I display the events
Then I should see them sorted by latest date first
When you run Cucumber, you will get the default code for steps – all pending of course. Naturally, I did one step at a time, to take each one from “pending” to green. Working on the Given, then the When, and finally the Then, I came up with these steps:
Given
Here I wanted to generate a set of data so that we could see if the list was sorted properly. You can check out the code on github for the randomness baked into the “dummy_*” helper methods. And I wanted to create the events for two users.
Given /^A set of events$/ do
fred = User.find_or_create_by_name("fred")
(1..10).each do
Event.create(:title=>"#{dummy_word} #{dummy_word 3} #{dummy_word 10}",
:date => dummy_date,
:user => fred)
end
harry = User.find_or_create_by_name("harry")
(1..10).each do
Event.create(:title=>"#{dummy_word} #{dummy_word 3} #{dummy_word 10}",
:date => dummy_date,
:user => harry)
end
Event.count.should == 20
end
When
Sort of playing along as if this is a web request, I coded this step to return a “response” that is generated by the “list all events” class method. In true BDD fashion, this is the code I wish I had 🙂
When /^I display the events$/ do
@response = Event.list_all
end
Then
The proof is in scanning the resultant “response” object to ensure date order is correct:
Then /^I should see them sorted by latest date first$/ do
first = Date.parse(@response.third[0..10])
last = Date.parse(@response.last[0..10])
first.should > last
end
Outside – In
As soon as I ran the “When” I got a failure due to Event.list_all not existing. So off to the RSpec-land we go, to write the expectations for list_all. This is known as the “Outside-In” approach. (I learned this term from the excellent RSpec book, and it looks like you can watch a video about it here.)
The behavior expressed above (in Cucumber) can be thought of as more of an “outer,” acceptance/integration style of test. Typically it would be the User Interface (UI) – but I have been known to blur that line, since not all code is about UI and since Cucumber is so darn fun to use. Working at this outer level often leads to expressing what is expected of our actual code; in this case, that Event have a class-level method that returns a list of it’s instances (a.k.a., documents). Since we are talking about the behavior of a class, that is more of the “inside” of the application. Not something that an external user might care so much about directly, but rather something that supports the end behavior in an indirect fashion. For the “inside” we turn to RSpec (basically a better-than-unit-test, unit test tool).
- Outside ≈ Feature ≈ Cucumber
- Inside ≈ unit test ≈ RSpec
describe "#list_all" do
it "should show each event, ordered by date" do
response = Event.list_all
response.should_not be_empty
response.class.should == Array
response.size.should == Event.count + 2 #for title and column header
# Yes, you should not output stuff as part of your tests, but this *is* our UI :-)
puts response
end
end
Many times, my initial pass at a new method is to simply return what is expected. Then write another test to make that fail. Sort of “sneak up on the answer.” But here it was easy enough to simply output some real text from the get-go:
def self.list_all
response = []
response << "%s %s %s" % ["*"*10, "LIST OF EVENTS", "*"*10]
response << "%6s %15s %s" % ["Date", "TITLE", "Attendees/Interested/Likes"]
events = Event.all(:order => 'date desc')
events.each {|e| response << e.to_summary}
response
end
Oops. LOL (:-D) While writing this post, I found a mistake when testing my code a bit further than my initial commit.
I decided to remove the order part of the query, which revealed that the Cucumber feature still passed. Crap! So, it wasn’t so easy after all! Dope.
events = Event.all
Second Attempt
My initial way of generating records resulted in the documents magically being in the right date order by default. Tests passed, but the test was wrong – not vigorous enough testing!
Note to self:
no matter how trivial things seem, write failing tests
that contradict each other – so to speak.
So, I tweaked the document generator to better randomize the list of events such that we won’t accidentally have them all in proper order by default:
...
fred = User.find_or_create_by_name("fred")
(1..10).each do
Event.create(:title=>"#{dummy_word} #{dummy_word 3} #{dummy_word 10}",
:date => Time.now + (rand(60)*secs_in_day - 30),
:user => fred)
end
...
And, instead of just spot-checking the order, here is a new RSpec test to ensure each event is in proper order, date-wise:
it "should show each event, ordered by date" do
response = Event.list_all
response.should_not be_empty
response.class.should == Array
response.size.should == Event.count + 2 #for title and column header
r_prior_date = Date.parse(response[2][0..10])
response[3..response.size].each do |r|
date = Date.parse(r[0..10])
date.should < r_prior_date
r_prior_date = date
end
# Yes, you should not output stuff as part of your tests, but this *is* our UI :-)
puts response
end
Now we’re talking! A failed test:
'Event#list_all should show each event, ordered by date' FAILED
expected: < Wed, 20 Apr 2011,
got: Wed, 20 Apr 2011
And similarly, I re-wrote the Cucumber test. Funny thing, further testing revealed that the error above was not actually a legitimate fail as it turns out! I discovered I needed “<=” instead of just “<“– sometimes the simplest things aren’t so simple after all. Especially when it comes to setting up sample data.
Then /^I should see them sorted by latest date first$/ do
last_date = Date.parse(@response.third[0..10])
@response[3..@response.size].each do |r|
date = Date.parse(r[0..10])
date.should <= last_date
last_date = date
end
end
And I got the above test to fail by “stepping back” and removing the “order by” clause to get me back to an original, non-sorted listing. Good! Now we can step forward again and try to get the functionality that we are looking for to work.
Cucumber Failing Tests
I re-enabled the order clause to see if the tests would now pass:
events = Event.all(:order => 'date desc')
And, fortunately, the tests are indeed passing:
Cucumber Passing Tests
Commit on Green
Once you get the bits of functionality working, commit (even if you still have pendings). Committing locally has no downside 🙂 Here I will commit and push to the repo (the “$” is my prompt (well, not really), and the #comments are not part of the command line!):
$git status #You can see your changes
$git commit -a -m "added initial event date functionality" #commit your changes
$git checkout master #switch to the master branch
$git merge --no-ff event_dates #merge all of your local feature branch commits, preserving each
$git push origin master #Pump it up to the repo
$git branch -d event_dates #Get rid of the feature branch
This rhythm gets to be very familiar.
More on Querying
You may have noticed some of the queries above, and this was one of Nick’s questions…
With MongoMapper, you can chain Plucky queries as follows:
def self.list_all(a_user=nil)
response = []
response << "%s %s %s" % ["*"*10, "LIST OF EVENTS", "*"*10]
response << "%6s %15s %s" % ["Date", "TITLE", "Attendees/Interested/Likes"] events = nil if a_user.nil? events = Event.all(:order => 'date desc')
else
events = Event.where(:user => a_user).all.sort(:date.desc)
end
events.each {|e| response << e.to_summary}
response
end
I added a new feature that shows off the above query, quickly ran through the entire process again, from git checkout to git push, with Cucumber and RSpec and code in between. You can find it all in the source code.
Cucumber Show Events For User
Git is really an amazing revision control tool… I can’t imagine using anything else now. Here is an example of looking at the “Network Graph” of my little project:
Github Network Graph