Mysterious syntax
If you’ve used Factory Bot at all, you’ve seen syntax like this:
FactoryBot.define do
factory :user do
first_name { 'John' }
last_name { 'Smith' }
email { 'john.smith@example.com' }
end
end
When I was first getting started with Rails and I wasn’t very familiar with Ruby, I would look at these files and understand what’s going on conceptually but I would have no idea what’s going on syntactically.
Why I didn’t understand this code
There were three barriers to my understanding at that early stage of my Ruby experience:
- Ruby’s optional parentheses feature, although it can help code be very expressive, can also make it hard for beginners to tell what’s what.
- I didn’t understand Ruby blocks yet.
- I didn’t know about dynamically-defined methods.
Let’s look at a modified example that will make some of the syntax clearer.
A modified factory definition example
Below is a version of the above factory definition that’s functionally equivalent but with two syntactical changes.
The first change is that I’ve included parentheses for all method calls. The second is that I’ve changed all block usages to do
syntax instead of the shorthand {}
syntax.
FactoryBot.define() do # define is a method
factory(:user) do
first_name do
'John'
end
last_name do
'Smith'
end
email do
'john.smith@example.com'
end
end
end
You can see here that define
and factory
are each just methods. Each of the two methods takes a block. (If you’re not very comfortable with blocks yet, check out my post on understanding Ruby blocks.)
first_name
, last_name
and email
are also methods that take blocks, speaking loosely. Before we can talk about those we need to talk about methods versus messages.
Methods and messages
When you call a method on an object, it can be said that you’re sending a message to that object. For example, when you call "5".to_i
, you’re sending the message to_i
to the object "5"
, which is of course a String
.
In the above case, the message to_i
also happens to be a method that’s defined on String
. This doesn’t need to be the case though. We could send any message at all to String
, String
just might not necessarily respond to that particular message. That’s why we have the respond_to?
method, to see what messages an object responds to.
The messages an object will respond to need not be limited to the methods that are defined on that object. An object author can use the method_missing method to allow an object to respond to any message that’s sent to it, and respond in any way that the object author chooses.
Factory definitions and messages
What’s likely happening with first_name
, last_name
and email
is that Factory Bot is using method_missing
to allow arbitrary messages to be sent, and if the message (e.g. the message of first_name
) matches an attribute on the model that the factory is for, then Factory Bot uses the block passed with the message to set the value of that attribute.
Takeaways
- Factory Bot’s factory definitions are made out of methods and blocks.
- Adding parentheses to any piece of DSL code can often make the code clearer.
- Ruby objects can be passed arbitrary messages, and objects can be designed to respond to those messages.
> Adding parentheses to any piece of DSL code can often make the code clearer.
Absolutely. Since that other post/tweet where you showed how using parentheses make it more clear, I’ve started using them and it feels way better. Less “magical”.