As you seem to be aware, let
basically just keeps you from evaluating the variable except in examples where you use it. If you use it in ten examples, you will indeed get ten hits to the expensive operation.
So for your first question, I don't know that I can offer a useful answer. It's pretty situational, but I'd say let
isn't viable if you're using the variable a lot and it's an expensive operation. But depending on your needs, it might still be the best option - maybe you have to have the state reset in most examples, but not all. In that case, the expense of the operation might not be worth the pain of trying to share it in just a few cases.
For your second question, I'd say it's probably not a good idea to try and make let
work within a block. That's a case for a before(:all)
block and an instance variable.
Your third question is where the real meat is, I think, so bear with me here.
FactoryGirl isn't really going to change your problem. It will build and optionally save objects, but you still have to decide where and how to use it. If you start popping it into before(:each)
blocks, or calling a builder in most examples, you'll still have performance hits.
Depending on your needs, you could do expensive operations in a before(:all)
block or even a before(:suite)
block (configuring in your spec_helper.rb
, for instance). This has the advantage of giving you fewer hits to the expensive operation, but the downside is that if you're modifying the data, it's modified for all other tests. This can obviously cause a lot of difficult-to-debug problems. If your data needs to be changed by multiple examples, and then reset to a pristine state, you're going to be stuck with some kind of performance hit or else custom logic of your own design.
If your data is primarily in ActiveRecord objects, and you aren't keen on stubbing/mocking to keep from hitting the database, chances are you're stuck with slow tests. Fixtures can be used with transactions to help a bit, and can be faster than factories, but can be a pain to maintain depending on your database schema, relationships, etc. I believe you can use factories in a before(:suite)
block, and then transactions will still work, but that isn't necessarily significantly easier to maintain than fixtures.
If your data is just CPU-expensive objects as opposed to database records, you could set up a bunch of objects and serialize them via the Marshal module. Then you can load them up in a let
block, prebuilt and ready, with just a disk hit (or memory, if you store the Marshalled string in memory):
# In irb or pry or even spec_helper.rb
object = SomeComplexThing.new
object.prepare_it_with_expensive_method_call_fun
Marshal.dump(object) # Store the output of this somewhere
# In some_spec.rb
let(:thing) { Marshal.load(IO.read("serialized_thing")) }
This has the advantage of serializing the object's state in full, and restoring it exactly as it was without re-computing expensive data. This probably won't work as well for really complex objects like an ActiveRecord model, but it can be handy for simpler data structures of your own design. You can even implement your own dumping / loading logic by implementing marshal_dump
and marshal_load
methods (see the Marshal docs I linked above), which can be handy outside of tests.
If your data is simple enough, you may even be able to get away with a setup like this:
# In spec_helper.rb
RSpec.configure do |config|
config.before(:suite) do
@object = SomeComplexThing.new
@object.prepare_it_with_expensive_method_call_fun
end
end
# In a test
let(:thing) { @object.dup }
This isn't necessarily going to work in all cases, as dup
is a shallow copy (see the Ruby docs for more info), but you get the idea - you're building a copy rather than re-computing whatever expensive stuff is hurting you.
I hope this information helps, as I'm not sure I fully understand exactly what you need.