## 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 ``` ---- ![](https://i.imgur.com/OTvqi5c.jpg) --- ### `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}]"}
    361 views