顯示具有 RSpec 標籤的文章。 顯示所有文章
顯示具有 RSpec 標籤的文章。 顯示所有文章

2018年12月8日 星期六

rspec test protocal (interface) - part 2 - define our own DSL for rspec


接續上一篇 https://everyday1percent.blogspot.com/2018/05/rspec-test-protocal-interface.html


現在我們要做一個客製化的 rspec matcher,達到的效果如下:


```ruby
RSpec.describe FooService do

  describe "interfaces" do
     specify do
        expect(FooService).to has_implemented_interfaces :call, :call!
     end
  end

end
```

可以先參考 rspec 的 custom matcher 應該就可以做到了

https://relishapp.com/rspec/rspec-expectations/docs/custom-matchers/define-a-matcher-supporting-block-expectations

如果要做成 gem 的話可以參考 rspec-expectations 開放的 protocal

https://github.com/rspec/rspec-expectations/blob/master/lib/rspec/matchers/matcher_protocol.rb

https://makandracards.com/makandra/662-write-custom-rspec-matchers

https://github.com/kucaahbe/rspec-html-matchers

2018年5月9日 星期三

rspec test protocal (interface) - part 1 - define what's interface and find it


通常我們會希望繼承於 Base class 底下的 class 會有相同的 interface,for example, 每個 service 都要有一個 #call 的 instance method,應該要有個很簡單的方法來對這些 services 測試是否有 implement 這個 protocal,理想情況如下:


```rb
class BaseService
  def call
    raise "NotImplemented"
  end
end
class FooService < BaseService
  def call
    puts "called"
  end
end
class BarService < BaseService
end
module Callable
  def call
    puts "called from module"
  end
end
class IncludeService
  include Callable
end
class PrivateService
  private

  def call
    puts "private call"
  end
end


RSpec.describe FooService do

  describe "interfaces" do
     specify do
        expect(FooService).to has_interface :call
        expect(FooService).to has_implemented_interface :call
     end
  end

end
```

it_has_interface => 有 respond_to 這個 method 就行 (個人覺得沒什麼用...)
it_has_implemented_interface => 有自己 implement (排除母 class) 或是 include method 進來


it_has_implemented_interface 希望達到的效果是:

FooService 和 IncludeService 會 passed
BarService 和 PrivateService 會 failed


來看看要怎麼樣做 :D


已知使用 respond_to? 是不行的,因為母 class 定義了 call,所以

```ruby
2.4.4 :013 > f = FooService.new
 => #<FooService:0x00007fed06152098>
2.4.4 :014 > b = BarService.new
 => #<BarService:0x00007fed06148700>
2.4.4 :015 > f.respond_to?(:call)
 => true
2.4.4 :016 > b.respond_to?(:call)
 => true
```

用 method_defined? 當然也不行,因為都是繼承於 BaseService 所以都有 defined,但來試來試一下

```ruby
2.4.4 :017 > FooService.method_defined?(:call)
 => true
2.4.4 :018 > BarService.method_defined?(:call)
 => true
```

我們要找的是一個方法檢測子 class 是否有定義這個 method,不管是不是用 mixin 的方式加進去的,有幾個 methods 可以用

* public_methods
* instance_methods
* public_instance_methods

instance_methods default 還會包含 protected 的 methods,不如我們先來試試看 public_instance_methods

```ruby
2.4.4 :030 > FooService.public_instance_methods.include?(:call)
 => true
2.4.4 :031 > BarService.public_instance_methods.include?(:call)
 => true
2.4.4 :032 > IncludeService.public_instance_methods.include?(:call)
 => true
2.4.4 :033 > PrivateService.public_instance_methods.include?(:call)
 => false
```

看起來還是不行,讓我們換個方法,用 public_methods(false)


```ruby
2.4.4 :068 > FooService.new.public_methods(false).include?(:call)
 => true
2.4.4 :069 > BarService.new.public_methods(false).include?(:call)
 => false
2.4.4 :070 > IncludeService.new.public_methods(false).include?(:call)
 => false
2.4.4 :071 > PrivateService.new.public_methods(false).include?(:call)
 => false
```

結果 public_methods(false) 沒辦法讓 include 進來的 method 包含在裡面,為什麼呢?可以看一下 IncludeService 的 ancessor

```
2.4.4 :072 > IncludeService.ancestors
 => [IncludeService, Callable, Object, Kernel, BasicObject]
```

可以想像成 include 其實就是在 parent class 以及 child class 中間插隊,所以也是算在 ancestors 裡面。


所以看起來沒有任何 built-in 的 method 可以直接做到這個功能,那不如我們就自幹一個看看吧?XD

由於要 support module 裡的 methods 所以想到最原始的方法就是去找 ancestors 裡在 superclass 之下的每個 instance_methods, code example:

```ruby
class Utils
  class << self
    def get_public_instance_methods_without_parent_class(klass)
      countable_ancestors = klass.ancestors[0...klass.ancestors.index(klass.superclass)]
      countable_ancestors.flat_map do |ancestor_class|
        ancestor_class.public_instance_methods(false)
      end.uniq
    end
  end
end
```

這個版本目前只能對instance method 做檢測,不過對我們來說已經足夠了,以後再想辦法擴充,接下來就是來研究自定義 rspec DSL 的部分。


2018年4月22日 星期日

Rspec aggregate_failures syntax


意思就是在 aggregate_failures block 底下任一個的 expectation 有 error 時會針對每個 expectation 顯示 error 的 message

example

```ruby
    it "returns false if any of balance, locked, saving is negative" do
      negative_balance_account = build(:account, balance: -1)
      negative_locked_account = build(:account, locked: -1)
      negative_saving_account = build(:account, saving: -1)

      aggregate_failures "testing all senarios" do
        expect(negative_balance_account.has_negative_asset?).to be true
        expect(negative_locked_account.has_negative_asset?).to be true
        expect(negative_saving_account.has_negative_asset?).to be true
      end
    end
```

假設這個 test failed 了,那 console 的訊息會是像這樣:

```
  1) Account#has_negative_asset? returns false if any of balance, locked, saving is negative
     Got 2 failures from failure aggregation block "testing all senarios".
     # ./spec/models/account_spec.rb:15:in `block (3 levels) in <top (required)>'

     1.1) Failure/Error: expect(negative_locked_account.has_negative_asset?).to be true

            expected true
                 got false
          # ./spec/models/account_spec.rb:17:in `block (4 levels) in <top (required)>'

     1.2) Failure/Error: expect(negative_saving_account.has_negative_asset?).to be true

            expected true
                 got false
          # ./spec/models/account_spec.rb:18:in `block (4 levels) in <top (required)>'
```

會顯示哪幾個 test failed 了

當有很多 test case 很相近其實可以寫在一個senario 裡時用 aggregate_failures 會讓 test 變得更簡潔,但要小心不要亂用,該分開的 test 還是用 it 分開比較好。


https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures

2018年2月24日 星期六

RSpec best practice



https://github.com/thoughtbot/guides/tree/dee16052f49cb4b41fb8d090ffdb75942512ddb1/best-practices#testing


https://robots.thoughtbot.com/lets-not

看到一個不錯的 mock example (RSpec)

之前一篇文章提到 RSpec 盡量不用 let, 那要用啥?可以用 method。

另外看到一個不錯的 mock example, 用 tap 來 return double 但是 yield Authentictor

```rb
def stub_authenticator(auth_hash)
  double('authenticator').tap do |authenticator|
    Authenticator.stub(:new).with(authenticator).and_return(authenticator)
  end
end

def stub_find_user(attributes = {})
  build_stubbed(:user, attributes).tap do |user|
    User.stub(:find).with(user.to_param).and_return(user)
  end
end
```

https://apidock.com/ruby/Object/tap

from https://thoughtbot.com/upcase/videos/rspec-best-practices

great feature spec example



```rb
feature 'User views all books' do
  scenario 'list all books' do
    create(:book, title: 'Practical Vim')
    create(:book, title: 'The Giver')

    visit books_path

    expect(page).to have_book('Practical Vim')
    expect(page).to have_book('The Giver')
  end

  # ...

  def have_book(title)
    have_selector('.book', text: title)
  end
end

```

2017年12月12日 星期二

RSpec 原來還可以在 factory 下面再用 factory

RSpec 原來還可以在 factory 下面再用 factory

這樣就可以用

```
create(:tiny_btc_account)
```

```
FactoryGirl.define do
  factory :account do
    locked { "0.0".to_d }
    balance { "0.0".to_d }

    trait :usd do
      currency :usd
    end

    trait :btc do
      currency :btc
    end

    trait :small do
      after :create do |account|
        account.plus_funds 10, reason: Account::DEPOSIT
      end
    end

    trait :tiny do
      after :create do |account|
        account.plus_funds 1, reason: Account::DEPOSIT
      end
    end

    factory :tiny_btc_account, traits: [:tiny, :btc]
    factory :tiny_usd_account, traits: [:tiny, :usd]

    factory :small_btc_account, traits: [:small, :btc]
    factory :small_usd_account, traits: [:small, :usd]
  end
end

```