Here’s something I haven’t done in a while – a genuine code blog entry. I needed to add a simple CSV file output, here’s how I did it, tests and all.
I used two gems, FasterCSV, which I assume that most of you are familiar with, and Comma, which is a nice little DSL for specifying CSV formats. (Thanks again to Jason Pearl for reminding me what the gem was called…).
My Cucumber test for the feature looked like this (details have been changed to protect the innocent…)
Background: Given I have valid adminstrative credentials And I have a user named "Charlie" And I have a user named "Bravo" Scenario: CSV Downloads When I go to the admin user page And I follow the link "CSV Report" Then I should see the table data: | Name | Assigned Number | Email | | Charlie | 123435 | email@example.com | | Bravo | 132134 | firstname.lastname@example.org |
Just one note on this is that both the assigned number (which you can imagine as an id given to to the user by some other entity, such as a social security number) and the email, are deterministic and generated by the step definition called by the background task.
The downside of this Cuke test is that it’s a little more explicit than I normally like, since the numbers and the email are kind of magical here. That said, the ability to test the tabular CSV data from a Cuke table is powerful enough that this still seemed like the best way. Other opinions welcome.
The only step here that is undefined and interesting is the last one. And you might think it’d be complicated, but in fact it’s super easy:
Then /^I should see the table data:$/ do |table| actual\_table = FasterCSV.parse(page.body) table.diff!(actual\_table) end
This does some complicated things compactly. The result of the eventual controller action is the text of the CSV file, and goes in
page.body (since I’m using Capybara). FasterCSV parses that back into an array of arrays, and then I used Cucumber’s table compare method
table#diff to compare the parsed CSV table to the table I passed in from the feature file. In theory, Cucumber provides a pretty output if the tables don’t match, but in practice I find I’m currently getting an error in the formatter.
This is nice, with the minor downside that the tables need to be an exact match. If that’s not feasible, then you can do more complex logic there to compare tables.
Next up was the model test, which I wrote more or less like this.
it "should produce expected csv" do @user\_1 = Factory.build(:user, :name =\> "Fred", :assigned\_number =\> 1, :email =\> "email@example.com") @user\_2 = Factory.build(:user, :name =\> "Barney", :assigned\_number =\> 2, :email =\> "firstname.lastname@example.org") @user\_1.to\_comma(:admin).should == ["Fred", "1", "email@example.com"] User.users\_to\_display.to\_comma(:admin).should == "Name,Assigned Number,Email\nBarney,2,firstname.lastname@example.org\,Fred,1,email@example.com\n" end
This test uses factory_girl to create two users, then uses
to_comma to invoke the Comma gem, first on a single user, then on a list of users. As you can see, Comma is smart enough to return an array of data for a single object, but a CSV formatted string when given a list.
Technically, I think the last line is unnecessary, because assuming the first part of the test passes and I know that the list generating function works then I’m really just testing Comma. Of course, since I’m using Comma for the first time, testing it once seems appropriate.
It’s also worth noting that I’m showing the final form of these tests, I did a little test based exploration to see what the format was going to be. So I ran the test and just outputted the result of the
to_comma, then wrote the test to match the format that I saw. Obviously, in that case you need to make sure that the format is what you want it to be, not just what the code outputs.
The Comma gem makes the model code super easy.
comma(:admin) do name assigned\_number email end
comma method sets up a comma format, the optional symbol names the format relative to any other comma formats specified for the class. Within the block, the basic idea is that each expression specifies a column of the CSV file, header and all. In this case, I’m using the basic form, which uses attributes of the current instance. Although I’m not doing it here, Comma lets you specify a custom header name, and also allows you to call arbitrary methods of associated objects. I like it, it’s short, easy to read, and to the point.
Comma also lets you easily use the CSV format in a controller. Here’s what the controller method looks like:
def index @users = User.users\_to\_display respond\_to do |format| format.html #default format.csv do send\_data(@users.to\_comma(:admin), :filename =\> "users.csv", :type =\> "text/csv") end end end
I didn’t add a controller test, on the grounds that there is no now controller logic and controller behavior is covered by the Cucumber test and the actual output is also covered by the model test (a previous controller test verifies that
users_to_display is called correctly.
And that’s it. Turned out to be pretty easy.