Closed swrobel closed 8 years ago
OoooooOOooo this is a good one! 😄
The reason your :random_question
fabricator is never random is because the from
array sample is evaluated as an argument to the fabricator definition. In other words, when this file is initially processed by Ruby it picks one from the list and that's the one used by the whole process.
I think in this case you'll actually want to make a helper method. Instead of the :random_question
fabricator, try this:
def random_question(attributes={})
Fabricate(%i(text_question single_select_question multi_select_question).sample, attributes)
end
Then somewhere in your tests you'll just say...
let(:question) { random_question(label: 'What... is your quest?') }
If you'd like to add to or clarify the docs PRs are always welcome. They are in a separate repo though.
Haha, glad you enjoyed that. I thought you might. I actually tried that but couldn't figure out the right place to define the helper so that it would be available in all of my fabricators. Suggestions?
I'm also wondering if the following will set the association properly:
Fabricator(:template)
questions { 10.times { random_question() } }
end
Ah, so when you're inside a definition block it is being evaluated in the context of a BasicObject so many of the things you're used to being present are not. This is so nothing that would normally be in an object steps on the toes of attributes in your models. I think the easiest thing in your case is to just monkey patch it into the Fabricate module.
def Fabricate.random_question
...
end
Fabricator(:template)
questions { 10.times { Fabricate.random_question } }
end
Almost there...
def Fabricate.random_question(attributes = {})
Fabricate(%i(section_heading text_question select_question).sample, attributes)
end
Fabricator(:question) do
template
end
Fabricator(:template) do
questions { 10.times { Fabricate.random_question() } }
end
Now when I try to Fabricate(:template)
I'm getting SystemStackError: stack level too deep
probably resulting from the question fabricator trying to fabricate a template and vice versa on to infinity...
Well you set up an infinite loop in your fabricators. You can avoid this by overriding the template in the question in the template. Setting it to nil
will allow ActiveRecord to properly associate them when it persists everything.
Fabricator(:template) do
questions { 10.times { Fabricate.random_question(template: nil) } }
end
Thanks for sticking with me on this. I finally got it working, but I'm hesitant to update the docs with a hack such as this.
# config/initializers/random_question_fabricator.rb
if defined? Fabricate
class Fabricate
class << self
def build_random_question(attributes = {})
Fabricate.build(%i(text_question single_select_question multi_select_question).sample, attributes)
end
def random_question(_attributes = {})
build_random_question.tap(&:save)
end
end
end
end
# test/fabricators/template_fabricator.rb
Fabricator(:template) do
# Apparently 10.times.map works, 10.times does not
# but rubocop insists 10.times.map should be written Array.new(10)
questions { Array.new(10) { Fabricate.build_random_question } }
end
# In tests (using Test::Unit on Rails 4.2 but I imagine this will work w/ rspec)
Fabricate.random_question
# or
Fabricate.build_random_question
Anyway, my discoveries in experimentation were:
Fabricate.random_question
at the top of the question_fabricator.rb
would result in Fabricate.random_question
working when called from other fabricators, but returning undefined method when called from tests, either as random_question
or Fabricate.random_question
. I think this is a load-order thing (ugh, my least-favorite category of issue), and hence I moved the definition to an initializer.build
rather than persist the fabricated children. This seems like a good candidate for updating the docs.Any thoughts on modifying from:
to accept a block? I could attempt a PR.
I don't think adding this case to the docs is worth doing. If you find anything in the docs that is confusing or out of date I would appreciate at least an issue pointing it out though.
Definitely a load order thing. You'll likely need to put it in your spec_helper.rb
or equivalent.
Fabrication will choose to build automatically in certain circumstances. It makes the best decisions it can for you but at the end of the day you need to really understand how ActiveRecord works and persists object trees for more advanced use cases.
So what you're doing here is a little unusual. I don't think modifying from to accept a block is a good idea. I'd actually rather reduce the acceptable values for that parameter to just a string representation of the class name.
I have several STI models where I'd like to be able to randomly fabricate one of them. Here's a simplified example:
The part that doesn't work is the dynamic from, which seems to be evaluated at load-time, and not call-time. I've tried wrapping it in a block and a proc, to no avail. Any helpful suggestions?
If possible, I'd like to submit a documentation PR, but I've tried everything I can think of.