Using and testing automatic accessors and virtual attributes of ActiveRecord models
An ordinary ActiveRecord model object has an attribute for each column in its table. Each attribute has a reader method and a writer method. However, ActiveRecord also defines accessors on the fly to hold non-column query results. Also, sometimes it’s useful to define a new attribute, one not corresponding to a column or even to a field in a query result. These different kinds of extra attributes can interfere with one another, making them tricky to use and test. The best defense is to understand all of them well before using any of them. Let’s have a look.
Kinds of extra attributes and their uses
(Inbound) virtual attributes. One reason to add an attribute to a model is to capture input, perhaps from a form, that doesn’t directly correspond to a column attribute but that can be used to calculate one or more column attributes. For example, a UI might have a single text box for an address but a model might store it as street, city, state and ZIP code. This kind of extra attribute was described long ago and is well known in the Rails world. When someone says “virtual attribute” this is usually what they mean. I’ll call it ‘inbound’ in this article to distinguish it from other kinds.
Here’s an example:
class Customer def address "#{street}\n#{city}, #{state} #{zip}" end def address=(string) self.street, self.city, self.state, self.zip = string.match(/([^\n]+)\n([^,]+),\s+([A-Z]{2}) (\d{5})/).captures end end
This implementation is completely virtual: it doesn’t add an instance variable, only new accessors that read and write existing attributes. It’s also possible to store the virtual attribute in an instance variable and override both the real and virtual attributes’ setters to update both the instance variable and, using super
, the virtual attributes. I’ve also seen non-attribute data stored in an instance variable and used only when the instance is saved, but that makes the model harder to think about (until the instance is saved, the real attributes don’t reflect the non-attribute data) so don’t do it that way if you can avoid it.
Outbound virtual attributes. Another reason to add an attribute to a model is to hold related information to display. Information that can’t be included in the model in a single efficient ActiveRecord query can be stored in what I’ve also seen called a virtual attribute. For example, each topic in a list of forum topics might need to be annotated with a score of the level of activity in that topic relative to all the others, which might take too long to calculate on the fly. Let’s call this kind of virtual attribute ‘outbound’ to distinguish it from the ‘inbound’ kind.
Implementing an outbound virtual attribute is trivial: just add an attr_accessor
to the model class, assign something to it in a model or controller method, and read it back later, perhaps in a view.
Long ago, in the days of Rails 2 and for all I know Rails 1, developers sometimes smuggled extra information into views through an ActiveRecord model’s hash interface. At some point using that interface to assign a value to a non-attribute key was deprecated, and these days it raises an error.
Automatic accessors. Yet another reason for extra attributes is to hold non-attribute information returned from the same ActiveRecord query that retrieves a model. When query results include a non-attribute field, ActiveRecord gives each result object a read accessor with the name of the field which returns the value of the field for that object.
Here’s an example:
airports = Airport. joins(:flights). group(airport: :id). select('airport.*, count(*) flight_count') airports.first.flight_count => 17 # or whatever
I did say ‘accessor’ rather than ‘reader’, and indeed a writer method is defined as well so you can assign an different value to the field if you need to.
I call this feature ‘automatic accessors’ partly because ActiveRecord creates the accessor without you having to write any code except for the query, partly because they really are accessors, and also as alliteration always attracts attention.
Using different kinds of extra attributes together
Don’t!
That is, a given extra attribute should be just one kind, not more than one kind at once.
Although their opposite names reflect the opposite directions in which they carry information from the UI to the database or vice versa, inbound and outbound virtual attributes aren’t normally used together with the same real attributes. While a real attribute that doesn’t map well to UI may be set through an inbound virtual attribute, it is typically turned back into a displayable form on the fly by a helper or decorator, with no need for an outbound virtual attribute. Conversely, outbound virtual attributes, and automatic accessors too, are generally for aggregated or derived values which would make no sense as input.
As for automatic accessors, they’re incompatible with virtual attributes. If you declare an attr_accessor with a given name, then use that name for an automatic accessor, the reader defined by the attr_accessor will block the automatic accessor:
class Airport attr_accessor :flight_count end airports = Airport. joins(:flights). group(airport: :id). select('airport.*, count(*) flight_count') airports.first.flight_count => nil
Since automatic accessors don’t need to be defined like virtual attributes do, it’s easy to unknowingly define a virtual attribute with the same name as an automatic accessor used in queries for the same model type. This can be hard to debug, especially for someone who’s not familiar with how they interact. Choosing clear, specific names for virtual attributes and automatic accessors might make collisions less likely, and is a good idea anyway. But testing is even better.
Testing extra attributes
Unit tests
Unit-testing inbound virtual attributes is easy: just call the virtual writer and check the real readers. Outbound virtual attributes probably don’t need unit tests at all: their trivial implementations are probably fully tested by integration tests. Also, neither kind of virtual attribute is complicated enough to need to be stubbed or mocked out of unit tests of other classes.
Testing automatic accessors, however, is harder. An automatic accessor is not defined on the model class, but on individual instances returned from queries that return non-attribute fields. An instance you create for testing wasn’t returned from a query with the non-attribute field, so it doesn’t have the automatic accessor and you can’t assign to it, directly or in a factory_girl factory. If you’re using RSpec, and have it configured to verify partial doubles (that is, to not let you stub nonexistent methods), which you certainly should, you can’t stub the automatic accessor either.
You might think that the fix is easy: just add an attr_accessor to the class. But, as we just learned, real accessors block automatic accessors, so, while adding a real accessor might fix your controller specs, it will break your model specs and integration tests that actually require the automatic accessor to work. That fix would also be structurally incorrect, since it would add the accessor to all instances of the model class, not just those that should have them. The first workaround suggested in the RSpec documentation has the same problems: adding a dummy method with the same name as an automatic accessor breaks the automatic accessor and affects every model instance. (The other suggestion in the RSpec documentation isn’t a workaround itself but a hook that lets you do some unspecified workaround of your choice before a partial double is verified.)
You might run a fake query to create instances with automatic accessors and then set them to the values you want, but that would mean creating objects in the test database to query against, which would be complicated and slow. If you prevent tests other than model tests from using the database (which I also strongly recommend), it would be impossible.
But this is Ruby, so there’s always a way. The simplest way I’ve found to stub an automatic accessor is just to define a singleton method on the test instance. It also feels correct, since that’s what ActiveRecord does. I create my test model instances with factory_girl, so the place to define the singleton method is in the factory definition:
FactoryGirl.define do factory :airport do # set any real attributes and/or associations that should be set in the factory transient do flight_count nil end # :stub because build_stubbed is normally the right kind of instance to use # in a spec that stubs out model methods after(:stub) do |airport, evaluator| if evaluator.flight_count airport.define_singleton_method(:flight_count) do evaluator.flight_count end end end end end
Then, in a test, say
airport = create :airport, flight_count: 17
to create an instance with the automatic accessor. If a value isn’t specified for the automatic accessor it won’t be present at all.
Note that we’re ignoring respond_to?
here. ActiveRecord incorrectly overrides respond_to?
for automatic attributes instead of overriding respond_to_missing?
as it should, and propagating that seems wrong. Just ignoring it hasn’t caused a problem for me yet.
The factory code would add up quickly if you have many automatic attributes, but it’s easy to extract them into reusable traits and even, with a few uses of send
, create all such traits in a single block of code.
Integration tests
Virtual attributes don’t pose any special problems for integration tests. Automatic accessors, however, need integration testing even more than ordinary methods, for two reasons:
Automatic accessors are prone to integration errors. Automatic accessors must be stubbed in test code in a way independent of how they’re defined in production code. It is, therefore, very easy to stub them incorrectly, i.e. to set them up differently than ActiveRecord sets them up in production, meaning that unit tests can miss integration errors.
Automatic accessors are easy to break. Defining a virtual attribute, or any method, on the same class and with the same name as an existing automatic accessor will break the automatic accessor in production, but won’t break unit tests.
The solution in both cases is to ensure that each automatic accessor is covered by an integration test that will fail if there is an integration error or if the automatic accessor breaks.
Leave a Reply