Upload
ben-hall
View
1.621
Download
2
Tags:
Embed Size (px)
DESCRIPTION
Presentation of Testing ASP.net using Ruby I gave at DDD8, January 30th 2010.
Citation preview
1| Why you should care2| Object Level testing3| UI level testing
What do I mean by Testing ASP.net?
Co-Author of Testing ASP.net Web Applicationshttp://www.testingaspnet.com
WHY TEST?
http://www.flickr.com/photos/atomicpuppy/2132073976/
It is 2010. Automated testing is no longer controversial.
[TestMethod]public void EditAction_Retrieves_Dinner_1_From_Repo_And_Countries_And_Sets_DinnerViewModel() { // Arrange var controller = CreateDinnersControllerAs("someuser");
// Act ViewResult result = controller.Edit(1) as ViewResult;
// Assert DinnerFormViewModel model = result.ViewData.Model as DinnerFormViewModel; Assert.AreEqual(13, model.Countries.Count());}
http://nerddinner.codeplex.com/SourceControl/changeset/view/23425#439968
[TestMethod]public void EditAction_Retrieves_Dinner_1_From_Repo_And_Countries_And_Sets_DinnerViewModel() { // Arrange var controller = CreateDinnersControllerAs("someuser");
// Act ViewResult result = controller.Edit(1) as ViewResult;
// Assert DinnerFormViewModel model = result.ViewData.Model as DinnerFormViewModel; Assert.AreEqual(13, model.Countries.Count());}
http://nerddinner.codeplex.com/SourceControl/changeset/view/23425#439968
[Fact] public void RsdReturnsValidRsdDoc() { FakeAreaService areaService = new FakeAreaService();
areaService.StoredAreas.Add("test", new Oxite.Models.Area(false, DateTime.MinValue, null, null, Guid.NewGuid(), DateTime.MinValue, "test"));
RouteCollection routes = new RouteCollection();
routes.Add("Posts", new Route("", new MvcRouteHandler()));
UrlHelper helper = new UrlHelper(new RequestContext(new FakeHttpContext(new Uri("http://oxite.net/"),"~/"), new RouteData()), routes);
Site site = new Site() { Host = new Uri("http://oxite.net") };
AreaController controller = new AreaController(site, areaService, null, null, null, null, null) { Url = helper };
ContentResult result = controller.Rsd("test");
Assert.NotNull(result);
XDocument rsdDoc = XDocument.Parse(result.Content); XNamespace rsdNamespace = "http://archipelago.phrasewise.com/rsd"; XElement rootElement = rsdDoc.Element(rsdNamespace + "rsd");
Assert.NotNull(rootElement); Assert.NotNull(rootElement.Attribute("version")); Assert.Equal("1.0", rootElement.Attribute("version").Value); Assert.Equal("Oxite", rootElement.Descendants(rsdNamespace + "engineName").SingleOrDefault().Value); Assert.Equal("http://oxite.net", rootElement.Descendants(rsdNamespace + "engineLink").SingleOrDefault().Value); Assert.Equal("http://oxite.net/", rootElement.Descendants(rsdNamespace + "homePageLink").SingleOrDefault().Value);
XElement apisElement = rootElement.Descendants(rsdNamespace + "apis").SingleOrDefault(); Assert.NotNull(apisElement); Assert.Equal(1, apisElement.Elements().Count());
XElement apiElement = apisElement.Elements().SingleOrDefault(); Assert.NotNull(apiElement); Assert.Equal(rsdNamespace + "api", apiElement.Name); Assert.Equal("MetaWeblog", apiElement.Attribute("name").Value); Assert.Equal(areaService.StoredAreas["test"].ID.ToString("N"), apiElement.Attribute("blogID").Value); Assert.Equal("true", apiElement.Attribute("preferred").Value); Assert.Equal("http://oxite.net/MetaWeblog.svc", apiElement.Attribute("apiLink").Value); }
http://oxite.codeplex.com/SourceControl/changeset/view/54721#419183
[Fact] public void RsdReturnsValidRsdDoc() { FakeAreaService areaService = new FakeAreaService();
areaService.StoredAreas.Add("test", new Oxite.Models.Area(false, DateTime.MinValue, null, null, Guid.NewGuid(), DateTime.MinValue, "test"));
RouteCollection routes = new RouteCollection();
routes.Add("Posts", new Route("", new MvcRouteHandler()));
UrlHelper helper = new UrlHelper(new RequestContext(new FakeHttpContext(new Uri("http://oxite.net/"),"~/"), new RouteData()), routes);
Site site = new Site() { Host = new Uri("http://oxite.net") };
AreaController controller = new AreaController(site, areaService, null, null, null, null, null) { Url = helper };
ContentResult result = controller.Rsd("test");
Assert.NotNull(result);
XDocument rsdDoc = XDocument.Parse(result.Content); XNamespace rsdNamespace = "http://archipelago.phrasewise.com/rsd"; XElement rootElement = rsdDoc.Element(rsdNamespace + "rsd");
Assert.NotNull(rootElement); Assert.NotNull(rootElement.Attribute("version")); Assert.Equal("1.0", rootElement.Attribute("version").Value); Assert.Equal("Oxite", rootElement.Descendants(rsdNamespace + "engineName").SingleOrDefault().Value); Assert.Equal("http://oxite.net", rootElement.Descendants(rsdNamespace + "engineLink").SingleOrDefault().Value); Assert.Equal("http://oxite.net/", rootElement.Descendants(rsdNamespace + "homePageLink").SingleOrDefault().Value);
XElement apisElement = rootElement.Descendants(rsdNamespace + "apis").SingleOrDefault(); Assert.NotNull(apisElement); Assert.Equal(1, apisElement.Elements().Count());
XElement apiElement = apisElement.Elements().SingleOrDefault(); Assert.NotNull(apiElement); Assert.Equal(rsdNamespace + "api", apiElement.Name); Assert.Equal("MetaWeblog", apiElement.Attribute("name").Value); Assert.Equal(areaService.StoredAreas["test"].ID.ToString("N"), apiElement.Attribute("blogID").Value); Assert.Equal("true", apiElement.Attribute("preferred").Value); Assert.Equal("http://oxite.net/MetaWeblog.svc", apiElement.Attribute("apiLink").Value); }
http://oxite.codeplex.com/SourceControl/changeset/view/54721#419183
[Story] public void Should_find_customers_by_name_when_name_matches() { Story story = new Story("List customers by name"); story.AsA("customer support staff") .IWant("to search for customers in a very flexible manner") .SoThat("I can find a customer record and provide meaningful support");
CustomerRepository repo = null; Customer customer = null; story.WithScenario("Find by name") .Given("a set of valid customers", delegate { repo = CreateDummyRepo(); }) .When("I ask for an existing name", "Joe Schmoe", delegate(string name) { customer = repo.FindByName(name); }) .Then("the correct customer is found and returned", delegate {Assert.That(customer.Name, Is.EqualTo("Joe Schmoe"));}); }
http://grabbagoft.blogspot.com/2007/09/authoring-stories-with-nbehave-03.html
http://www.flickr.com/photos/gagilas/2659695352/
RECORD AND PLAYBACK
You can make C# readable
But it’s hard
RUBY?
http://www.flickr.com/photos/buro9/298994863/
Natural Language
http://www.flickr.com/photos/mag3737/1914076277/
RSpec
http://www.flickr.com/photos/dodgsun/467076780/
Behaviour Driven Development
Intent
[TestMethod]public void EditAction_Retrieves_Dinner_1_From_Repo_And_Countries_And_Sets_DinnerViewModel() { // Arrange var controller = CreateDinnersControllerAs("someuser");
// Act ViewResult result = controller.Edit(1) as ViewResult;
// Assert DinnerFormViewModel model = result.ViewData.Model as DinnerFormViewModel; Assert.AreEqual(13, model.Countries.Count());}
describe
describe “when editing” do
describe “when editing” do itend
describe “when editing” do it “should return countries where dinners can be hosted”end
D:\SourceControl\nerddinner-23425\specs>ispec DinnersController_specs.rb*
Pending:
when editing should return countries where dinners can be hosted (Not Yet Implemented)
./DinnersController_specs.rb:2
Finished in 0.3511328 seconds
1 example, 0 failures, 1 pending
describe NerdDinner::Controllers::DinnersController, “when editing” do
require ‘NerdDinner.dll’describe NerdDinner::Controllers::DinnersController, “when
editing” do
$: << ‘../NerdDinner/bin’require ‘NerdDinner.dll’describe NerdDinner::Controllers::DinnersController, “when
editing” do
$: << ‘../NerdDinner/bin’require ‘NerdDinner.dll’Include NerdDinner::Controllers
describe DinnersController, “when editing” do
it “returns countries where dinners can be hosted” do controller = DinnersController.newend
it “returns countries where dinners can be hosted” do controller = DinnersController.new(dinner_repos(dinners))end
it “returns countries where dinners can be hosted” do controller = DinnersController.new(dinner_repos(dinners)) result = controller.Edit(1).ViewData.Modelend
it “returns countries where dinners can be hosted” do controller = DinnersController.new(dinner_repos(dinners)) result = controller.Edit(1).ViewData.Model result.Countries.Count().should == test_data.lengthend
RSpec has really powerful matchers
D:\SourceControl\nerddinner-23425\specs>ispec DinnersController_specs.rbF
1)'NerdDinner::Controllers::DinnersController when editing should return
countries where dinners can be hosted' FAILEDexpected: 13, got: nil (using ==)./DinnersController_specs.rb:8:Finished in 0.4824219 seconds
1 example, 1 failure
D:\SourceControl\nerddinner-23425\specs>ispec DinnersController_specs.rb.
Finished in 0.4355469 seconds
1 example, 0 failures
require ‘caricature’
def dinner_repos(test_data) IDinnerRepository.isolate(:FindUpcomingDinners) {returns test_data} End
def create_dinners(count=13) dinners = [] count.times do |i| dinners << Dinner.new(:country => “Value#{i}”) endend
describe DinnersController, "when editing" do let(:dinners) {create_dinners} let(:controller) {DinnersController.new(dinner_repos dinners)} it "returns countries where dinners can be hosted" do result = controller.Edit(dinners.first.id).view_model result.Countries.Count().should == dinners.length endend
result.Countries.Count().should == dinners.length result.Countries.should have_same_count(dinners)
module Matchers class CountEqual def initialize(expected) @expected = expected end def matches?(actual) actual.Count() == @expected.Count() end end def have_same_count(expected) CountEqual.new(expected) endend
Duck Typing FTW!
describe DinnersController, “Managing dinner reservations” do let(:dinners) { valid_dinners } let(:controller) {DinnersController.new(dinner_repository dinners)}
describe “when editing“ it_should_behave_like “valid dinners” it "returns countries where dinners can be hosted"
end
describe “when saving“ do describe “the validation for invalid dinners” do let(:dinners) { bad_dinners(1) }
it “should reject a dinner without a name” it “should reject a dinner without a email address” it “should accept a dinner if it has a name and email address” end describe “confirmation” do it “should send an email to the organiser once saved” end describe “valid dinners” do it “redirects to thank you page after completing" end endend
describe "NHibernate" do before do config = Configuration.new @cfg = config.configure(File.join(Dir.pwd, "nhibernate.config")) end
it "can create session factory" do session_factory = @cfg.BuildSessionFactory() session_factory.should_not be_nil end
it "can create session" do session_factory = @cfg.BuildSessionFactory() session = session_factory.open_session session.should_not be_nil endend
Outside-in Development
Cucumber
http://www.flickr.com/photos/vizzzual-dot-com/2738586453/
Documentation, automated tests and development-aid
[Story] public void Should_find_customers_by_name_when_name_matches() {
Story story = new Story("List customers by name"); story.AsA("customer support staff")
.IWant("to search for customers in a very flexible manner")
.SoThat("I can find a customer record and provide meaningful support");
CustomerRepository repo = null; Customer customer = null; story.WithScenario("Find by name")
.Given("a set of valid customers", delegate { repo = CreateDummyRepo(); })
.When("I ask for an existing name", "Joe Schmoe", delegate(string name) { customer = repo.FindByName(name); })
.Then("the correct customer is found and returned", delegate {Assert.That(customer.Name, Is.EqualTo("Joe Schmoe"));});
}
Feature: List customers by name As a customer support staff I want to search for customers in a very flexible manner So that I can find a customer record and provide meaningful support
Scenario: Find by name Given a set of valid customers When I ask for an existing name Then the correct customer is found and returned
Feature: List customers by name As a customer support staff I want to search for customers in a very flexible manner So that I can find a customer record and provide meaningful support
Scenario: Find by name Given a set of valid customers When I ask for an existing name Then the correct customer is found and returned
D:\SourceControl\nerddinner-23425\features>cucumber list.featureFeature: List customers by name As a customer support staff I want to search for customers in a very flexible manner So that I can find a customer record and provide meaningful support
Scenario: Find by name # list.feature:6 Given a set of valid customers # list.feature:7 When I ask for an existing name # list.feature:8 Then the correct customer is found and returned # list.feature:9
1 scenario (1 undefined)3 steps (3 undefined)0m0.020s
You can implement step definitions for undefined steps with these snippets:
Given /^a set of valid customers$/ do pending # express the regexp above with the code you wish you hadend
When /^I ask for an existing name$/ do pending # express the regexp above with the code you wish you hadend
Then /^the correct customer is found and returned$/ do pending # express the regexp above with the code you wish you hadend
Given /^a set of valid customers$/ do @repo = CreateDummyRepo()end
When /^I ask for an existing name$/ do @customer = @repo.FindByName("Joe Schmoe")end
Then /^the correct customer is found and returned$/ do @customer.Name.should == "Joe Schmoe“end
NOT BEST PRACTICE!!
Given /^customer “([^\"]*)” $/ do |name| @repo = CustomerRepository.new(Customer.new(:name => name)end
When /^I search for customer “([^\"]*)”$/ do |name| @customer = @repo.FindByName(name)end
Then /^”([^\"]*)” should be found and returned$/ do |name| @customer.Name.should == nameend
WebRat
http://www.flickr.com/photos/whatwhat/22624256/
visitclick_link
fill_inclick_button
check and uncheckchooseselect
attach_file
EXAMPLESCucumber, WebRat and Automated UI testing
One more thing...
Meerkatalyst
http://blog.benhall.me.uk/2009/12/sneak-peek-at-meerkatalystlonestar.html
http://www.flickr.com/photos/leon_homan/2856628778/
Expressing intent
Ruby -> C#
http://www.flickr.com/photos/philliecasablanca/2456840986/
using Cuke4Nuke.Framework;using NUnit.Framework;using WatiN.Core;namespace Google.StepDefinitions{ public class SearchSteps { Browser _browser; [Before] public void SetUp() { _browser = new WatiN.Core.IE(); } [After] public void TearDown() { if (_browser != null) { _browser.Dispose(); } } [When(@"^(?:I'm on|I go to) the search page$")] public void GoToSearchPage() { _browser.GoTo("http://www.google.com/"); }
[When("^I search for \"(.*)\"$")] public void SearchFor(string query) { _browser.TextField(Find.ByName("q")).TypeText(query); _browser.Button(Find.ByName("btnG")).Click(); } [Then("^I should be on the search page$")] public void IsOnSearchPage() { Assert.That(_browser.Title == "Google"); } [Then("^I should see \"(.*)\" in the results$")] public void ResultsContain(string expectedResult) { Assert.That(_browser.ContainsText(expectedResult)); } }}
Given /^(?:I'm on|I go to) the search page$/ do visit 'http://www.google.com'end When /^I search for "([^\"]*)"$/ do |query| fill_in 'q', :with => query click_button 'Google Search'end Then /^I should be on the search page$/ do dom.search('title').should == "Google"end Then /^I should see \"(.*)\" in the results$/ do |text| response.should contain(text)end
Software
• Recommended:– IronRuby– Ruby– Cucumber– Rspec– WebRat– mechanize– Selenium RC– selenium-client– Caricature– activerecord-sqlserver-
adapter
• Optional:– XSP \ Mono– JetBrain’s RubyMine– JRuby – Capybara– Celerity– Active record – active-record-model-
generator– Faker– Guid
Useful Links• http://www.github.com/BenHall• http://blog.benhall.me.uk• http://stevehodgkiss.com/2009/11/14/using-activerecord-migrator-
standalone-with-sqlite-and-sqlserver-on-windows.html• http://www.testingaspnet.com• http://• http://msdn.microsoft.com/en-us/magazine/dd434651.aspx• http://msdn.microsoft.com/en-us/magazine/dd453038.aspx• http://www.cukes.info
Getting SQL Server to work
1. gem install activerecord-sqlserver-adapter1. Download dbi-0.2.2.zip 2. Extract dbd\ADO.rb to ruby\site_ruby\1.8\DBD\
ADO.rb