Thursday, August 27, 2009

My Broken Test Found My Missing Object

I'm writing a rails application off and on for the last six months or so. Yesterday I started a major architectural change which broke a few tests. Getting those tests to pass proved pretty difficult until I finally realized what the tests were trying to tell me: "You are missing a domain object."

I'm writing a chore tracking tool for families. You enter the chores you need done on a weekly basis, and it helps organize them by letting you assign due dates and family members to do the chore. I call the family members 'doers'.

Initially, I had a User model and a Doer model. The User was the login, and the Doers were the ones who were assigned to chores. Perhaps you see the problem: doers did not have a way to login. That meant the User had to give her user credentials to all the doers, so each doer could update his or her chore. This also meant doers could reassign chores they didn't want to do or change due dates, or delete the chore altogether.

I came up with two solutions, because you can't pick the simplest until you have at least two. First, I could copy a lot of the User login code over to the DoerController and then check against both models. This would lead to a lot of duplication until I could refactor it out. Second I could remove Doer and just have Users with different roles. This would involve more changes (possibly).

I drew a little IBIS chart in my notes and determined idea two would be the best long term solution.

I created a migration that deleted the doer table, added the fields from Doer to User, and copied all the Doers into the Users table. I then began to fix all the code this broke. When I got to the tests that were failing, I ran into a little issue. See, I added a field to User called user_id that linked Users playing the role of Doers to the User with the login credentials. This created a problem with my rails fixtures because I needed the fixtures to assign a user_id to a User before the User was created. The User didn't have an id yet. ActiveRecord doesn't create an id until the model is saved. I was in a paradox.

The real problem was that I was using User to determine which Board the Doers belonged to. But I didn't even have a Board model. And I need one. That's what the failing tests were trying to tell me. And that is why tests and the five whys are so important. If the tests would have just worked, I never would have had the opportunity to start digging into why the tests didn't work.

Imagine this conversation going on in my head (which I strongly advise you don't do very often):

Me: The tests are failing!
Me: Why are the tests failing?
Me: Because the fixtures don't know how to load user_id's of Users that don't exist yet.
Me: Why are you trying to load user_id's of Users that don't exist yet?
Me: Because each user determines which set of chores the user_doers have access to.
Me: Why is that a User responsibility?
Me: Because the User is the one who owns the board.
Me: What board?
Me: A HA!

Here's my best secret of consulting: a well thought out question goes a lot farther than even the best proposed solution.

So, test and test first. When you are stuck, ask why until you are unstuck.

4 comments:

John Goodsen said...

Nice post. Glad to see you use IBIS - I want to have support for Ibis discussions in radtrack. Thinking about your rails project - would radtrack work for you instead? If so, maybe you should just hack what you need into a branch from radtrack???? (hint, hint - especially, since I'm using it on 2 real projects right now) :-)

Curtis Cooley said...

I thought about branching radtrack, but I'm focusing choreboard on more of a weekly rhythm. I am using radtrack to track my stories for choreboard though :)

I like the IBIS format. Especially when I'm arguing with myself.

Karmen Blake said...

Nice writeup of your experiences. Maybe we could pair on it sometime at a ruby user group. :)

Curtis Cooley said...

Hi Karmen,

I'd enjoy pairing with you. I'm planning on actually attending the next meeting. It's this Wed, right?