
- Simple Tests
- Double-Blind Tests
- Making it Practical with RSpec Matchers
The last article in this series defined the concept of double-blind test-driven development, but didn’t get much into real-world examples. In this article, we’ll explore several such examples.
The Example Application
This article includes a sample app that you can download using the link above. Be sure to checkout tag “double_blind_tests” to see the code as it appears in this article. The next article will have a lot of refactoring. I limited my samples to the model layer, where 100% coverage is a very realistic goal, and this is likely to be the greatest benefit.
I chose a simple high school scheduling app with teachers, the subjects they teach, students, and courses. In this case, I’m defining a course as a student’s participation in a subject. Teachers teach (ie, have) many subjects. Students take (have) many subjects, via courses. The course record contains that student’s grade for the given subject.
The database constraints are intentionally strict, and most of the validations in the models ensure that these constraints are respected in the application layer. We don’t want the user seeing an error page because of bad data. Depending on the application, that can be worse than actually having bad data creep in.
Associations
Here’s an example of a has_many association:
# excerpt from spec/models/teacher_spec.rb
describe Teacher do
it "has many subjects" do
teacher = Factory.create :teacher
teacher.subjects.should be_empty
subject = teacher.subjects.create Factory.attributes_for(:subject)
teacher.subjects.should include(subject)
end
end
In order to factor out our own assumptions, we have to ask what they are. The assumption is that the subject we add to the teacher’s subject list works because of the has_many relationship. So we’ll first test that teacher.subjects is, in fact, empty when we assume it would be. Then we’re free to test that adding a subject works as we expect.
Here’s a belongs_to association:
# excerpt from spec/models/subject_spec.rb
describe Subject do
it "belongs_to a teacher" do
teacher = Factory.create :teacher
subject = Subject.new
subject.teacher.should be_nil
subject.teacher = teacher
subject.teacher.should == teacher
end
end
Again, we’re challenging the assumption that the association is nil by default, by testing against it before verifying that we can add a teacher. This tests that this is a true belongs_to association, and not simply an instance method. This is the kind of thing that can and will change over the life of an application.
Validations
Let’s test validates_presence_of:
# excerpt from spec/models/teacher_spec.rb
describe Teacher do
describe "name" do
it "is present" do
error_message = "can't be blank"
teacher = Teacher.new :name => 'Joe Example'
teacher.valid?
teacher.errors[:name].should_not include(error_message)
teacher.name = nil
teacher.should_not be_valid
teacher.errors[:name].should include(error_message)
teacher.name = ''
teacher.should_not be_valid
teacher.errors[:name].should include(error_message)
end
end
end
This example was actually explained in detail in the last article. Validate that the error doesn’t already exist before trying to trigger it. Don’t just test the default value when you create a blank object, test the likely possibilities. Refactor the error message to DRY up the test and add readability. And finally, test by modifying the object you already created (as little as possible) rather than creating a new object from scratch for each part of the test.
A more complex version is needed to validate the presence of an association:
# excerpt from spec/models/subject_spec.rb
describe Subject do
describe "teacher" do
it "is present" do
error_message = "can't be blank"
teacher = Factory.create(:teacher)
subject = Factory.create(:subject, :teacher => teacher)
subject.valid?
subject.errors[:teacher].should_not include(error_message)
subject.teacher = nil
subject.should_not be_valid
subject.errors[:teacher].should include(error_message)
end
end
end
While the test is more complex, the code to satisfy it is not:
# excerpt from app/models/subject.rb
validates_presence_of :teacher
testing validates_length_of:
# excerpt from spec/models/teacher_spec.rb
describe Teacher do
describe "name" do
it "is at most 50 characters" do
error_message = "must be 50 characters or less"
teacher = Teacher.new :name => 'x' * 50
teacher.valid?
teacher.errors[:name].should_not include(error_message)
teacher.name += 'x'
teacher.should_not be_valid
teacher.errors[:name].should include(error_message)
end
end
end
And here’s the model code that satisfies the test:
# excerpt from app/models/teacher.rb
validates_length_of :name, :maximum => 50, :message => "must be 50 characters or less"
While you can definitely start to see a pattern in validation testing, this introduces a new element. Instead of freshly setting the name attribute to be 51 characters long, we test the valid edge case first and then add *just* enough to make it invalid – one more character.
This does two things: it verifies that our edge case was as “edgy” as it could be, and it makes our test less brittle. If we wanted to change the test to allow up to 100 characters, we’d only have to modify the test name and the initial set value.
validating a number’s range using validates_numericality_of:
# excerpt from spec/models/teacher_spec.rb
describe Teacher do
describe "salary" do
it "is at or above $20K" do
error_message = "must be between $20K and $100K"
teacher = Teacher.new :salary => 20_000
teacher.valid?
teacher.errors[:salary].should_not include(error_message)
teacher.salary -= 0.01
teacher.should_not be_valid
teacher.errors[:salary].should include(error_message)
end
it "is no more than $100K" do
error_message = "must be between $20K and $100K"
teacher = Teacher.new :salary => 100_000
teacher.valid?
teacher.errors[:salary].should_not include(error_message)
teacher.salary += 0.01
teacher.should_not be_valid
teacher.errors[:salary].should include(error_message)
end
end
end
And here’s the code that satisfies the test:
# excerpt from app/models/teacher.rb
validates_numericality_of :salary, :message => "must be between $20K and $100K",
:greater_than_or_equal_to => 20_000, :less_than_or_equal_to => 100_000
We’re doing the same here as in our testing of name’s length. We’re setting the edge value that’s *just* within the allowed range, then adding or subtracting a penny to make it invalid. I split up the top and bottom edge tests, because it’s better to test as atomically as possible – one limit per test.
Defaults
Another tricky database constraint to test for is a default value:
# excerpt from spec/models/course_spec.rb
describe Course do
describe "grade_percentage" do
it "defaults to 1.0" do
course = Course.new :grade_percentage => nil
course.grade_percentage.should be_nil
course = Course.new :grade_percentage => ''
course.grade_percentage.should be_blank
course = Course.new :grade_percentage => 0.95
course.grade_percentage.should == 0.95
course = Course.new
course.grade_percentage.should == 1.0
end
end
end
In this case, we can’t avoid having to recreate the model from scratch, because the nature of the implementation. There’s no actual code in the model that makes this happen, it’s purely in the database schema. Why should we test it, then? Because we test any behavior we’re going to rely on in the application. The fact that this model behavior is implemented at the database level (and therefore, not purely TDD) is a small inconvenience.
What’s the assumption our double-blind test is verifying in this case? That the value is only set in the absence of other values being explicitly assigned. Testing with nil and blank values verifies that the default doesn’t override them – it only works in the complete absence of any assignment. I also test an arbitrary (but valid) value as the anti-assumption test before finally verifying that the default is setting to the correct value.
Most default tests verify only that the correct default value is set – the double-blind version verifies that it’s acting only as a default value in all cases.
Summary
The point of double-blind testing is bullet-proof tests, that can’t be reasonably thwarted by antagonistic coding – whether that’s your anti-social pairing partner, or yourself several months down the road. The bottom line is this: test all assumptions.
That being said, this is very time consuming, and we can see a ton of repetition even in this small test suite. What we need is a way to get back to speedy testing before our boss/client notices it now takes an hour to implement one validation.*
The answer lies in RSpec matchers, which are easy to implement, and can grow with your application. The benefit is not just speedier development – it’s also consistency across your application. We’ll explore that in the last article of this series.
Nested Comments in Ruby on Rails, part 2: Controllers and Views
January 26, 2011Part 1 of this series came out exactly 3 months and 3 days ago. Special thanks to a reader named Edward who prodded me to finally add the controllers and views to this.
Going beyond the model layer for nested comments introduces a new programming idiom: recursion. Some ruby developers may not be familiar with it – especially if your experience is mostly web-related, where the need doesn’t come up as often. Recursion in a nutshell is the act of a method calling itself. If you’ve seen Inception, The ability to have dreams within dreams within dreams means those dreams are recursive. If you haven’t seen the movie, think of russian matryoshka dolls. You won’t experience star-studded special effects with the dolls, but you’ll at least get the idea of recursion.
Unlike russian dolls or most of Leo’s recent work, recursion in software is potentially infinite. Practically speaking though, it’s more like the doll thing. After all, a system only has so many resources, and recursion is expensive in this regard – the method must copy itself in memory at each layer, local variables and all. On the plus side, they tend to be lightning fast compared to standard iteration using loops. And in our case, we’ll be hitting the database at each layer. We’ll ignore the dangers in our simple app, though.
Routing
Let’s start with our routing file:
# config/routes.rb NestedComments::Application.routes.draw do resources :comments do resources :comments end resources :posts do resources :comments end root :to => 'posts#index' endWorking backward, we’re making our Posts controller’s index action our default route. That’s just to get the app functional. Next comes something interesting: nesting our comments inside of our posts. Interesting, but boring. Finally, the main event: nesting our comments within our comments!
Before you get too excited and start pulling out your Nana’s childhood russian doll set for comparision, this isn’t true recursion. It’s well documented that nesting resources any more than two layers deep is painful and unnecessary, so think of this as the lamest russian doll ever.
Controllers
First, our Posts controller, which is less exciting:
# app/controllers/posts_controller.rb class PostsController < ApplicationController def index @posts = Post.all end def show @post = Post.find(params[:id]) end def new @post = Post.new end def create @post = Post.new(params[:post]) if @post.save redirect_to posts_path, :notice => "Your post was created successfully." else render :action => :new end end endWe’re setting up a pretty standard restful resource here, with a couple actions skipped for simplicity. Now the comments controller (get those dolls ready):
# app/controllers/comments_controller.rb class CommentsController < ApplicationController before_filter :get_parent def new @comment = @parent.comments.build end def create @comment = @parent.comments.build(params[:comment]) if @comment.save redirect_to post_path(@comment.post), :notice => 'Thank you for your comment!' else render :new end end protected def get_parent @parent = Post.find_by_id(params[:post_id]) if params[:post_id] @parent = Comment.find_by_id(params[:comment_id]) if params[:comment_id] redirect_to root_path unless defined?(@parent) end endIt’s not much bigger, but there’s a lot going on here! First, since comments are nested, we have to look for a parent. We’re only creating comments in this example, so we only have those related actions. Comments will always be shown on a post page.
The really exciting part is after a successful comment creation. How do we redirect back to the post page? For all we know, this comment could buried down 12 layers of replies. All we really have access to so far is the parent of the object. This necessitates a new model method:
Recursive functions are often short and sweet for two reasons: they’re already complex by nature, and adding more code than necessary would make them unmanageable. Also, they’re getting a lot done in just a few lines. In this case, the second line is the key: if “commentable” (the parent object) is a post, return that. Otherwise, call this same method on the parent, which will in turn check if *it* is a Post, and so on.
I could have written it shorter, like this:
In fact, I did at first. But the extra code that checks and sets an instance variable is caching the result. This way, if we call the same method on an object more than once, it stores the result for future use. Remember, recursion can be expensive – especially when the database is involved.
Views
Finally, it’s view time, with one more bit of recursion for fun.
Or post views are standard scaffolding mostly, with the exception of the show view:
Notice we have the partial app/views/comments/_comment.html.erb. We’re calling this for each of our post’s comments. Nothing too fancy here. Now, for the partial itself:
# app/views/comments/_comment.html.erb <li class="comment"> <h3><%= comment.title %></h3> <div class="body"> <%= comment.body %> </div> <p><%= link_to 'Add a Reply', new_comment_comment_path(comment) %></p> <% unless comment.comments.empty? %> <ul class="comment_list"> <%= render :partial => 'comments/comment', :collection => comment.comments %> </ul> <% end %> </li>This partial is recursive! The comments controller doesn’t have a show method, because we’re never going to view a comment by itself. Instead, the show-like code is in this partial, and at the end it checks to see if *this* comment has comments. If so, it calls the partial again on the whole collection. The end result is a nested, bulleted list of comments. This is not very sexy if you fire up the code yourself, but it’s a great starting point.
Summary
Hopefully this article as done a good job of explaining both recursion, and how to use it to achieve nested comments in your applications. If you’re new to recursion as a concept, haven’t seen Inception, didn’t inherit russian dolls from Nana or receive them as a snazzy graduation present, and my explanation somehow fell short, it’s a well documented programming idiom. There are tons of resources online, so take the time to learn this powerful tool, then learn not to overuse it :)
Please download the code and play with it if you want to learn more – the code is fully test-driven so you can see how that works, which is just as important.
On a final note, I’m tempted to do a follow-up article with ajax and some nicer formatting. Perhaps in 3 months and 3 days…
Tags:comments, nesting, polymorphism, posts, Rails, recursion, recursive, Ruby, ruby on rails
Posted in Database, Rails, Testing | 5 Comments »