Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Testing ASP.net and C# using Ruby (NDC2010)

Session given at NDC2010, Oslo, Norway on Testing ASP.net using Ruby

  • Login to see the comments

  • Be the first to like this

Testing ASP.net and C# using Ruby (NDC2010)

  1. 1. Testing C# and ASP.net using Ruby<br />Meerkatalyst<br />@Ben_HallBen@BenHall.me.ukBlog.BenHall.me.ukCodeBetter.com/blogs/benhall<br />
  2. 2. London based C# MVPWeb Developer @ 7digital.com Working on a number of Open Source Projects<br />Co-Author of <br />Testing ASP.net Web Applications<br />http://www.testingaspnet.com<br />
  3. 3. Outside-in development<br />UI level testing<br />Cucumber<br />Object level testing using RSpec<br />
  4. 4. WHY?<br />http://www.flickr.com/photos/atomicpuppy/2132073976/<br />
  5. 5. VALUE<br />
  6. 6. State of .NET today<br />
  7. 7. Realised Ruby doesn’t have as many problems as .net<br />
  8. 8. Innovation<br />
  9. 9. Outside-in Development<br />
  10. 10. Behaviour Driven Development<br />Cucumber, RSpec and many others<br />
  11. 11. Direct communication with customers<br />
  12. 12. Focus<br />
  13. 13.
  14. 14. UI Testing is not hard...<br />Unless you make an effort<br />
  15. 15.
  16. 16. What does this do?<br />[Test]public void WatiNSearchText_SearchWatiN_TextDisplayedOnScreen(){    IE ie = new IE("http://localhost:49992/WatiNSite/");    ie.TextField(Find.ByName(“q")).TypeText("WatiN");    ie.Button(Find.ByValue("Search")).Click();    bool result = ie.Text.Contains("WatiN");    Assert.IsTrue(result); }<br />
  17. 17.
  18. 18. WebRat<br />http://www.flickr.com/photos/whatwhat/22624256/<br />
  19. 19. visit<br />click_link<br />fill_in<br />click_button<br />check and uncheck<br />choose<br />select<br />attach_file<br />
  20. 20. visit ‘http://localhost’<br />
  21. 21. Reduce Ceremony<br />
  22. 22. visit ‘http://www.google.com’<br />fill_in :q, :with => ‘asp.net’<br />click_button ‘Google Search’<br />response_body.should contain(‘Microsoft’)<br />
  23. 23. Design UI for testability<br />Outside-in development encourages this<br />
  24. 24. DATA!<br />http://www.flickr.com/photos/gagilas/2659695352/<br />
  25. 25. Hard and Important<br />Production data in dev anyone?<br />
  26. 26. Active Record and SQL Server<br />Order.create<br />
  27. 27. class Order < ActiveRecord::Base<br />set_primary_key :OrderID<br />has_many :OrderItem<br />has_one :OrderStatus, :primary_key => "OrderStatusID", :foreign_key => "OrderStatusID"<br />belongs_to :Customer, :primary_key => "UserName", :foreign_key => "UserName"<br /> def self.create()<br /> order = Order.new (:modified_on => DateTime.now, <br /> :created_on => DateTime.now, <br /> :order_id => Guid.new.to_s)<br />order.OrderStatusID = OrderStatus.find_or_create<br />order.Customer = Customer.find_or_create(Faker::Internet.user_name)<br />order.save!<br /> return order<br /> end<br />end<br />Legacy Database<br />Looking at auto-generating<br />model for testing<br />
  28. 28. Test Data Builders<br />ProductCatalogBuilder<br /> .create(10.products)<br /> .for_shop(123) <br />Customer<br /> .create(“Bob”)<br /> .owning(10.products)<br /> .with_credit_card(CreditCard.create)<br /> .with_address(Address.create) <br />
  29. 29. Customer.find_or_create(Faker::Internet.user_name)<br />>>> Faker::Internet.user_name<br /><ul><li>"akeem_kris“</li></ul>>>> Faker::Internet.user_name<br /><ul><li>"taylor.hodkiewicz“</li></ul>>>> Faker::Internet.user_name<br />=> "nicolette.nitzsche"<br />>>> Faker::Internet.email<br /><ul><li>dejon@hayeswalsh.uk</li></ul>>>> Faker::Name.name<br />=> "Murray Hermiston"<br />
  30. 30. 2 rules of data generation<br />Always insert<br />Always clean up<br />
  31. 31. We still need to write the tests in a successful manner<br />
  32. 32. Cucumber<br />http://www.flickr.com/photos/vizzzual-dot-com/2738586453/<br />
  33. 33. Feature:Search Google<br />In order to find information<br />As a researcher<br />I want to search google<br /> <br /> Scenario: Searching for a valid term<br />GivenI visit "http://www.google.com/"<br />    WhenI enter a search for "asp.net"<br />    AndI click the "Google Search" button<br />    ThenI should see the first result "The Official Microsoft ASP.NET Site"<br />
  34. 34. Given /^I visit "([^"]*)"$/ do |website|<br />  visit website<br />end<br /> <br />When /^I enter a search for "([^"]*)"$/ do |term|<br />  fill_in :q, :with => term<br />end<br /> <br />When /^I click the "([^"]*)" button$/ do |button|<br />  click_button button<br />end<br /> <br />Then /^I should see the first result "([^"]*)"$/ do |result|<br />  response_body.should contain(result)<br />end<br />
  35. 35. Thanks to TekPub for Kona sample<br />
  36. 36. Thanks to TekPub for Kona sample<br />
  37. 37. Feature: Display Products<br /> In order to browse the catalog<br /> The customer<br /> Needs to see products<br />Scenario: Single featured product on homepage<br /> Given the featured product "Adventure Works 20x30 Binoculars"<br /> When I visit the homepage<br /> Then I should see "Adventure Works 20x30 Binoculars" listed under "Featured"<br />
  38. 38. Given /^the featured product "([^"]*)"$/ do |product_name|<br />Product.createproduct_name, ‘featured’<br />end<br />
  39. 39. When /^I visit the homepage$/ do<br />visit url + '/'<br />end<br />
  40. 40. “Designed” UI for Testability<br />Then /^I should see "([^"]*)" listed under "([^"]*)"$/ do |product, area|<br /> within div_id(area) do |products|<br />products.should contain(product)<br /> end<br />End<br />def div_id(area)<br /> "#" + area.downcase.underscore<br />end<br />
  41. 41. Using a ‘real’ framework<br />Unit testing frameworks for UI testing? Really?!<br />
  42. 42. Before do<br />open_connection<br />empty_database<br />end<br />at_exit do<br />empty_database<br />End<br />def empty_database<br />date_of_last_restore = '01/01/2010'<br />OrderItem.delete_all["DateAdded > ?", date_of_last_restore]<br />Order.delete_all["CreatedOn > ?", date_of_last_restore]<br />end<br />Multiple Hooks<br />
  43. 43. require 'cucumber_screenshot'<br />After do |scenario| <br /> screenshot if scenario.failed?<br />end<br />AfterStep('@screenshot') do |scenario| <br /> screenshot if screenshot_due? <br />end<br />
  44. 44. def url<br /> "http://www.website.local"<br />end<br />Webrat.configure do |config|<br />config.mode = :selenium<br />config.application_framework = :external<br />end<br />env_selenium.rb<br />
  45. 45. Webrat.configure do |config|<br />config.mode = :selenium<br />config.application_framework = :external<br />config.selenium_browser_key = get_browser_key<br />end<br />def get_browser_key()<br /> command = "*firefox"<br /> command = ENV['BROWSER'] if ENV['BROWSER']<br /> return command<br />end<br />env_selenium.rb<br />
  46. 46. cucumber --profile selenium browser=*firefox<br />cucumber --profile selenium browser=*safari<br />cucumber --profile selenium browser=*iexplore<br />cucumber --profile selenium browser=*googlechrome<br />
  47. 47. env_headless.rb<br />Webrat.configure do |config|<br />  config.mode = :mechanize<br />end<br />cucumber --profile webrat<br />
  48. 48. ANTI-PATTERNS<br />http://www.flickr.com/photos/gagilas/2659695352/<br />
  49. 49. Noisy, repeated scenarios<br />Scenario: Blowout Specials<br /> Given "Cascade Fur-lined Hiking Boots" in "Blowout Specials"<br /> When I visit the homepage<br /> Then I should see "Cascade Fur-lined Hiking Boots" listed under "Blowout Specials“<br />Scenario: Blowout Specials<br /> Given " Trailhead Locking Carabiner " in "Blowout Specials"<br /> When I visit the homepage<br /> Then I should see "Trailhead Locking Carabiner " listed under "Blowout Specials"<br />Scenario: Blowout Specials<br /> Given " Climbing Rope with Single Caribiner " in "Blowout Specials"<br /> When I visit the homepage<br /> Then I should see " Climbing Rope with Single Caribiner " listed under "Blowout Specials"<br />
  50. 50. Single scenario, multiple examples<br />Scenario Outline: Blowout Specials<br /> Given <product> in "Blowout Specials"<br /> When I visit the homepage<br /> Then I should see <product> listed under "Blowout Specials"<br /> Examples:<br /> | product |<br /> | "Cascade Fur-lined Hiking Boots" |<br /> | "Trailhead Locking Carabiner" |<br /> | "Climbing Rope with Single Caribiner" |<br />
  51. 51. Long scenarios<br />When I go to the "Checkout" page<br />And I enter “Mr A. Nonymous ” in “Card Name” textbox<br />And I enter “1111111111111111” in “Card Number” textbox<br />And I enter “GU1 2PE” in “Card Post Code” textbox<br />And I select “01” in “Card Start Month” textbox<br />And I select “09” in “Card Start Year” textbox<br />And I select “01” in “Card End Month” textbox<br />And I select “19” in “Card End Year” textbox<br />And I enter “123” in “Card Validation” textbox<br />And I click the "Pay with Card (Add Card)" button<br />Then I should be on the "Download" page<br />
  52. 52. When I go to the "Checkout" page<br />And I enter a new credit card:<br /> | type       | field     | value             |<br /> | textbox   | Card Name         | Mr A. Nonymous   |<br /> | textbox   | Card Number       | 4444333322221111 |<br /> | textbox   | Card Post Code   | GU1 2PE           |<br /> | select     | Card Start Month | 01               |<br /> | select   | Card Start Year  | 09               |<br /> | select     | Card End Month   | 01               |<br /> | select   | Card End Year    | 19               |<br /> | textbox   | Card Validation  | 123              |<br />And I click the "Pay with Card (Add Card)" button<br />Then I should be on the "Download" page<br />
  53. 53. Clear, re-useable, readable<br />When I go to the "Checkout" page<br />And I enter a valid credit card <br />And I click the "Pay with Card (Add Card)" button<br />Then I should be on the "Download" page<br />
  54. 54. Implementation details in steps<br />When I go to the "Checkout" page<br />And I enter “Mr A. Nonymous ” in “Card Name” textbox<br />And I enter “1111111111111111” in “Card Number” textbox<br />And I enter “GU1 2PE” in “Card Post Code” textbox<br />And I select “01” in “Card Start Month” textbox<br />And I select “09” in “Card Start Year” textbox<br />And I select “01” in “Card End Month” textbox<br />And I select “19” in “Card End Year” textbox<br />And I enter “123” in “Card Validation” textbox<br />And I click the "Pay with Card (Add Card)" button<br />Then I should be on the "Download" page<br />
  55. 55. Business focused<br />When I go to the "Checkout"<br />And I enter a valid credit card <br />And pay<br />Then I should be taken to my Downloads<br />
  56. 56. Conjunction Steps<br />Scenario: Recommendations should include other similar purchased products<br /> Given customer order for "Trailhead Locking Carabiner" and "Climbing Rope with Single Caribiner"<br /> And customer order for "Trailhead Locking Carabiner" and "North Star Compass"<br />Given I have shades and a brand new Mustang<br />
  57. 57. Reuse and recombine<br />Scenario: Recommendations should include other similar purchased products<br /> Given the following customers and orders:<br /> |customer | order_list |<br /> |Customer A | Trailhead Locking Carabiner, Climbing Rope with Single Caribiner |<br /> |Customer B | Trailhead Locking Carabiner, North Star Compass |<br />Given I have shades <br />And I have a brand new Mustang<br />
  58. 58. All steps within a single file<br />
  59. 59. Steps focused around application domain<br />
  60. 60. One environment file<br />
  61. 61. Small, focused environment config<br />
  62. 62. Multiple extension frameworks<br />Test command line applications<br />Test SilverlightFlex<br />Test WPF based desktop applications<br />Execute in parallel across multiple machines<br />
  63. 63. Lonestar<br />
  64. 64. Outside-in Development<br />
  65. 65. RSpec<br />http://www.flickr.com/photos/dodgsun/467076780/<br />
  66. 66. Context Specification<br />
  67. 67. Shift focus<br />Final Intent<br />
  68. 68. Code Coverage<br />Feature coverage is far more important<br />
  69. 69.
  70. 70. Ruby (via IronRuby)<br />p = ProductController.new(nil)<br />C#<br />public class ProductController : Controller<br />{<br />IStoreService _service;<br /> public ProductController(IStoreService service)<br /> {<br /> _service = service;<br /> }<br />}<br />Thanks to TekPub for Kona sample<br />
  71. 71. Define the context<br />C#<br />public ActionResult Index(int? id)<br />{<br />ProductListViewModel model = null;<br /> if (id.HasValue)<br /> model = _service.GetHomeModel(id.Value);<br /> else<br /> return RedirectToAction("Index", "Home");<br /> return View(model);<br />}<br />Ruby (via IronRuby)<br />describe ProductController, “Product homepage requested" do<br /> context "when Category ID is empty" do<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  72. 72. Create the context<br />$: << ‘../../Kona/bin/’<br />require ‘Kona.dll’<br />Include Kona::Controllers<br />describe ProductController, “Product homepage requested" do<br /> context "when Category ID is empty" do<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  73. 73. Create the context<br />require '../spec_helper‘<br />describe ProductController, “Product homepage requested" do<br /> context "when Category ID is empty" do<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  74. 74. Create the context<br />require '../spec_helper‘<br />describe ProductController, “Product homepage requested" do<br /> context "when Category ID is empty" do<br /> before(:each) do<br /> controller = ProductController.new nil<br />category_id = nil<br /> @result = controller.Indexcategory_id<br /> end<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  75. 75. Define the spec<br />require '../spec_helper‘<br />describe ProductController, “Product homepage requested" do<br /> context “with empty Category ID" do<br /> before(:each) do ... End<br /> it "should redirect to main landing page" do<br /> @result.route_values[‘controller'].should == ‘Index’<br /> @result.route_values[‘home'].should == ‘Home’<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  76. 76. Stubbing via caricature<br />context “with validCategory ID" do<br /> Before(:each)<br />view_model = product_list('Test Category')<br /> service = isolate Services::IStoreService<br />service.when_receiving(:get_home_model).with(valid_category_id).return(view_model)<br /> controller = ProductController.new service<br /> @result = controller.Indexvalid_category_id<br /> end<br /> it “returns a view model” do<br /> @result.view_model.should_not == nil<br /> end<br /> it “should return name of selected category” do<br /> @result.view_model.selected_category.name.should == 'Test Category‘<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  77. 77. http://www.flickr.com/photos/buro9/298994863/<br />WHY RUBY?<br />
  78. 78. [Subject(typeof(HomeController))]public class when_the_navigation_controller_is_asked_for_the_header : specification_for_navigation_controller{ static ActionResult result; Establish context = () => identity_tasks.Stub(i => i.IsSignedIn()).Return(true); Because of = () => result = subject.Menu(); It should_ask_the_identity_tasks_if_the_user_is_signed_in = () => identity_tasks.AssertWasCalled(x => x.IsSignedIn()); It should_return_the_default_view = () => result.ShouldBeAView().And().ViewName.ShouldBeEmpty(); It should_not_use_a_master_page = () => result.ShouldBeAView().And().MasterName.ShouldBeEmpty(); It should_set_the_view_model_property_to_a_new_menu_view_model = () =>result.Model<MenuViewModel>().ShouldNotBeNull(); It should_set_the_properties_of_the_view_model_correctly = () => result.Model<MenuViewModel>().IsLoggedIn.ShouldBeTrue(); }<br />
  79. 79. describe HomeController, “When the navigation controller is asked for the header” do before(:all) do @identity_tasks.Stub(i => i.IsSignedIn()).Return(true); @result = subject.Menu(); end it “should ask the identity tasks if the user is signed in” do @identity_tasks.did_receive(:is_signed_in) .should be_successful end it “should return the default view” do @result.shouldbe_view @result.view_name.shouldbe_empty end it “should not use a master page” do @result.shouldbe_view @result.master_name.shouldbe_empty end it “should set the view model property to a new menu view model” do @result.shouldbe_view @result.model.should_notbe_nil end it “should set the properties of the view model correctly” do @result.model.is_logged_in.shouldbe_true endend<br />
  80. 80. Consider your test suite containing > 500 tests<br />Each test matters.<br />But each problem hurts more<br />
  81. 81. http://www.flickr.com/photos/leon_homan/2856628778/<br />
  82. 82. Focus on finding true value<br />Look at other communities for advice, support and inspiration<br />
  83. 83. @Ben_Hall<br />Ben@BenHall.me.uk<br />Blog.BenHall.me.uk<br />http://lolcatgenerator.com/lolcat/120/<br />
  84. 84. Tests and Databases<br />describe Services::StoreService do<br /> before(:all) do<br />session = NHibernate::create_session<br />NHibernate::insert_category session<br /> Services::StoreService.new session<br /> @model = service.get_home_model 0<br /> end<br /> it "should return constructed object" do<br /> @model.should_notbe_nil<br /> end<br /> it "should return all categories from database" do<br /> @model.Categories.to_a.Count.should == 1<br /> end<br />end<br />Thanks to TekPub for Kona sample<br />
  85. 85. Share specs<br />share_examples_for "unauthorized user" do it 'sets an-error message' do result.view_model.error_message.should == "Sorry, you need to login to access." end it 'redirects to the login page' do result.view_name.should end end describe CheckoutController do it_should_behave_like “unauthorized user“describe AccountController doit_should_behave_like “unauthorized user“ <br />
  86. 86. Dynamically create specs<br />def find_products(category)Product.Find(:all, :conditions=> “category #{category}”)enddescribe ‘products should be able to be added to basket' do before(:all) do @basket = Basket.new endfind_products(‘Boots’).each do |p| it “can add #{p.product_name} to basket" do @basket.add_item p.id @basket.should contain(p) end endend<br />
  87. 87. INTEGRATION TESTS<br />http://www.flickr.com/photos/gagilas/2659695352/<br />
  88. 88. Useful Links<br />http://www.github.com/BenHall<br />http://blog.benhall.me.uk<br />http://www.codebetter.com/blogs/benhall<br />http://stevehodgkiss.com/2009/11/14/using-activerecord-migrator-standalone-with-sqlite-and-sqlserver-on-windows.html<br />http://www.testingaspnet.com<br />http://msdn.microsoft.com/en-us/magazine/dd434651.aspx<br />http://msdn.microsoft.com/en-us/magazine/dd453038.aspx<br />http://www.cukes.info<br />http://github.com/aslakhellesoy/cucumber<br />http://github.com/brynary/webrat<br />
  89. 89. using Cuke4Nuke.Framework;<br />usingNUnit.Framework;<br />usingWatiN.Core;<br />namespaceGoogle.StepDefinitions<br />{<br />    publicclassSearchSteps<br />    {<br />        Browser _browser;<br /> [Before]<br />        publicvoidSetUp()<br />        {<br />            _browser = new WatiN.Core.IE();<br />        }<br /> [After]<br />        publicvoidTearDown()<br />        {<br />            if (_browser != null)<br />            {<br />                _browser.Dispose();<br />            }<br />        }<br /> [When(@"^(?:I'm on|I go to) the search page$")]<br />        publicvoidGoToSearchPage()<br />        {<br />            _browser.GoTo("http://www.google.com/");<br />        }<br /> [When("^I search for "(.*)"$")]<br />        publicvoidSearchFor(string query)<br />        {<br />            _browser.TextField(Find.ByName("q")).TypeText(query);<br />            _browser.Button(Find.ByName("btnG")).Click();<br />        }<br /> [Then("^I should be on the search page$")]<br />        publicvoidIsOnSearchPage()<br />        {<br />            Assert.That(_browser.Title == "Google");<br />        }<br /> [Then("^I should see "(.*)" in the results$")]<br />        publicvoidResultsContain(stringexpectedResult)<br />        {<br />            Assert.That(_browser.ContainsText(expectedResult));<br />        }<br />    }<br />}<br />
  90. 90. Given /^(?:I'm on|I go to) the search page$/ do<br />  visit 'http://www.google.com'<br />end<br /> <br />When /^I search for "([^"]*)"$/ do|query|<br />  fill_in 'q', :with => query<br />  click_button 'Google Search'<br />end<br /> <br />Then /^I should be on the search page$/ do<br /> dom.search('title').should == "Google"<br />end<br /> <br />Then /^I should see "(.*)" in the results$/ do|text|<br />  response.should contain(text)<br />end<br />
  91. 91. Software<br />Recommended:<br />IronRuby<br />Ruby<br />Cucumber<br />Rspec<br />WebRat<br />mechanize<br />Selenium RC<br />selenium-client<br />Caricature<br />activerecord-sqlserver-adapter<br />Optional:<br />XSP Mono<br />JetBrain’sRubyMine<br />JRuby<br />Capybara<br />Celerity<br />Active record <br />active-record-model-generator<br />Faker<br />Guid<br />
  92. 92. SQL Server and Ruby<br />gem install activerecord-sqlserver-adapter<br />Download dbi-0.2.2.zip <br />Extract dbdADO.rb to rubysite_ruby1.8DBDADO.rb<br />

×