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
另外看到一個不錯的 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
```
這樣就可以用
```
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
```
訂閱:
文章 (Atom)