## RSpec or Let's Not
by Wu Chao Chao @ 2022/07/31 COSCUP
---
### [RSpec](https://rspec.info/documentation/) - Intro (1/3)
在 RSpec 中,測試不僅僅是驗證應用的程式碼。測試也是規格,用英文說明 application 應該如何表現。
----
### RSpec - [Better SPECS](https://www.betterspecs.org/) (2/3)
```ruby=
RSpec.describe Calculator do
let(:calculator) { Calculator.new }
describe '#add' do
let(:number) { 1 }
subject { calculator.add(number) }
it { is_expected.to include(number) }
end
describe '#perform' do
subject { calculator.perform }
before { calculator.add(1) }
it { is_expected.to eq(1) }
end
end
```
----
### RSpec - mutate (3/3)
```ruby=
describe User do
let(:cancelled) { false }
subject(:user) { User.new(cancelled: cancelled) }
context 'something' do
# ...
end
context 'cancelled' do
let(:cancelled} { true }
# ...
end
end
```
---
### 實際上...
```ruby=
RSpec.describe SomeOrder do
let(:user) { create(:user) }
let(:payment) { 'atm' }
let(:shipping) { 'oversea' }
let(:invoice) { 'paper' }
let(:product1) { create(:product) }
let(:product2) { create(:product, :main_product) }
let!(:sub_order) { create(:sub_order, main_order: main_order) }
let!(:sub_order_item1) { create(:sub_order_item, sub_order: sub_order, main_order: main_order, product: product1) }
let!(:sub_order_item2) { create(:sub_order_item, sub_order: sub_order, main_order: main_order, product: product2) }
let(:sub_order_items) { sub_order.reload.sub_order_items }
let(:main_order_recipient_phone) do
if main_order.mobile_phone.present?
# ...
else
# ...
end
end
let(:store_id) { nil }
let(:store_name) { nil }
# # More tests...
end
```
----

---
### `factory_bot` - 測試案例 (1/9)
```ruby=
describe FactoryBot::EvaluatorClassDefiner do
let(:simple_attribute) {
stub("simple attribute", name: :simple, to_proc: -> { 1 })
}
let(:relative_attribute) {
stub("relative attribute", name: :relative, to_proc: -> { simple + 1 })
}
let(:attribute_that_raises_a_second_time) {
stub(
"attribute that would raise without a cache",
name: :raises_without_proper_cache,
to_proc: -> { raise "failed" if @run; @run = true; nil }
)
}
let(:attributes) {
[
simple_attribute,
relative_attribute,
attribute_that_raises_a_second_time
]
}
let(:class_definer) {
FactoryBot::EvaluatorClassDefiner.new(
attributes,
FactoryBot::Evaluator
)
}
let(:evaluator) {
class_definer.evaluator_class.new(
stub("build strategy", add_observer: true)
)
}
it "adds each attribute to the evaluator" do
evaluator.simple.should eq 1
end
it "evaluates the block in the context of the evaluator" do
evaluator.relative.should eq 2
end
# More tests
end
```
----
### `factory_bot` - Code Smells (2/9)
- [General Fixture](http://xunitpatterns.com/Obscure%20Test.html#General%20Fixture):測試案例參考一個很大的 fixture,但實際上測試案例只使用到 fixture 的一小部份。
- [Mystery Guest](http://xunitpatterns.com/Obscure%20Test.html#Mystery%20Guest):閱讀測試案例時,看不出來 fixture 和驗證邏輯之間的關係,因為有些 fixture 是在 `it` block 外部設定。
----
### `factory_bot` - Behavior Smells (3/9)
- [Fragile Test](http://xunitpatterns.com/Fragile%20Test.html#Fragile%20Fixture):因為 fixture 彼此存在相關性,在時間推移後,這樣的測試寫法會增加 maintain 測試的成本。
- [Slow Test](http://xunitpatterns.com/Slow%20Tests.html):使用 `let` 事先建立資料,則每個測試案例會多出比所需更多的測試資料。
----
#### `factory_bot` - 使用 Ruby methods 重構測試案例 (4/9)
```ruby=
it "adds each attribute to the evaluator" do
evaluator.simple.should eq 1
end
```
----
#### `factory_bot` - 使用 Ruby methods 重構測試案例 (5/9)
```ruby=
it "adds each attribute to the evaluator" do
simple_attribute =
stub("simple attribute", name: :simple, to_proc: -> { 1 })
relative_attribute =
stub("relative attribute", name: :relative, to_proc: -> { simple + 1 })
attribute_that_raises_a_second_time =
stub("attribute that would raise without a cache",
name: :raises_without_proper_cache,
to_proc: -> { raise "failed" if @run; @run = true; nil })
attributes = [
simple_attribute,
relative_attribute,
attribute_that_raises_a_second_time
]
class_definer = FactoryBot::EvaluatorClassDefiner.new(
attributes,
FactoryBot::Evaluator
)
evaluator = class_definer.evaluator_class.new(
stub(
"build strategy",
add_observer: true
)
)
evaluator.simple.should eq 1
end
```
----
#### `factory_bot` - 使用 Ruby methods 重構測試案例 (6/9)
```ruby=
it "adds each attribute to the evaluator" do
simple_attribute =
stub("simple attribute", name: :simple, to_proc: -> { 1 })
attributes =
[simple_attribute]
class_definer = FactoryBot::EvaluatorClassDefiner.new(
attributes,
FactoryBot::Evaluator
)
evaluator = class_definer.evaluator_class.new(
stub("build strategy", add_observer: true)
)
evaluator.simple.should eq 1
end
```
----
#### `factory_bot` - 使用 Ruby methods 重構測試案例 (7/9)
```ruby=
it "evaluates the block in the context of the evaluator" do
simple_attribute =
stub("simple attribute", name: :simple, to_proc: -> { 1 })
relative_attribute =
stub("relative attribute", name: :relative, to_proc: -> { simple + 1 })
attributes =
[simple_attribute, relative_attribute]
class_definer = FactoryBot::EvaluatorClassDefiner.new(
attributes,
FactoryBot::Evaluator
)
evaluator = class_definer.evaluator_class.new(
stub("build strategy", add_observer: true)
)
evaluator.relative.should eq 2
end
```
----
#### `factory_bot` - 使用 Ruby methods 重構測試案例 (8/9)
```ruby=
it "adds each attribute to the evaluator" do
attribute = stub_attribute(:attribute) { 1 }
evaluator = define_evaluator(attributes: [attribute])
evaluator.attribute.should eq 1
end
it "evaluates the block in the context of the evaluator" do
dependency_attribute = stub_attribute(:dependency) { 1 }
attribute = stub_attribute(:attribute) { dependency + 1 }
evaluator = define_evaluator(attributes: [dependency_attribute, attribute])
expect(evaluator.attribute).to eq 2
end
def define_evaluator(arguments = {})
evaluator_class = define_evaluator_class(arguments)
evaluator_class.new(FactoryBot::Strategy::Null)
end
def define_evaluator_class(arguments = {})
evaluator_class_definer = FactoryBot::EvaluatorClassDefiner.new(
arguments[:attributes] || [],
arguments[:parent_class] || FactoryBot::Evaluator
)
evaluator_class_definer.evaluator_class
end
def stub_attribute(name = :attribute, &value)
value ||= -> {}
stub(name.to_s, name: name.to_sym, to_proc: value)
end
```
----
### `factory_bot` 小結 (9/9)
- Easier Readable:在 `it` block 驗證邏輯時,我們可以清楚地看到所有相關變數。
- Not Brittle:因為每個測試案例只有所需的信息,我們不用擔心異動一個測試案例會造成另一個測試案例壞掉。
---
## Reference
- [xUnit Test Patterns](http://xunitpatterns.com/index.html)
- [factory_bot/spec - evaluator_class_definer_spec.rb](https://github.com/thoughtbot/factory_bot/blob/main/spec/factory_bot/evaluator_class_definer_spec.rb)
- [Let and let!](https://relishapp.com/rspec/rspec-core/v/2-11/docs/helper-methods/let-and-let)
- [Use let and let! #8](https://github.com/betterspecs/betterspecs/issues/8)
---
## Q & A
----
### 自訂 helper
```ruby=
# spec/support/helper/some_helper.rb
# NOTE: 通常 spec_helper 會 require support 下的所有東⻄
module SomeHelper
extend ActiveSupport::Concern
class_method do
def say_class_method
puts 'class method'
end
end
def say_helper_method
puts 'helper method'
end
end
RSpec.configure do |config|
config.include SomeHelper, :say_some_module
end
```
----
{"metaMigratedAt":"2023-06-17T05:55:06.837Z","metaMigratedFrom":"YAML","title":"RSpec or Let's Not","breaks":true,"slideOptions":"{\"theme\":\"serif\",\"allottedMinutes\":25}","contributors":"[{\"id\":\"d54ce6e5-6ae3-4721-9f3c-8ec2241a8a35\",\"add\":9398,\"del\":1661}]"}