Rails 5 Test Prescriptions 第7章 double stub mock

 

https://relishapp.com/rspec/rspec-mocks/v/3-7/docs/basics/test-doubles 

 

 

      你有一个问题,如果想为程序添加一个信用卡程序用于自己挣钱。测试信用卡函数很难。你不像在测试时,真来一个信用卡购买。而且网络远程调用很慢。

      或者你有另一个问题。你想要使用模块设计来开发代码。你想要你的测试尽可能的和其他相关的部分的代码隔离开。 比如你有一个业务逻辑会调用一个model,但是你的测试不想用这个model,你想要你的workflow test有效,但和这个model无关,假定这个model不存在或坏掉。

      解决以上问题的方案是a test double.这是一个fake object,在自动化测试中用于取代一个真实的object。所谓假fake,就是这个对象不是一个真实的implementation工具。但是它可以取代那些已装罐的values,并可以返回对指定信息的回复。

    Test double也有小issue,作者会给出解释,怎么用自己决定。

    开始讲运作方式,(使用rspec-mocks library),然后讨论不同的方法使用来解决上面的2个问题,最后在12章Minitest,会讲解相关用法。

 


 

 

  • Test doubles defined✅
  • creating Stubs
  • Mock Expectations
  • Using Mocks to simulate database Failure
  • Using Mocks to Specify behavior
  • More expectation annotations注释简单看了
  • Mock Tips(窍门,a useful piece of advise)  简单看了

 

Test Doubles Defined

 

a fake是一个典型的Ruby对象,被专门设计用于测试的。

A stub是一个fake,针对一个方法调用返回预先决定的值,而不是在一个实际的对象调用实际的方法。

使用double方法创建a stub 桩件。或者创建一个partial stub通过使用特殊的方法在一个已存在的对象中:

allow(thing).to receive(:name).and_return("Fred")

解释:调用thing.name,得到Fred作为结果

重点是,thing.name方法默认是没有touched(我的理解是被建立),所以无论真实方法会返回什么值都无关重要。Fred作为回复来自于stub,而stub不是真实的对象。

 

A mock 驭件:stub类似,但是还会返回the fake value。一个mock对象建立一个可测试的期待。如果这个方法没有被调用,mock会发出测试失败信息。用expect取代allow

expect(thing).to  recceive(:name).and_return( "Fred"

使用mock,然后调用thing.name, 仍会得到Fred并且thing.name方法也untouched.

区别:

但是在测试中,如果你不调用thing.name,测试会失败并提供一个error信息。

 

 


 

 

Creating Stubs 

A stub用于取代一个对象的全部或部分,防止一个普通的方法调用发生,作为替代当stub被创建时返回一个预制的value。 

RSpec,有2个类型的stubs。full doubles, partical doubles.

 

a partical double 在什么情况下使用:创建一个对象但有几个方法想要bypass。

a full double使用范围:当测试时,你的代码和a specific API一起工作,而不是一个对象的时候。 

 

 

Full Doubles (mock/stub)

 

创建一个假的对象和其假方法。 

下面是一个愚蠢的expect()例子,只用与说明double的定义。 

it "can create doubles" do

  twin = double(first_name: "Paul", weight: 100)

  expect(twin.first_name).to eq("Paul")

end 

 

double(参数1,参数2)  

#参数1 是一个可选的string,作为名字,参数2是key/value pairs 代表传送给double的信息。上一个double没有名字。

allow and expect方法: 

twin = double(first_name: "Paul", weight: 100) 等同于👇

twin= double

allow(twin).to receive(first_name).and_return("Paul") 

allow(twin).to receive(weight).and_return(100) 


RSpec doubles是strict,如果调用stub中不存在的方法(hash参数),RSpec会返回error 

如:

     Failure/Error: expect(mocky.name111).to eq "paul"
       #<Double "Mock"> received unexpected message :name111 with (no args)

 

as_null_object方法:当没有方法在hash参数中时,返回double本身。 

spy(name)作为可选方法代替double(name).as_null_object 

如以下,定义时mocky = spy("Mock"):

      Failure/Error: expect(mocky.nam1e).to eq "paul"

expected: "paul" got: #<Double "Mock">

(compared using ==)

Diff: @@ -1,2 +1,2 @@

-"paul"

+#<Double "Mock"> 

 

A verifying double 

一个问题是,当一个真实对象的方法名字改变时,a double 不会知道这个改变,因此就没法模拟真实的代码了。所以RSpec提供了a verifying double作为原则。

instance_twin = instance_double(User) #只能使用User类中的实例方法,否则error 

class_twin = class_double(User)      #只能用类方法

object_twin = object_double(User.new)   #和instance_double类似,额外可以用method_missing定义的实例方法。

这样double 保证了传入的参数是验证过的。 

 

如果你使用string作为double的名字,而这个常量不存在,RSpec会把这个double当作非verifying double。 

上面三个方法都有spy格式 :

 

instance_spy, class_spy, and object_ spy 

 

Partial mock (Stubs / mock)

take a real object and stub out only the methods you need.

创建一个真实的实例对象,然后加一些假的方法。这时不需要使用double方法。

  it "stubs an object", :stubs do
    project = Project.new(name: "Project Greenlight")
    allow(project).to receive(:name)
    expect(project.name).to be_nil
  end

第三行建立stub,第四行拦截intercept project.name调用并返回nil。 

 

以上必须设置才能生效,现在都是默认设置为true。如果使用虚假的方法,会报告错误。 

  config.mock_with :rspec do |mocks|

 

    # Prevents you from mocking or stubbing a method that does not exist on
    # a real object. This is generally recommended, and will default to
    # `true` in RSpec 4.
    mocks.verify_partial_doubles = true
  end

 

如果改成allow(project).to receive(:name).and_return("Chen") 

方法name返回值Chen。and_return方法是一个annotaion信息,把返回值和方法关联起来。

 

class在Ruby中也是对象,自然也可以使用stub。

  it "stubs the class" do
    allow(Project).to receive(:find).and_return(
      Project.new(name:"Project Greenlight")
    )
    project = Project.find(1)
    expect(project.name).to eq "Project Greenlight"
  end

因为使用数据库会让测试变得非常慢,测试使用test doubles to stub database access 是一个回避数据库来加速测试的策略 。

但是记住,stub不应用于测试find方法是否工作;应当用在其他测试需要find方法辅助测试逻辑。比如可以用于保证一个控制器正在存取数据库获得一个实例是可控制的。 

 

allow_any_instance_of

用于从一个class创建多个partial stubs ⚠️文档提示,尽量避免使用。

 

and_raise() 

模仿失败的方法。

 

 


 

Mock Expectations 

总结上面⬆️,method:

double("name"), syp("name")

allow().to receive().and_return()

expect().to receive().and_return()

instance_double, object_double, class_double()

instance_spy, object_spy,  class_spy

and_raise()

 

概念:

mock, stub。区别在于声明一个key/value后,是否必须调用,mock必须调用否则error。

full/ partial double。区别在于是否借助一个真实的类的实例对象。

double, spy。 区别在于如果调用一个未声明的方法,报错的结果,spy只返回double本身,

⚠️ 这个概念,用途暂时不理解。

 

⚠️ mock,默认定义的方法必须调用且只能调用一次,也可以指定调用几次。

如:

proj = Project.new

expect(proj).to receive(:name).twice.and_return(...)

expect(proj).to receive(:name).at_least(:once

expect(proj).to receive( :name ).at_most( :twice )      #at_most(n).times

expect(proj).not_to receive( :name )    #保证某个特别的方法不被调用


 

Using Mocks to Simulate Database Failure 

使用test doubles模仿数据失败。另外尽量不要用在集成测试模仿失败。 

 

  describe "mocking a failure" do

 

 

    it "fails when we say it fails" do
 1     project = instance_spy(Project, save: false)
 2     allow(Project).to receive(:new).and_return(project)
      creator = CreatesProject.new(name: "Name", task_string: "Task")
      creator.create
      expect(creator).not_to be_a_success
    end
  end

 使用given, when, then.分析。

given:给了2行数据。1行是建立了一个double project。2行是建立了一个stub。

when: CreatesProject创建新对象,然后create方法调用build方法,此时使用了full stub,替代了build方法内的数据。在project.save时,使用double已经定义save方法的值false. 这时是模仿了保存失败的情况。

then:  期待结果自然是没有保存成功。

 

 


Using Mocks to Specify Behavior 

除了取代昂贵的方法调用(占用较多的测试时间)。test doubles可以在测试过程进行中测试某个行为。 

作者认为使用test doubles仍然是一个潜在的控制器中的单元逻辑测试的验证方法。

 

controller的责任:交通警察,一边设置特定的值来满足视图,另一边调用系统中其他部分来查询数据。

 

Prescription:

一个桩方法返回一个桩,通常是没问题的。 一个桩方法返回一个桩,而这个桩本身还包含一个桩,就不好了,这意味着你的代码过于依靠其他对象了。 


一个控制器测试:

    it "calls the workflow with parameters" do
      workflow = instance_spy(CreatesProject, success?: true)
      allow(CreatesProject).to receive(:new).and_return(workflow)
      post :create,
        params: {project: {name: "Runway", tasks: "start something:2"}}
      expect(CreatesProject).to have_received(:new)
        .with(name: "Runway", task_string: "start something:2")
    end

More Expectation Annotations 

RSpec 允许大量不同的注释给expectation part of 声明一个test double,可以指定更多的返回值,或者大量stubbed medthod的参数。 

and_return(1,2,3)  #可以返回多个值。

 

⚠️:没案例,没懂。allow(project).to receive(:method).and_yield("arg")

 

你可以传递一个block 来作为接收。这个块被作为这个方法的值来执行。

first_name = "Noel"
last_name = "Rappin"
allow(project).to receive( :name ) { first_name + last_name } 

 

 

 


Mocks with Arguments 

 

 with 方法。用于接受参数。

 

Prescription:

Don't mock what you don't own. 

使用test doubles仅仅用来代替那些本来在你的程序中的方法。 别使用第三方的框架的方法。

因为第三方框架可能会更新变化,还有你在重构代码的时候会导致变化过于复杂。

 

TDD开发,不要过度使用mock. 

 


receive方法有 一个相关写法have_received().with()方法。这个是rspec.mock中的方法。

 

 


 

 

 

 

 

       

posted @ 2018-05-28 17:58  Mr-chen  阅读(364)  评论(0编辑  收藏  举报