Praveen Angyan
Praveen Angyan

Reputation: 7265

Variable is both nil as well as not nil within rspec test

This is the strangest bug I've seen in my years of programming. Please ignore the strange idioms I am using below as I am using trailblazer, a programming framework for Rails:

The code below

let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }

it 'gets app details from Steam' do
  VCR.use_cassette('app/get') do
    p app.id
    app_id = app.id
    app = App::Get.(id: app_id).model
  end

  expect(app.app_type).to eq('game')
end

works as expected.

The code below

let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }

it 'gets app details from Steam' do
  VCR.use_cassette('app/get') do
    p app.id
    app = App::Get.(id: app.id).model
  end

  expect(app.app_type).to eq('game')
end

throws an undefined method `id' for nil:NilClass in the line

app = App::Get.(id: app.id).model

However the line

p app.id 

which is just before the offending line displays the correct id.

What could possibly be going on?

Upvotes: 0

Views: 364

Answers (1)

user94559
user94559

Reputation: 60143

This line:

let (:app) { App::Create.(app: {name: "Half-Life", steam_id: "70"}).model }

creates a method called app that returns an App when invoked. (It memoizes the result, so it will always return the same App after the initial invocation.)

This line:

app = App::Get.(id: app.id).model

creates a new local variable called app that is ambiguous with the method app. According to the Ruby docs on assignment:

In Ruby local variable names and method names are nearly identical. If you have not assigned to one of these ambiguous names ruby will assume you wish to call a method. Once you have assigned to the name ruby will assume you wish to reference a local variable.

The local variable is created when the parser encounters the assignment, not when the assignment occurs:

So at the moment when you try to assign to app, a new local variable is created. When the right-hand side of the assignment is evaluated, app.id tries to call id on the local variable app (which so far is nil).

Here's the simplest code I can think of that reproduces the issue:

def a
  "Hello"
end

p a.upcase # "HELLO"
a = a.upcase # undefined method `upcase' for nil:NilClass (NoMethodError)

There are a couple ways to fix your code. I think the best solution by far is:

app2 = App::Get.(id: app.id).model

The name app was already used by a method... don't hide it by creating a local variable with the same name.

You could also do this:

app = App::Get.(id: app().id).model # the parens disambiguate

or the solution you already have:

app_id = app.id # capture the ID before the local variable gets created
app = App::Get.(id: app_id).model

But I think both of those are worse than simply not creating the naming collision in the first place.

Upvotes: 1

Related Questions