Testing survival Guide
Upcoming SlideShare
Loading in...5
×
 

Like this? Share it with your network

Share

Testing survival Guide

on

  • 2,726 views

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 ...

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.

Statistics

Views

Total Views
2,726
Views on SlideShare
2,669
Embed Views
57

Actions

Likes
2
Downloads
15
Comments
0

1 Embed 57

http://www.slideshare.net 57

Accessibility

Categories

Upload Details

Uploaded via as Adobe PDF

Usage Rights

© All Rights Reserved

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Processing…
Post Comment
Edit your comment

Testing survival Guide Presentation Transcript

  • 1. TESTING SURVIVAL GUIDE Thilo Utke A high level view on how to keep sane while doing tdd
  • 2. WHY DO WE TEST? why we take this extra step
  • 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
  • 6. BUG IN CODE?
  • 7. BUG IN TEST?
  • 8. EXTERNAL DEPENDENCY?
  • 9. A NEW CUCUMBER VERSION?
  • 10. A BUG IN RUBY BIG DECIMAL! forget to use rvm ruby
  • 11. TESTS GIVE US CONFIDENCE
  • 12. ALLOW US TO MOVE FORWARD IMHO Lack of confidence reason why software in Enterprise tend to become outdated Not to philosophical
  • 13. DOING TDD WITH RUBY IS EASY
  • 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
  • 19. 1. SIMPLICITY Break down the domain problem in no brainers
  • 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
  • 26. 2. DURABILITY
  • 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
  • 30. MAINTAINABILITY
  • 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?
  • 33. 2. CONTEXT Context is important What are you dealing with
  • 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
  • 38. BETTER UNDERSTANDABLE THAN DRY others have to work with your code
  • 39. 3. SPEED Most important for unit tests as you run them over and over again
  • 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
  • 45. STUBS • Pretend to be some object but without any logic
  • 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
  • 50. WRONG USAGE OF MOCKS HURT DURABILITY
  • 51. MOCK BEHAVIOR UNDERT TEST AND STUB THE REST Rule of thump
  • 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
  • 54. TO MANY DEPENDENCIES • split up • add layer • decouple logic
  • 55. MANY MOCKS / SETUP FOR INTERNALS before(:each) do @converter = mock_model CurrencyConverter, convert: 4, exchange_fee: 4, convertible?: true CurrencyConverter.stub!(:new).and_return(@converter) @modified_converter = mock_model ModifiedCurrencyConverter convert: 200 ModifiedCurrencyConverter.stub!(:new).and_return(@modified_converter) @user = mock_model(User, cleared_balance: 1000, add_balance_transaction: true, request_balance_transaction: true) @currency_conversion = CurrencyConversion.new source_amount: 100, source_currency: 'USD', destination_currency: 'EUR', user: @user end it "should request source amount plus fee from users source currency balance" do @user.should_receive(:request_balance_transaction).with(@currency_conversion, 104, 'USD') @currency_conversion.save! end 5 mocks/partial mocks
  • 56. INTERNAL DEPENDENCIES • Inject Dependencies • Iffrom callbacks, use a observer or think about Presenter/ Service
  • 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
  • 60. MORE INFRASTRUCTURE more to maintain
  • 61. BENEFIT > COST ?
  • 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
  • 67. WEBMOCK/SHAM_RACK + Allow to fake or mock http apis - Don’t tell you when the real api changes ;)
  • 68. ENVIRONMENTS + Allow to isolate tools completely - Extra startup time cucumber is doing it, you can do this too.
  • 69. INFO • Name: Thilo Utke • Company: Upstream-Agile GmbH • Web: http://upstre.am • Twitter: @freaklikeme