Testing Rails Validators

It’s challenging to test Rails custom validators.

I recently had to write a validator to require that an entered date is before or after a specified date.

It didn’t seem like writing the validator would be too difficult – I’ve written custom validators before, and date comparisons aren’t all that tricky. But when it came time to write the tests, I ran into several issues. And since I always try to follow TDD / test-first, I was blocked before I even began.

The biggest issue was the ActiveModel::EachValidator#validates_each API. It’s definitely not a well-designed API. You write your validator as a subclass, overriding validates_each. The method takes a model object, the name of the attribute of the model being tested, and the value of that attribute. You can also get the options passed to the custom validator via the options method. To perform a validation, you have to update the model’s errors hash.

The big flaw in the API is that instead of returning a result, you have to update the model. This needlessly couples the model and the validator. And it violates the Single Responsibility Principle — it has to determine validity of the field, and it has to update the errors hash of the model. This is not conducive to testing. Testing this method requires testing that the side-effect has taken place in the collaborator (model), which means it’s not really a unit test any more.

So to make it easier to unit test the validator, I broke the coupling by breaking it into 2 pieces, one for each responsibility. I moved the responsibility for determining validity to a separate method, which I called errors_for. It returns a hash of the errors found. This simplified the validates_each method to simply take the result of errors_for and update the errors hash of the model:

def validate_each(record, attribute_name, attribute_value)
  record.errors[attribute_name].concat(errors_for(attribute_value, options))
end

This made it much easier to unit test the errors_for method. This method doesn’t even need to know about the model — only about the value of the attribute we’re trying to validate. We simply pass in the attribute’s value and the options.

So we could write the tests without even pulling in ActiveRecord or any models:

describe DateValidator do
  let(:validator) { DateValidator.new(attributes: :attribute_name) }
  let(:errors) { validator.errors_for(attribute_value, validation_options) }

  describe 'when attribute value is NOT a valid date' do
    let(:attribute_value) { 'not a valid date' }
    it { errors.must_include 'is not a valid date' }
  end

  describe 'when attribute value IS a valid date' do
    let(:attribute_value) { Date.parse('2013-12-11') }
    it { errors.must_be :empty? }
  end
end

And the errors_for method looked something like this:

def errors_for(attribute_value, options)
  unless attribute_value.is_a?(Date)
    return [options.fetch(:message, "is not a valid date")]
  end
  []
end

Integration testing can also be a bit of a challenge. I recommend following the example from this Stack Overflow answer. Basically, create a minimal model object that contains the field and the validation. Then test that the model behaves like you expect with different values and validations.

Introspective

Today I was informed that I’ve been impeding progress on our team.
This was somewhat shocking, since I feel like I’m always pushing
forward to make our team and myself better.

Like most anyone, my initial reaction to criticism was defensiveness.
I don’t handle criticism well. (You might find that hard to believe,
after reading the rest of this post. Maybe it’s just the confrontation
part I don’t handle well.) Thankfully the blow was softened somewhat,
because the person providing the criticism didn’t tell me directly —
they told Amos, a trusted colleague of mine. Amos then passed it on to
me. I’m grateful for that — this is the best way for me to have received
that criticism.

What I did next is the astonishing thing. I swallowed my pride for a
minute. Not an easy thing for me to do, for all the usual reasons, and
more. I decided to address the problem head on. If someone was telling
someone else that I was causing some sort of a problem, then even that
perception was a problem that I needed to address. So I decided to hold
a retrospective for myself. Retrospectives are the way Agile teams address
problems and improve how they work. If it would work for a team, I figured
it should work for an individual.

I’d held retrospectives by myself before. Before I even knew about
retrospectives, I’d taken some time to assess how I had done things
on a couple of projects I had worked on as an independent contractor. But
those were about technical decisions I had made and processes I had
followed. This one would be different. This one was about a personal
shortcoming, an action of mine that had caused some harm. This one
involved feelings and emotions.

So I asked Amos to facilitate my one-person retro. We’d both done a lot
of retrospectives, but neither one of us had done anything like this before.
We had a bit of trouble getting started. We didn’t have any details
about why the anonymous person felt the way he felt. So it was hard
to address something that nebulous. So I asked Amos if he could follow
up with the person. Luckily, the person was online and able to respond
in real time.

So we talked things through some. We went on some tangents. We made some
progress — things that I could write down as take-aways and action items.
We got off-topic again, and then came back to it. Then Amos decided to
play The Why Game. This worked amazingly well. It helped me to find the
root reasons why I was holding back change. Basically I’m afraid of
failure. Going deeper, we looked into why I’m afraid of failure. This
line of inquiry was particularly effective. Hopefully it’ll be enough for
me to stop being an impediment to change. Here are my notes / take-aways:

  • Be careful playing devil’s advocate in public.
    • People will think I’m taking that stance.
  • Don’t always take the boss’s side in public.
  • Don’t be the person to allow the team to take the easy path.
  • Why do I think that managers won’t let us try a 2nd time?
  • Why do I think we might fail the first time?
    • I want to put us in a better position to succeed.
  • I’m being too conservative.
  • Be willing to fail. Don’t fail to try.
  • Don’t say we shouldn’t try because I think other people don’t want to try.
    • That is just self-perpetuating.
  • I’m afraid of failing because I haven’t done it much.
    • Because I’m good at learning from other people’s failures.
    • But this is holding me back and making me cautious.
      • Despite the fact that I try to not be cautious.
    • So I need to work to counter-act those fears.

At first, I thought that I’d never heard of anyone doing anything like
this. But then I recalled that I’d heard about somewhere that everyone
does this, with their whole team. That would be hard. I’d have to
trust everyone in the room.

But I would recommend this for anyone that can pull it off. It’s hard,
but it has a large payoff. Just like a team retrospective, it feels good
making this kind of breakthrough, and knowing that just spending that
little amount of time has made me a better person. (Mine took about an hour.)
I think the hardest part is finding someone (or a group of people) you
trust to be your facilitator.

I’ve decided to call this thing an introspective. A retrospective is
about looking back. This is about looking inward. I’d be interested to
find out who is doing this kind of thing. Does it have a commonly accepted
name? How does it work? What techniques work best? If you’ve got any answers or ideas, please comment below, or tweet me @CraigBuchek.

So thank you to that anonymous person. Your way of addressing this was
probably more effective than you could have imagined that it might be.
I hope that this exercise will help me make the changes required, to make
me a better person, and help the team become the best that we can be.

Amos has written up his thoughts on the introspective.

What’s Your Open Source Resolution?

I’ve been using GNU/Linux since 1994, starting with a Slackware 2.2 CD-ROM I ordered from the back of a magazine. I’ve been heavily involved in my local GNU/Linux community since 1999, and I give frequent presentations at various groups. I’ve made some small contributions to several Open Source projects.

But I don’t feel like I’ve done enough to contribute to the wider community. So this year, I’m going to resolve to step up my contributions, and do more.

Change my role in the LUG

I was lucky enough this past year to find someone interested in helping to run the St. Louis GNU/Linux Users Group (STLLUG). I’ve been running the group so long that I’ve somewhat stagnated. It’s great to find someone who will bring a fresh energy and new ideas. Thanks, Andrew!

Mostly freed from the more general administrative tasks (mostly planning meeting topics and speakers), I’m hoping to find the time to work on a few projects. First, I want to overhaul the web site. It needs a new look, as well as an easier way to update it. I’m planning to implement some sort of CMS — possibly WordPress or Octopress, or at the very least, a git repo to manage updates.

Blog regularly

I installed WordPress a long time ago, but I haven’t published many posts. I’ve got a few drafts in various stages. So this year I’m going to actually get the blog going, and hopefully post about once a week. I’ll be sharing things I learn on the Internet, as well as some of the things I learn from my experiences with various technologies and techniques.

Contribute more significant patches

There are a few items I’ve been meaning to work on and contribute. I’d like to finally get around to them:

  • Make the GNU sort command be able to (easily) sort a list of IP addresses, or semantic version numbers.
  • Add MySQL-style commands to PostgreSQL: SHOW DATABASES; SHOW TABLES; DESCRIBE table. (I’m not sure how welcome these additions might be, but I think they’d make transitioning from MySQL easier for people.)

Publish and maintain several Ruby gems

I’ve been working on a few Ruby gems. I’d like to get them to a state where they’re useful to more people. These include:

  • A way to simplify standard CRUD controller actions
  • A way to declare list views in a manner similar to SimpleForm or Formtastic
  • A way to declare ActiveRecord attributes in models using Virtus
  • A higher-level interface to ROM (Ruby Object Mapper)
  • A similar high-level way to define Perpetuity attributes in models using Virtus
  • My Rails template, making it much easier to start a Rails app

Build some apps

I enjoyed building a quick Rails app during last year’s Rails Rumble, even if it doesn’t turn out to be useful or make any money. I’d like to create a few small apps again this year, either in Rails or Apache Cordova. I’ve got some ideas — we’ll see how if they turn into anything useful or worth selling.

Podcast more often

Last year, I started joining the This Agile Life podcast. I enjoy it. I’m hoping to continue with that, and possibly doing some podcasting about Ruby and Rails.

Present at a conference

I’m going to attempt to give a presentation at a conference. I often give presentations at local groups, but haven’t addressed a larger audience. I’ve got a few ideas rattling around, and will see if I can turn them into presentations worthy of a larger conference.

What’s your Open Source resolution?