In this talk that I gave at RailsWayCon I talk about practices that help to maintain readable, fast and simple test. I also show some examples where hard tests point to design issues. In the last part i introduced some tool that may help to maintain a good test suite.
3. TESTING CONS
Write Extra Code
Interrupt the Flow
Add Complexity to our Codebase
More Maintenance
the negative effects are either blends or can be minimized
4. TESTING PROS
Think Double
Avoid Errors
Narrow Down Bugs
Prevent Regression
Improve Design
This is what I get out of TDD
5. Failing Scenarios:
cucumber features/currency_conversion.feature:129 # Scenario: Using the web-
service supplied exchange rate for XAU
cucumber features/statistics.feature:161 # Scenario: view statistics for completed
withdrawals
cucumber features/statistics.feature:189 # Scenario: view statistics for completed
deposits
cucumber features/transaction_confirmation.feature:6 # Scenario: Fund transactions
must be confirmed before the funding is added to the balance
In situations like this I ask myself the following questions
14. class Test{
//JMock & JUnit
public void testWithCategoryNameGetNameOfPostCategory(){
final Category category = context.mock(Category.class) //MockObjects
final Post post = context.mock(Post.class)
oneOf (post).name; will(returnValue('TestPost')); //Stub Methods
oneOf (post).category; will(returnValue(category))
context.checking(new Expectations() {{ //Mock
oneOf (category).name; will(returnValue('TestCat'))
}}
PostNamesWithCategoryLoader loader = new PostNamesWithCategoryLoader
context.assertIsSatisfied();
}
}
might be improved a little (anotations?)
15. # mocha
def test_show_gets_name_of_post_category
category = mock('category', :name => 'test') # mock, name must be called
post = stub('post', :category => category) #stub
Post.stubs(:find).returns(post) #partial stub for find
get :show
end
Ruby the better Language for Testing DSL
16. DOING TDD RIGHT IS HARD
How often do you swear at your breaking test?
How often does you feel that tests break your flow?
17. Simple
Fast
Maintainable
Durable
Side-effect Free
Repeatable
Thats what you want.
18. GUIDELINES NOT RULES
1. Part
I assume the basics like using setups, cleaning up
mostly unittests
20. THINK DOUBLE
What I want Presentation
Required Info Controller
How get these Info DomainModel
21. STORY
For customer information I want a listing of payment providers
and their logos. Their logo should be linked when I provide a
link. If their is no logo, show the link instead.
How many started to think of it as one problem to solve?
Split it up in separate problems.
22. REDUCE TO NO-BRAINER
LOGO URL
X X
X
X
Do this on complex problems and you will implement them easier, sometimes this will leave
you wondering.
23. BE EXPLICIT
it "should show the description for a payment provider" do
payme = PaymentProvider.new(description: 'Pay me')
payment_provider_listing(payme).should include(payme.description)
end
Not a good Idea.
24. BE EXPLICIT
it "should show the description for a payment provider" do
payme = PaymentProvider.new(description: 'Pay me')
payment_provider_listing(payme).should include('Pay me')
end
Be explicit.
25. 1 THING AT A TIME
payment_provider_listing(payme).should ==
'<a href="pay.me">PayMe</a>, Get Paid'
VS
payment_provider_listing(payme).should
include('<a href="pay.me">PayMe</a>')
That also relates to another problem
27. 1 THING AT A TIME
payment_provider_listing(payme).should ==
'<a href="pay.me">PayMe</a>, Get Paid'
VS
payment_provider_listing(payme).should
include('<a href="pay.me">PayMe</a>')
28. RELAX SPECIFICATION
payment_provider_listing(payme).should
match(/<a.*href="pay.me".*>PayMe</a>/)
Improves durability but not so easy to read anymore. Some things contradict.
29. RELAX SPECIFICATION
it "should initialize the correct gateway with the order" do
order = Order.make(:ecurrency => 'GoldPay')
GoldPayGateway.should_receive(:new).with(order, anything)
GatewayFactory.build(order)
end
Next Topic
31. it "should return the last created owner as the current owner" do
gateway = Gateway.create!(api_key: 'XYZ', url: 'api.pay.me')
provider = PaymentProvider.create!(url: 'pay.me', name: 'Payme', gateway: gateway)
owner_1 = Owner.create!(first_name: 'Phil', name: 'Adams', provider: provider)
owner_2 = Owner.create!(first_name: 'Maria', name: 'Williams', provider: provider)
provider.current_owner.should == owner_2
end
This is not good!
32. it "should return the last created owner as the current owner" do
@provider_with_two_owners.current_owner.should == @owner_2
end
This neither
You ask why?
34. TOO NOISY
it "should return the last created owner as the current owner" do
gateway = Gateway.create!(api_key: 'XYZ', url: 'api.pay.me')
provider = PaymentProvider.create!(url: 'pay.me', name: 'Payme', gateway: gateway)
owner_1 = Owner.create!(first_name: 'Phil', last_name: 'Adams', provider: provider)
owner_2 = Owner.create!(first_name: 'Maria', last_name: 'Williams', provider: provider)
provider.current_owner.should == owner_2
end
To much
35. NO CONTEXT
it "should return the last created owner as the current owner" do
@provider_with_two_owners.current_owner.should == @owner_2
end
To little
36. MAINTAIN CONTEXT
it "should return the last created owner as the current owner" do
provider = Provider.make
owner_1 = Owner.make provider: provider
owner_2 = Owner.make provider: provider
provider.current_owner.should == owner_2
end
right amout
37. SPLIT SETUP TO DRY
CONTEXT
describe 'transaction' do
before(:each) do
@payer = User.make
end
describe "percentage payout bonus set" do
before(:each) do
PayoutBonus.create!(amount: 20, unit: :percentage)
end
end
describe "fixed payout bonus set" do
before(:each) do
PayoutBonus.create!(amount: 10, unit: :usd)
end
end
end
another contradiction because this also increases creates complexity by adding new places
40. TEST IN ISOLATION
it "should be false when order has a single product from a single partner" do
partner = Partner.make
product = Product.make new_partner_name: partner.name
order = Order.make_unsaved partner: partner, contact: Contact.make, address: Address.make
order.items.build product: product, price: 100, scale_basis: 1, quantity: 1
order.save!
order.reload
order.should_not have_multiple_product_partners
end
Most speed is gained if only that code executes that is necessary for that test
41. TEST IN ISOLATION
it "should be false when order has a single product from a single partner" do
product = Product.make new_partner_name: "Pear"
order = Order.make_unsaved
order.items << OrderItem.make_unsaved :product = product
order.should_not have_multiple_product_partners
end
Most speed is gained if only that code executes that is necessary for that test
42. ISOLATION THROUGH
MOCKING
Instead of real dependencies inject mocks
different techniques
43. FAKES
• Mimic the behavior of the real object but don’t share all
characteristics
good example are in memory data storage vs. persistence.
44. FAKE USAGE
class FakeActivityLogger
def log(object)
@changes[object.id] ||= []
@changes[object.id] << object.changes
end
def changes_for(object)
@changes[object.id]
end
end
it "should call loggers on changes" do
logger = FakeActivityLogger.new
@logger_config.register(logger, User)
user = User.make(name: 'Paul')
user.update_attribute(:name, 'Paula')
logger.changes_for(user).should = [:name, 'Paul', 'Paula']
end
46. STUB USAGE
it "should change the given attribute" do
logger = stub('stub_logger', log: true)
@logger_config.register(logger, User)
user = User.make(name: 'Paul')
user.update_attribute(:name, 'Paula')
user.name.should == 'Paula'
end
47. MOCKS
• Pretend to be some object, also no logic but monitor if
interaction with them is specified
48. MOCK USAGE
it "should call loggers on changes" do
logger = mock('mock_logger')
@logger_config.register(logger, User)
user = User.make(name: 'Paul')
logger.expects(:log).with(user).once
user.update_attribute(:name, 'Paula')
end
49. MOCKS AND FAKES CAN
HIDE INTEGRATION BUGS
Integration or Acceptancetests to the rescue
excessive use of mocking my counteract fast testing if more integration test is required
52. LISTEN TO YOUR TESTS
“If something hurts you probably doing it wrong”
examples taken from real code I was involved.
53. TO MANY DEPENDENCIES
it "should be false when order has a single product from a single partner" do
partner = Partner.make
product = Product.make new_partner_name: partner.name
order = Order.make_unsaved partner: partner, contact: Contact.make, address: Address.make
order.items.build product: product, price: 100, scale_basis: 1, quantity: 1
order.save!
order.reload
order.should_not have_multiple_product_partners
end
Bad Design
57. STUB CHAINS/METHOD
CHAINS
it "should be false when order has a single product from a single partner" do
order = Order.make_unsaved
item = OrderItem.make_unsaved :name => 'iPet'
order.stub_chain(:items, :delivered, :from_partner, :last => item)
last_delivered_item_for_partner_label(partner, order).should include('iPed')
end
artificial
exposes to many internals
58. EXPLAINING COMMENTS ON
EXPECTATIONS
it "should sum all credits for the partner" do
credit1 = @partner.credits.make(:order => @order, :payment => 100)
credit1.items.make :price => 100, :quantity => 1
credit2 = @partner.credits.make(:order => @order, :payment => 100)
credit2.items.make :price => 150, :quantity => 1
@partner.credits.sum_for_month(Date.today.month, Date.today.year).should ==
297.5 # including 19% tax
end
59. TOOLS
help to write faster/better tests
beside your test/mock framework of choice
62. SPORK
+ Reduce startup time for testing frameworks (RSpec,
Cucumber, Test-Unit)
- Reloading breaks for code loaded in environment/
initializers
Great timesaver in unittest for bigger projects with lot of gems and plugins
63. BUNDLER
+ full dependency resolution at once
+ version lockdown
- beta
fixed version are great, no surprises with unexpected updates.
64. FACTORIES
(MACHINIST, FACTORY GIRL)
+ Greatly remove noise in tests
+ Dry Setups
+ Keep Context
- DB Overhead
65. HYDRA/PARALLEL SPEC
+ distribute tests on multiple cores or even machines
- extra setup
- concurrency/load order issues
So far no serious project running with them
66. CAPYBARA
+ Allow to run cucumber features agains different backends
+ full stack testing with culerity or selenium where required
- not one feature on many backends
setup is a super easy with cucumber