hardscrabble 🍫

By Max Jacobson

Psst. Check out my RubyConf 2017 talk, There are no rules in Ruby.

blog posts

stubbing constants in ruby tests

21 Jul 2015

Let’s say you have some code that doesn’t have tests and you want to add tests. Because the code wasn’t written with tests in mind, it might be hard to write tests for it.

Last year, DHH wrote a blog post called Test-induced design damage which argued that code “written with tests in mind” (quoting myself from the previous paragraph, not his post) isn’t necessarily better than code written with other goals in mind, and can often be worse.

When I’ve attempted TDD, I’ve had times when I felt like it helped me write nice code and times where I think I went too far like if you’re rolling out some dough to make a pie crust but you’re watching TV and you end up spreading it out until it covers the whole counter.

So this code you want to test. What makes it hard to test? Let’s say it’s a Ruby class in a Rails app. In Rails apps, all the classes are available for all the other classes to reference and depend on. Maybe it looks like this:

class PieCrust
  def initialize(pounds_of_dough)
    @lbs = pounds_of_dough
    @ready = false
  end

  def prep
    RollingPin.new.roll_out(self)
    Oven.new.preheat
    Instagram.upload(self)
    @ready = true
  end

  def ready?
    @ready
  end
end

(This example is revealing more about my state of mind right now than anything)

But like, look at this thing. How do we write a test that covers all that? And what if we want “fast tests”?

(Note: a lot of people really want their tests to run fast. For TDD enthusiasts, this allows a tight feedback loop between when you write the failing test to when you write the code which makes the test pass. They kind of expect to do that over and over and over and don’t want to wait for more than an instant. I don’t think they’re wrong to want that although I am personally often OK with waiting for longer than an instant.)

(Other people want their tests to run fast as a general principle, like they want their cars to go fast or their legs to.)

Let’s say there are a couple hundred classes in your app and a bunch of initializers which run whenever your Rails application is loaded and none of them are strictly necessary for you to feel confident that your PieCrust class is behaving properly. All you want to know is that calling the prep method rolls out the crust, preheats the oven, and uploads the pie crust to instagram.

You already know that all those things work as long as you call them properly because you have unit tests for those classes demonstrating how to call them properly. You can see that here they’re being called properly, so you don’t feel the need to actually load the rolling pin code, or the oven code, or the instagram code. And you really don’t want to upload something to instagram every time you run your tests.

What do you do?

There’s the dependency injection approach, where you might refactor the earlier code to look like:

class PieCrust
  def initialize(pounds_of_dough, roller: RollingPin.new, heater: Oven.new, photo_sharing_service: Instagram)
    @lbs = pounds_of_dough
    @roller = roller
    @heater = heater
    @photo_sharing_service = photo_sharing_service
    @ready = false
  end

  def prep
    roller.roll_out(self)
    heater.preheat
    photo_sharing_service.upload(self)
    @ready = true
  end

  def ready?
    @ready
  end

  private

  attr_reader :roller, :heater, :photo_sharing_service
end

Which lets you leave your other application code the same – it can interact with PieCrust the same as it did before, as the default values are totally sensible there. But you can now write a test like this:

RSpec.describe PieCrust do
  describe '#prep, #ready' do
    it 'rolls the crust, preheats the oven, and uploads the photo' do
      roller = double
      heater = double
      photo_sharing_service = double

      pie_crust = PieCrust.new(10, roller: roller, heater: heater, photo_sharing_service: photo_sharing_service)
      expect(pie_crust).to_not be_ready

      expect(roller).to receive(:roll_out).with(pie_crust)
      expect(heater).to receive(:preheat).with_no_arguments
      expect(photo_sharing_service).to receive(:upload).with(pie_crust)

      pie_crust.prep

      expect(pie_crust).to be_ready
    end
  end
end

I feel like this is OK but it feels like it prescribes and duplicates a lot of the stuff that’s going on in the application code, which doesn’t feel ideal to me but also feels kind of fine.

Is there any other way? There is. I learned this one from my brilliant coworker Máximo Mussini. While looking through a gem he made (Journeyman, a lightweight replacement to FactoryGirl), I discovered some super interesting code, and without his blessing I extracted it out into a gem which I may use to help me write tests in the future. That gem is called stub_constant, and using it, I would revert that code to the first version, avoiding the arguably awkward dependency injection.

You might be assuming: OK, so if you don’t inject the constants, you must load the entire application environment, because you’re going to be depending on those dependencies. Or like, maybe you don’t load the entire application environment, but you must at least load the code which defines those 3 constants, right?

Nope! Doing that is usually really difficult, because once you load the files which define those constants, those files are probably referencing other constants, so you need to load the files which define those constants and now you might as well just load the whole thing…

So… “Whaaaat?” You might say.

Here’s what the constant-referencing isolation tests would look like:

require "stub_constant"
StubConstant.klass(:Oven)
StubConstant.klass(:RollingPin)
StubConstant.module(:Instagram)

RSpec.describe PieCrust do
  describe '#prep, #ready?' do
    it 'rolls the crust, preheats the oven, and uploads the photo' do
      roller = double
      expect(RollingPin).to receive(:new).with_no_arguments.and_return(roller)

      heater = double
      expect(Oven).to receive(:new).with_no_arguments.and_return(heater)

      pie_crust = PieCrust.new
      expect(pie_crust).to_not be_ready

      expect(roller).to receive(:roll_out).with(pie_crust)
      expect(heater).to receive(:preheat).with_no_arguments
      expect(Instagram).to receive(:upload).with(pie_crust)

      pie_crust.prep

      expect(pie_crust).to be_ready
    end
  end
end

Sooo it got even more prescriptive. But it’s a pretty neat way to do a purely isolated test without needing to rewrite your code.

How would you test this? I want to know.

making a pull request to rails

14 Jul 2015

Today I impulsively made a pull request to Rails, which feels kind of like a milestone for me. It’s about two years since I started using Rails at the Flatiron School. It’s also been about two years since the method I edited was last edited. I feel like there may be a reason and it won’t get merged, but who knows? I feel sort of exposed.

This post was helpful for me: Eileen Codes | Getting Your Local Environment Setup to Contribute to Rails.

In order to make a proper contribution, I needed to know that my change didn’t break the existing tests, and so I needed to be able to run the tests.

I also wanted to be able to add tests and confirm that they pass. So I really needed to be able to run the tests.

I had some trouble configuring my local environment, despite the post explaining it well (…databases…), BUT the post mentions rails/rails-dev-box which lets you skip a lot of the environment configuration by using a preconfigured virtual machine, and that turned out to be a god send of a casual aside for me and I’m writing this post largely to promote the existence of that project because it’s awesome.

It uses vagrant which is kind of magical… I had never used it before and it totally blew my mind. It allowed me to have a tmux session with windows like I’m used to, with the code open in vim in one tab using all the existing configuration from my local Mac machine, and then another tab where my code changes were immediately available for running the tests against in the Linux virtual machine. It was super seamless and sweet.

Here’s a four minute long gif of what it looks like – I’m refraining from embedding it so you don’t need to download a 4mb image if you don’t want to, and so you can open it in a new tab to start at the beginning easily if you want to.

I don’t really love my solution and I should probably consider it further, but I know the tests are passing, including the new one I added. I think being a little sleep deprived lowered my inhibition tonight.