Test-driven development – Simple example

Posted by Brian in Rails, snacks (March 9th, 2007)

This is a really simple example of why unit tests are important. I was just working with a project where I am creating workshops. Students enroll in these workshops and I have a method on the Workshop model that handles the enrollment.

I have a method that looks like this:

class Workshop < ActiveRecord::Base has_many :enrollments has_many :students, :through =>:enrollments, :source=>:user

# Enrolls a student in the class. (receives the user id)
def enroll(id)
e = self.enrollments.find_or_initialize_by_user_id(id)
e.enrolled = true
e.withdrew = false
e.workshop_id = self.id if e.new_record?
e.save
end

...
end

One thing that I noticed from this code was the fact that while this worked, it didn’t handle a business rule t hat all registration systems face – seat limits. In this system, a workshop has a limited number of seats. When the workshop is full, you need to start rejecting enrollment requests.

Since I am all into test-driven development, I decided I should really code up a test that ensures that a user can’t register for a workshop without any seats.


def test_should_not_enroll_because_class_is_full
# Assume @dreamweaver_workshop was found by the setup method
@dreamweaver_workshop.seats = 0
@dreamweaver_workshop.save
u = User.find(:first)
assert !@dreamweaver_workshop.enroll(u.id)
end

When I ran my test, it failed because the current code allows me to basically overbook a class. The key here is that I added a new business requirement and added the unit test before I implemented the requirement.

Once I had the test written, I went and did the following:

# Enrolls a student in the class. (receives the user id)
def enroll(id)
e = self.enrollments.find_or_initialize_by_user_id(id)
if self.remaining_seats > 0
e.enrolled = true
e.withdrew = false
e.workshop_id = self.id if e.new_record?
e.save
else
return false
end

end

Raise your hand if you see a problem here…. and you can assume that I am decrementing the seats appropriately.

When I looked at the code, it made sense… when I do @workshop.enroll(1) it will work, but if I just create an enrollment a different way, say directly, I’ll still have a way to bypass my business rule.

I really needed to embrace validations a little more. I added the following method to my Enrollment model:


def validate
errors.add_to_base("The specified workshop is full") if self.workshop.remaining_seats < 1 end

I reverted my changes to the workshop class and reran my tests.


.....
Finished in 2.616155 seconds.

6 tests, 28 assertions, 0 failures, 0 errors

Hurray! Of course there's still something about this approach that I don't like.... If the student is already enrolled, it just updates the record for the student anyway. I'll live with it for now though because we withdraw students but still leave their enrollment record there.

So there you have it, a real-world example of test-driven development. I hope that helps someone else out there.

Comments are closed.

Sorry, the comment form is closed at this time.