About me • 張⽂文鈿a.k.a. ihower • http://ihower.tw • http://twitter.com/ihower • Instructor at ALPHA Camp • https://alphacamp.co • Rails Developer since 2006 • i.e. Rails 1.1.6 era
3.
Agenda • Part1: RSpecand TDD • 什麼是⾃自動化測試和 RSpec • Continuous Testing and Guard • TDD and Code Kata • Mocks and Stubs • Part2: RSpec on Rails • model, controller, view, helper, routing, mailer, request spec • feature spec and capybara • Tools: spring, simplecov, cucumber • CI (Continuous Integration)
Ruby 的 Test::Unit classOrderTest < Test::Unit::TestCase def setup @order = Order.new end def test_order_status_when_initialized assert_equal @order.status, "New" end def test_order_amount_when_initialized assert_equal @order.amount, 0 end end Method 的命名是 個⿇麻煩
28.
describe Order do beforedo @order = Order.new end context "when initialized" do it "should have default status is New" do expect(@order.status).to eq(“New”) end it "should have default amount is 0" do expect(@order.amount).to eq(0) end end end RSpec 寫法
加⼊入 it describe Orderdo describe "#amount" do context "when user is vip" do it "should discount five percent if total > 1000" do # ... end it "should discount ten percent if total > 10000" do # ... end end context "when user is not vip" do it "should discount three percent if total > 10000" do # ... end end end end
describe Order do describe"#amount" do context "when user is vip" do it "should discount five percent if total >= 1000" do user = User.new( :is_vip => true ) order = Order.new( :user => user, :total => 2000 ) expect(order.amount).to eq(1900) end it "should discount ten percent if total >= 10000" { ... } end context "when user is vip" { ... } end end
before 和 after •如同 xUnit 框架的 setup 和 teardown • before(:each) 每段 it 之前執⾏行 • before(:all) 整段 describe 執⾏行⼀一次 • after(:each) • afte(:all)
45.
describe Order do describe"#amount" do context "when user is vip" do before(:each) do @user = User.new( :is_vip => true ) @order = Order.new( :user => @user ) end it "should discount five percent if total >= 1000" do @order.total = 2000 @order.amount.should == 1900 end it "should discount ten percent if total >= 10000" do @order.total = 10000 @order.amount.should == 9000 end end context "when user is vip" { ... } end end
describe Order do describe"#amount" do context "when user is vip" do let(:user) { User.new( :is_vip => true ) } let(:order) { Order.new( :user => user ) } it "should discount five percent if total >= 1000" do order.total = 2000 order.amount.should == 1900 end it "should discount ten percent if total >= 10000" do order.total = 10000 order.amount.should == 9000 end end context "when user is vip" { ... } end end
What have welearned? • ⼀一個 it 裡⾯面只有⼀一種測試⺫⽬目的,最好就 只有⼀一個 expectation • 要先從測試 failed 失敗案例開始 • 確保每個測試都有效益,不會發⽣生砍 掉實作卻沒有造成任何測試失敗 • ⼀一開始的實作不⼀一定要先直攻⼀一般解, 可以⼀一步⼀一步在 cycle 中進⾏行思考和重構 • 測試程式碼的可讀性⽐比 DRY 更重要 • 安全重構:無論是改實作或是改測試碼, 當時的狀態應該要維持 Green As the tests get more specific, the code gets more generic. Programmers make specific cases work by writing code that makes the general case work.
測試狀態 describe "#receiver_name" do it"should be user name" do user = double(:user, :name => "ihower") order = Order.new(:user => user) expect(order.receiver_name).to eq("ihower") end end
版本⼀一: 這個 Order 完全隔離Item describe Order do before do @order = Order.new end describe "#<<" do it "should push item into order.items" do Item = double("Item") item = double("item", :name => "foobar") allow(Item).to receive(:new).and_return(item) @order << "foobar" expect(@order.items.last.name).to eq(“foobar”) end end end
版本⼆二: 實作真的 Item 物件 (拿掉剛才的所有stub code 即可) describe Order do before do @order = Order.new end describe "#<<" do it "should push item into order.items" do @order << "foobar" expect(@order.items.last.name).to eq "foobar" end end end
91.
需要寫真的 Item 讓測試通過 class Item attr_accessor:name def initialize(name) self.name = name end end describe Item do it "should be created by name" do item = Item.new("bar") expect(item.name).to eq "bar" end end
92.
差在哪? 假設我們後來 修改了 Item的實作 class Item attr_accessor :name def initialize(name) self.name = name.upcase end end
造假舉例: MVC 中的Controller 測試 #測狀態 it "should be created successful" do post :create, :name => "ihower" expect(response).to be_success expect(User.last.name).to eq("ihower") # 會真的存進資料庫 end # 測⾏行為 it "should be created successful" do expect(Order).to receive(:create).with(:name => "ihower").and_return(true) post :create, :name => "ihower" expect(response).to be_success end