ruby 安全导航操作符的使用是否应该在两个场景下进行单元测试(对象exist +对象nil)?

t40tm48m  于 2023-04-29  发布在  Ruby
关注(0)|答案(2)|浏览(82)

考虑以下代码:

class Child
  belongs_to :some_parent, optional: true
  def status
    some_parent&.calculate_status
  end
end

这是否足够简单,以至于不为每个场景编写单元测试,或者编写诸如以下场景更好:

describe '#status' do 
  context 'when some_parent does not exist' do
    ## assume let/before block exist to properly set this up
    it 'returns nil' do
      expect(subject.status).to_be nil
    end
  end 

  context 'when some_parent exists' do
    ## assume let/before block exist to properly set this up
    it 'returns the value from some_parent' do
      expect(subject.status).to eql('expected status')
    end
  end
end
xxb16uws

xxb16uws1#

这不一定是一个事实的答案。您需要多少代码覆盖率取决于您。但是,如果我们将您的问题重新定义为 * 当不测试nil状态时,代码覆盖率是否更低?* 那么答案是,因为它们是两个不同的分支,每个分支都必须单独评估。

some_parentnil时:

some_parent&.calculate_status
#          ^ short circuits here and we branch to nil

some_parent存在时:

some_parent&.calculate_status
#           ^ method evaluation occurs and we branch to the method

验证这一点的一个简单方法是使用simplecov进行测试。下面是一个示例应用程序,可以验证它:

# Gemfile

source 'https://rubygems.org'

gem 'rspec'
gem 'simplecov'
gem 'simplecov-console'
# app.rb

# We create the equivalent of your ++belongs_to++ call
# with these classes
class Parent
  def calculate_status
    "in parent"
  end
end

class Child
  attr_reader :some_parent

  def initialize(parent = nil)
    @some_parent = parent
  end

  def status
    @some_parent&.calculate_status
  end
end
# app_spec.rb

require "simplecov"
require "simplecov-console"

SimpleCov.start { enable_coverage :branch }
SimpleCov.formatter = SimpleCov::Formatter::Console

require_relative "app"

RSpec.describe "#status" do
  context "when some_parent does not exist" do
    it "returns nil" do
      expect(Child.new.status).to be nil
    end
  end

  context "when some_parent exists" do
    it "returns the value from some_parent" do
      expect(Child.new(Parent.new).status).to eq("in parent")
    end
  end
end

然后我们运行测试:

bundle exec rspec app_spec.rb
..

Finished in 0.00401 seconds (files took 0.09368 seconds to load)
2 examples, 0 failures

COVERAGE: 100.00% -- 9/9 lines in 1 files
BRANCH COVERAGE: 100.00% -- 2/2 branches in 1 branches

我们已经测试了这两个分支,并有100%的覆盖率。如果我们告诉它只运行两个规范中的一个,我们会得到不同的结果:

bundle exec rspec app_spec.rb:14
Run options: include {:locations=>{"./app_spec.rb"=>[14]}}
.

Finished in 0.00141 seconds (files took 0.08878 seconds to load)
1 example, 0 failures

COVERAGE:  88.89% -- 8/9 lines in 1 files
BRANCH COVERAGE:  50.00% -- 1/2 branches in 1 branches

+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
| coverage | file   | lines | missed | missing | branch coverage | branches | branches missed | branches missing |
+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
|  88.89%  | app.rb | 9     | 1      | 3       |  50.00%         | 2        | 1               | 15[then]         |
+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+

另一个spec:

bundle exec rspec app_spec.rb:20
Run options: include {:locations=>{"./app_spec.rb"=>[20]}}
.

Finished in 0.00128 seconds (files took 0.08927 seconds to load)
1 example, 0 failures

COVERAGE: 100.00% -- 9/9 lines in 1 files
BRANCH COVERAGE:  50.00% -- 1/2 branches in 1 branches

+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
| coverage | file   | lines | missed | missing | branch coverage | branches | branches missed | branches missing |
+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
| 100.00%  | app.rb | 9     | 0      |         |  50.00%         | 2        | 1               | 15[else]         |
+----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+

因此,如果你想要完整的 * 分支覆盖 * 然后写两个规范。

kyxcudwk

kyxcudwk2#

正如在其他地方提到的,您的团队的代码覆盖率标准由您来确定。你需要理解收益递减的论点和“你希望我不要测试什么”的论点,然后打电话。
然而,你的问题有一个不同的方面值得注意:* 使用安全导航操作符的代码是否比不使用它的代码更复杂?*
安全导航操作符被添加到Ruby中,以减少违反得墨忒耳定律所导致的undefined method 'x' for nil:NilClass (NoMethodError)错误。由于在ActiveRecord中将查询和子句附加在一起的方式,这些冲突在Rails应用程序中很常见。从某种意义上说,操作符可以减少代码。而且,一般来说,代码越少,复杂性越低。但是,让我们看看这个和其他两个方法,并比较复杂性。
如果我们将安全导航与使用&&的旧方法和第三种方法(使用NullObject模式)进行比较:

# safe_navigation.rb

class Parent
  def calculate_status
    "in parent"
  end
end

class Child
  attr_reader :some_parent

  def initialize(parent = nil)
    @parent = parent
  end

  def status_with_safe_navigation
    parent&.calculate_status
  end

  def status_with_conditional
    parent && parent.calculate_status
  end

  def status_with_null_object
    return "" unless parent

    parent.calculate_status
  end
end

我们从flog中得到以下内容:

~/so > flog -am safe_navigation.rb
    12.3: flog total
     3.1: flog/method average

     4.0: Child#status_with_conditional      safe_navigation.rb:22-23
     3.7: Child#status_with_null_object      safe_navigation.rb:26-29
     2.5: Child#status_with_safe_navigation  safe_navigation.rb:18-19
     2.2: Child#initialize                   safe_navigation.rb:14-15

所以,是的,孤立地说,安全导航操作员是最不复杂的解决方案。但是,当您考虑到Child类的客户机现在还必须处理nil时,整个复杂性场景就会发生变化。让我们添加一个ClientOfChild类,看看它的复杂性:
这是一个类:

class ClientOfChild
  def call_status_with_safe_navigation
    result = Client.new.status_with_safe_navigation

    result.nil? ? "" : result
  end

  def call_status_with_conditional
    result = Client.new.status_with_conditional

    result.nil? ? "" : result
  end

  def call_status_with_null_object
    Client.new.status_with_null_object
  end
end

在这段代码中,包含和不包含安全导航操作符的代码都必须处理nil,但基于NullObject的代码不需要。
以下是Flog:

~/so > flog -am safe_navigation.rb
    22.4: flog total
     3.2: flog/method average

     4.0: Child#status_with_conditional                   safe_navigation.rb:22-23
     3.8: ClientOfChild#call_status_with_safe_navigation  safe_navigation.rb:34-37
     3.8: ClientOfChild#call_status_with_conditional      safe_navigation.rb:40-43
     3.7: Child#status_with_null_object                   safe_navigation.rb:26-29
     2.5: Child#status_with_safe_navigation               safe_navigation.rb:18-19
     2.4: ClientOfChild#call_status_with_null_object      safe_navigation.rb:46-47
     2.2: Child#initialize                                safe_navigation.rb:14-15

现在,看看我们得到的每条路径的总flog:

* With safe navigation = 2.5 + 3.8 = 6.3
 * With conditional     = 4.0 + 3.8 = 7.8
 * With NullObject      = 3.7 + 2.4 = 6.1

因为这是对真实的代码的过度简化,所以flog中的差异很小。但是,总的赢家是NullObject模式。你会发现,方法越复杂,这些数字就会增长得越远。
出于这个原因,为了在代码中不违反Demeter定律,我选择了NullObject模式,而不是使用安全导航操作符(ActiveReccord查询除外,因为,嗯。.. Rails)。

相关问题