I don’t always install the Rails gem globally, but when I do, I cry myself to sleep at night. —Me
Ruby gems fall into one of two categories based on how you install them. There are the gems you install globally, outside the context of an application, like Bundler or Pry. And then there are the gems you install locally, inside the context of an application, like Faraday or BCrypt. But then there’s Rails.
Rails is an application dependency so it should be installed locally with Bundler. But the Rails gem is also used to generate the skeleton of the application, which includes the Gemfile that Bundler uses in order to install Rails locally. It’s a bit chicken and the egg.
You’re probably wondering why that even matters. You just install the Rails gem globally.
$ gem install rails
Fetching: thread_safe-0.3.4.gem (100%)
Successfully installed thread_safe-0.3.4
Fetching: minitest-5.4.2.gem (100%)
Successfully installed minitest-5.4.2
Fetching: tzinfo-1.2.2.gem (100%)
Successfully installed tzinfo-1.2.2
Fetching: i18n-0.7.0.beta1.gem (100%)
Successfully installed i18n-0.7.0.beta1
Fetching: activesupport-4.1.6.gem (100%)
Successfully installed activesupport-4.1.6
Fetching: erubis-2.7.0.gem (100%)
Successfully installed erubis-2.7.0
Fetching: builder-3.2.2.gem (100%)
Successfully installed builder-3.2.2
Fetching: actionview-4.1.6.gem (100%)
Successfully installed actionview-4.1.6
Fetching: rack-1.5.2.gem (100%)
Successfully installed rack-1.5.2
Fetching: rack-test-0.6.2.gem (100%)
Successfully installed rack-test-0.6.2
Fetching: actionpack-4.1.6.gem (100%)
Successfully installed actionpack-4.1.6
Fetching: activemodel-4.1.6.gem (100%)
Successfully installed activemodel-4.1.6
Fetching: arel-184.108.40.20640414130214.gem (100%)
Successfully installed arel-220.127.116.1140414130214
Fetching: activerecord-4.1.6.gem (100%)
Successfully installed activerecord-4.1.6
Fetching: mime-types-2.4.3.gem (100%)
Successfully installed mime-types-2.4.3
Fetching: mail-2.6.1.gem (100%)
Successfully installed mail-2.6.1
Fetching: actionmailer-4.1.6.gem (100%)
Successfully installed actionmailer-4.1.6
Fetching: thor-0.19.1.gem (100%)
Successfully installed thor-0.19.1
Fetching: railties-4.1.6.gem (100%)
Successfully installed railties-4.1.6
Fetching: sprockets-3.0.0.beta.2.gem (100%)
Successfully installed sprockets-3.0.0.beta.2
Fetching: sprockets-rails-2.2.0.gem (100%)
Successfully installed sprockets-rails-2.2.0
Fetching: rails-4.1.6.gem (100%)
Successfully installed rails-4.1.6
22 gems installed
Because Rails has a ton of dependencies and installing it globally makes an absolute mess of your gem list. It completely obscures any relevant information you might be trying to find in there. And good luck uninstalling it; you’re going to have to manually uninstall each dependency.
I realize this is definitely a nit, but it bothers me to no end. You don’t need Rails globally except to generate new app skeletons.
I thought I’d be clever and install Rails with
--ignore-dependencies, but it turns out the Rails binary isn’t even in the Rails gem, it’s in the Railties gem. I tried installing the Railties gem without dependencies, but it turns out the logic for generating a Rails skeleton is spread throughout a bunch of dependencies.
So I whipped up a simple gem called Railyard. It sandboxes Rails, installing it locally inside the gem, on demand. You can use it to switch to any Rails version you like and generate a Rails skeleton for it, without having to install Rails globally.
$ gem install railyard
Fetching: thor-0.19.1.gem (100%)
Successfully installed thor-0.19.1
Fetching: railyard-0.1.0.gem (100%)
Successfully installed railyard-0.1.0
2 gems installed
$ railyard new next_big_thing
Ah, that’s so much better.
The canonical example for an ActiveRecord callback is sending a welcome email after a
User is created.
class User < ActiveRecord::Base
I remember how awesome that felt the first time. It seemed like such great design. I was fat-modeling, skinny-controllering with the best of them. But the joy didn’t last long.
One time, I manually created a bunch of users from the console in production. The intention was to set up their accounts and then personally send them an email inviting them to try out the product. But before I could do that I started getting confused emails asking why they’d been signed up for some product they’d never heard of. It hadn’t occurred to me that the welcome emails would be sent if I created users from the console.
Another time, I was trying to speed up an agonizingly slow test suite. Out of curiosity I commented out the welcome email callback on
User. The test suite ran 10 seconds or so faster. It hadn’t occurred to me that every time a user was created in a test it would send a mailer to the test delivery queue, and that all that time would add up to a significant amount.
I don’t mean to specifically pick on sending emails in a callback, that’s just a really common example. It could be changing an attribute before saving or even creating an associated record. The point is that when you use an ActiveRecord callback you’re saying you always want it to run every time. But that’s not what I really wanted.
I didn’t want a welcome email to be sent if I created a user from the console. I didn’t want a welcome email to be sent every time a user was created in a test. I really only wanted a welcome email to be sent when a user signed up. Which means the right place to send the email was in the controller, where it was in the first place before I tried to get clever.
Almost every ActiveRecord callback I’ve ever written I eventually removed later on after I realized it was actually contextual—it had only seemed like it should always run. Now I just don’t use them at all any more.
Inspired by Gary Bernhardt’s gem Do Not Want I wrote a gem that codified my intent not to use them. It’s called Hold Please and it will raise an exception if you or anyone else tries to use an ActiveRecord callback. As you’d expect it will allow third-party gems to use them so they don’t break.
If you want to ensure you don’t inadvertently relapse and prevent anyone else on your project from doing the same, check out Hold Please.
Enjoy your saner future.
Database transactions are a way of making multiple queries to a database such that all of them must succeed or none of them will. This helps prevent data from getting into an unexpected state. Take for example a user signing up.
It’s possible that the second query might fail, which would leave the user in a bad state: the user will be created but won’t be charged. Transactions to the rescue!
subscribe! raises an exception the transaction will fail and the database will roll back. It will be like nothing ever happened.
This is all pretty straight-forward, but what can be a little confusing is how to test this. Let’s walk through it.
Transactions only roll back when an exception is raised, so we need to force
subscribe! to raise an exception. One way to do this would be with your mocking and stubbing framework of choice.
it "must not create a user if billing fails" do
But you’ll find that doesn’t quite work. The transaction will fail and correctly roll back the database but the uncaught exception continues to bubble up and fails our test before
User.count can be tested.
In order to make them pass we’ll need to catch the exception somehow. One way would be to test the exception.
Another approach would be to just rescue the exception.
Either of these will work, and the test will pass, but both of these solutions are janky. They’re not quite right. Asserting the exception isn’t right because we don’t technically care that there was one. Rescuing the exception isn’t right because we’re not actually doing anything in response to the rescue. Both are just hacks we’re using in order to swallow the exception so we can test what we’re actually trying to test: the transaction.
There is a more idiomatic way to do this, and that’s with the
suppress kernel method added by ActiveSupport, which exists for this very purpose—suppressing exceptions.
It’s entirely a semantic difference, but semantics exist for a reason. In the future, when this test is read or changed, the semantics will convey that the exception is irrelevant. The use of
suppress reinforces the intent of the test.