rspec 测试中变量既为 nil 也非 nil

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

这是我多年编程中见过的最奇怪的错误。请忽略我在下面使用的奇怪习语,因为我正在使用 trailblazer,一个用于 Rails:

的编程框架

下面的代码

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

按预期工作。

下面的代码

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

行中为 nil:NilClass 抛出一个未定义的方法“id”
app = App::Get.(id: app.id).model

但是行

p app.id 

就在违规行显示正确 ID 之前。

可能发生了什么?

这一行:

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

创建一个名为 app 的方法,调用时 return 是一个 App。 (它会记住结果,因此在初始调用后它总是 return 相同 App。)

这一行:

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

创建一个名为 app 新局部变量 ,它与方法 app 不明确。根据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:

因此,当您尝试分配给 app 时,会创建一个新的局部变量。当评估赋值的右侧时,app.id 尝试在局部变量 app(目前为 nil)上调用 id

这是我能想到的最简单的重现问题的代码:

def a
  "Hello"
end

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

有几种方法可以修复您的代码。我认为目前最好的解决方案是:

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

方法已使用名称 app...不要通过创建具有相同名称的局部变量来隐藏它。

你也可以这样做:

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

或您已有的解决方案:

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

但我认为这两者都比不首先创建命名冲突更糟糕。