代码之家  ›  专栏  ›  技术社区  ›  PressingOnAlways

是否应对安全导航操作员进行单元测试?

  •  0
  • PressingOnAlways  · 技术社区  · 3 年前

    考虑以下代码:

    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
    
    0 回复  |  直到 3 年前
        1
  •  2
  •   anothermh ScarletMerlin    3 年前

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

    什么时候 some_parent 是 无 以下为:

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

    什么时候 某些租金 存在:

    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]         |
    +----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
    

    或其他规格:

    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]         |
    +----------+--------+-------+--------+---------+-----------------+----------+-----------------+------------------+
    

    所以如果你想吃饱 分支机构覆盖范围 然后写两个规格。

        2
  •  2
  •   aridlehoover    3 年前

    如前所述,您的团队的代码覆盖率标准由您决定。你需要理解收益递减的论点和“你希望我不测试什么”的论点,然后打电话。

    然而,你的问题还有一个不同的方面值得注意: 使用安全导航运算符的代码是否比不使用它的代码更复杂?

    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
    

    在该代码中,带有和不带有安全导航操作员的代码必须处理 无 ,但是 虚拟对象 基于代码则不然。

    下面是鞭笞:

    ~/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中的差异很小。但是,总的赢家是 虚拟对象 图案你会发现,方法越复杂,这些数字就会增长得越远。

    出于这个原因,为了不违反代码中的德米特定律,我选择 虚拟对象 模式,而不是使用安全导航操作符(除了在ActiveReccord查询中,因为,嗯…Rails)。

    推荐文章