Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
February 2, 2021 01:06 pm GMT

When not to use instance variables in RSpec

Using RSpec there is some confusion about the differences between let, let!, and instance variables in specs. I'd like to focus on how instance variables work in RSpec in combination with before :context blocks, and in what kind of scenarios you should and should not use them.

Why use instance variables?

The advantage of declaring instance variables in before :context (or before :all) blocks is that whatever value is assigned is only queried or calculated once for many specs. The before :context block is only executed once for all specs in that context. Those specs can use the instance variable without repeating the same setup for every spec, which should speed up the test suite.

Reading the above it might be tempting to put a lot of spec setup in before :context blocks. A fast test suite creates happy developers, right? But there's a downside to using instance variables in RSpec, which could make for very unhappy developers.

From the RSpec docs:

It is very tempting to use before(:context) to speed things up, but we recommend that you avoid this as there are a number of gotchas, as well as things that simply don't work.

[...]

Instance variables declared in before(:context) are shared across all the examples in the group. This means that each example can change the state of a shared object, resulting in an ordering dependency that can make it difficult to reason about failures.

The RSpec docs give us a warning about changing values of the instance variable. State can leak between specs using instance variables defined in a before :context block this way.

Let's look at some examples of specs using instance variables and in what scenarios in will break.

Specs sharing instance variables

In the example below specs only assert if the value of the instance variable matches the expected value. Since the instance variable is not changed, all the specs will pass. If the instance variable value took a long time to query or calculate we have saved that time for two specs in this file.

# spec/lib/example_1_spec.rbdescribe "Example 1" do  before :context do    # Imagine this being a complex value to prepare    # This block is only run once in the `describe "Example 1"` block    @my_instance_variable = :my_value  end  it "spec 1" do    expect(@my_instance_variable).to eql(:my_value)  end  it "spec 2" do    expect(@my_instance_variable).to eql(:my_value)  endend
Enter fullscreen mode Exit fullscreen mode
$ bundle exec rspec spec/lib/example_1_spec.rb --order definedFinished in 0.00223 seconds2 examples, 0 failures
Enter fullscreen mode Exit fullscreen mode

(I'm using --order defined in the examples in this post so that the spec execution order is predictable and reproducible.)

But specs can be more complicated than this. They may pass the instance variable to some other part of the app, which modifies the given value. This is where things go wrong, what the RSpec docs warn us about.

Reassigning the instance variable

If changing the instance variable is the problem, let's reassign it and see what happens in other specs.

In the example below the "spec 1" spec changes the instance variable to test a slightly different scenario.

# spec/lib/example_2_spec.rbdescribe "Example 2" do  before :context do    # Imagine this being a complex value to prepare    @my_instance_variable = :my_value  end  it "spec 1" do    @my_instance_variable = :new_value    expect(@my_instance_variable).to eql(:new_value)  end  it "spec 2" do    expect(@my_instance_variable).to eql(:my_value)  endend
Enter fullscreen mode Exit fullscreen mode
$ bundle exec rspec spec/lib/example_2_spec.rb --order definedFinished in 0.00223 seconds2 examples, 0 failures
Enter fullscreen mode Exit fullscreen mode

In this example "spec 2" does not fail, even though "spec 1"which runs before "spec 2"changes the instance variable. There was no need for us to reset the original value of the instance variable at the end of the spec even though we changed it.

RSpec is helping here by restoring the original instance variable, set in the before :context block, before every spec starts. This allows the next spec to the originally set value.

This instance variable restore functionality by RSpec doesn't always quite work though. Let's see what happens if we use a bit more complex value. That way we know the limitations of using instance variables in RSpec.

Modifying instance variable values

In the next example the @my_instance_variable is assigned a more complex value: an array with multiple values. We will intentionally break the spec in this scenario.

In "spec 1" we're testing a slightly different scenario again, modifying the value before running the assertion. Instead of reassigning the variable we're adding a value to the array on @my_instance_variable.

# spec/lib/example_3_spec.rbdescribe "Example 3" do  before :context do    @my_instance_variable = [:one, :two]  end  it "spec 1" do    @my_instance_variable << :three    expect(@my_instance_variable).to eql([:one, :two, :three])  end  it "spec 2" do    expect(@my_instance_variable).to eql([:one, :two])  endend
Enter fullscreen mode Exit fullscreen mode
$ bundle exec rspec spec/lib/example_3_spec.rbFailures:  1) Example 3 spec 2     Failure/Error: expect(@my_instance_variable).to eql([:one, :two])       expected: [:one, :two]            got: [:one, :two, :three]       (compared using eql?)       Diff:       @@ -1 +1 @@       -[:one, :two]       +[:one, :two, :three]     # ./spec/lib/example_3_spec.rb:14:in `block (2 levels) in <top (required)>'Finished in 0.01596 seconds (files took 0.10576 seconds to load)2 examples, 1 failureFailed examples:rspec ./spec/lib/example_3_spec.rb:12 # Example 3 spec 2
Enter fullscreen mode Exit fullscreen mode

Unlike before, the "spec 2" spec has now failed. It fails because the instance variable still has the value from the first spec. State has leaked from "spec 1" into "spec 2". RSpec wasn't able to restore the value of the instance variable between specs. Let's look at how RSpec "restores" these instance variables to see what breaks it.

How instance variables work in RSpec

To recap what we learned from the examples earlier:

  • Assigning instance variables in before :context means they'll only be assigned once, as the before :context block is only once run before all specs in the spec context.
  • Before every spec starts, RSpec restores the values of instance variables set in before :context.
  • This restore works for basic Ruby objects such as Symbols, numbers, etc.
  • This restore breaks for more complex objects such as Arrays, Strings, Class instances, etc.

Let's take a closer look at how RSpec restores instance variables before specs to see how this could break in our test suite.

How RSpec restore works

When RSpec runs a spec context, it first runs the before :context blocks. After a before :context block is executed RSpec then stores the list of the instance variables that were set on the spec class. It will use this list of variables and their values to restore from later.

Before a spec is run, RSpec restores the instance variables to their original values from after the before :context block was executed. RSpec reassigns the original value back to the instance variable. This way we don't have to watch out for this ourselves.

Let's look at how this works using the same scenario from the second example, where we reassigned the instance variables.

# Example how RSpec restores instance variables@var = 1 # Set instance variable in `before :context`# After `before :context` blockoriginal_var = @var # Keep original value of instance variable# During spec@var = 2 # Reassign instance variable value# Before the next spec@var = original_var # "Restore" original value of instance variableputs "original_var:", original_var.inspectputs "@var:", @var.inspect
Enter fullscreen mode Exit fullscreen mode

(Simplified example for demonstration purposes.)

original_var:1@var:1
Enter fullscreen mode Exit fullscreen mode

RSpec restores the original value of the instance variable here, by reassigning back the original value.

When "restoring" the instance variables RSpec goes back to its list of original instance variable values and reassigns the instance variables. When the instance variable value was modified however, the restored value is also modified, as it is still the same value.

# Example how RSpec restores complex instance variable values@var = [1, 2] # Set instance variable in `before :context`# After `before :context` blockoriginal_var = @var # Keep original value of instance variable# During spec@var << 3 # Modify instance variable value# Before the next spec@var = original_var # "Restore" original value of instance variableputs "original_var:", original_var.inspectputs "@var:", @var.inspect
Enter fullscreen mode Exit fullscreen mode
original_var:[1, 2, 3]@var:[1, 2, 3]
Enter fullscreen mode Exit fullscreen mode

In this example we can see the instance variable's value has changed, because the value was modified, rather than the instance variable being reassigned. This causes the modified state to leak into other specs.

Pointers

What happens is, RSpec (or rather Ruby) is "restoring" pointers to the values in memory. Variables in Ruby are pointers to a place in the application's memory. Assigning a value to another variable does not make a copy of it, but points to the same location in memory.

When RSpec restores the instance variables, it doesn't restore a copy of the original Array value. Instead it restores the pointer to the Array value in memory. If the Array in memory has changed during the spec run, it will restore not the original value, but the modified Array instead. This is part of how Ruby works, this is not something RSpec can "fix". And which is why the RSpec docs warn us about using instance variables in before :context blocks.

Alternatives

To prevent state from leaking into other specs by modified values, it's possible to "freeze" objects in Ruby. If we freeze an Array, String or other object instance, Ruby will not allow any modifications.

var = [1, 2].freezevar << 3# Raises an error to prevent modification# => FrozenError (can't modify frozen Array: [1, 2])
Enter fullscreen mode Exit fullscreen mode

But this will be more difficult to do for larger objects with nested objects, as it only freezes the top object and not all nested objects.

var = [[1, 2], [4, 5]].freezevar[0] << 3puts var.inspect# => [[1, 2, 3], [4, 5]] # The nested value was modified
Enter fullscreen mode Exit fullscreen mode

Alternatively it's possible to deep clone or dup the object. The problem with this is that it will take up a lot more memory, as every object will be kept in memory multiple times, so I can't recommend it.

Conclusion

RSpec restoring instance variable values between specs is a great help from RSpec for basic Ruby objects, but this behavior shouldn't be relied upon for more complex Ruby objects such as Arrays, Strings, and other object instances.

If a spec is modifying an instance variable value, you can't be sure what the value of that instance variable will be in the next spec. State may leak to other specs, breaking them in unexpected ways. This will be especially difficult to track down when the specs are run in a random order each time.

Make sure that if you use instance variables, you absolutely do not modify any value set on the instance variable if you want a predictable and reproducible test suite. And that's something we should all want. I'm all for fast test suites, but what I like more is a stable test suite.

A big thanks to Benoit Tigeot for fact checking this article!


Original Link: https://dev.to/tombruijn/when-not-to-use-instance-variables-in-rspec-3jb9

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To