So as I’ve been doing some Ruby+Rails work recently, I’ve become very enamoured of Rspec, it’s ability to turn plain text tests into runable tests is awesome.
I love that I can run this as an automated test: Story: News Page As a visitorI Want to go to the news site
So that I can keep up with all the latest news Scenario: A visitor goes to the news site and one post exists
Given a news_post named ‘Test News Post’ exists with content ‘This is a news post created for the scenario’
When visitor goes to the home page
And clicks on ‘News’ Then visitor should see the news show page
And page should include a notice ‘Test News Post’
And page should have the news post’s name, and content it’s really awesome to see this kind of feature, and makes testing in C# pale in comparison… however, there is also the syntax of RSpec that I’m getting used to that allows you to write specifications that look like this:
require File.dirname(__FILE__) '/../spec_helper'describe User, "A new user" do before(:each) do @user = User.new end it "should be able to retain email address" do email = "email" @user.email = email @user.email.should be_equal(email) end it "should be able to retain password" do password = "password" @user.password = password @user.password.should be_equal(password) end it "should not be valid for save" do @user.should_not be_valid end it "should require an email address" do login = "login" password = "password" @user.password = password @user.password_confirmation = password @user.login = login @user.should_not be_valid end it "should require a password" do login = "login" email = "email" password_confirm = "password" @user.email = email @user.password_confirmation = password_confirm @user.login = login @user.should_not be_valid end it "should require a password confirmation" do login = "login" email = "email" password = "password" @user.email = email @user.login = login @user.password = password @user.should_not be_valid end it "should require a login" do email = "email" password = "password" @user.email = email @user.password = password @user.password_confirmation = password @user.should_not be_valid end it "should be valid if login, email password and password confirmation are provided" do login = "login" email = "email" password = "password" @user.password = password @user.password_confirmation = password @user.login=login @user.email = email @user.should be_valid end it "should only be valid if password and password confirmation match" do login = "login" email = "email" password = "password" @user.password = password @user.password_confirmation = password "not the same" @user.login=login @user.email = email @user.should_not be_valid end it "should not be valid if password and password confirmation are under 4 characters" do login = "login" email = "email" password = "pas" @user.password = password @user.password_confirmation = password @user.login=login @user.email = email @user.should_not be_valid end it "shouldn't be valid if the same login name and email exist already" do login = "login" email = "email" password = "password" @user.password = password @user.password_confirmation = password @user.login=login @user.email = email @user.save @user = User.new @user.password = password @user.password_confirmation = password @user.login=login @user.email = email @user.should_not be_valid end after(:all) do User.delete_all endenddescribe User, "A saved user" do before(:all) do @initial_password = "password" login = "login" user = User.new(:login => login,:password=>@initial_password,:password_confirmation=>@initial_password,:email=>"email") user.save @user = User.find_by_login(login) end it "should encrypt the password" do @user.password.should_not equal(@initial_password) end after(:all) do @user.destroy endend
which gives you an output like this:
User A saved user- should encrypt the password User A new user
- shouldn’t be valid if the same login name and email exist already
- should not be valid if password and password confirmation are under 4 characters
- should only be valid if password and password confirmation match
- should be valid if login, email password and password confirmation are provided
- should require a login
- should require a password confirmation
- should require a password
- should require an email address
- should not be valid for save
- should be able to retain password
- should be able to retain email address Finished in 0.594 seconds 12 examples, 0 failures It would be great to have this in my C# world as well, but hey I’ve got to work within the boundaries of NUnit, due to the fact that there are other members on the team too and they aren’t as grounded in BDD and TDD as I am. There is one thing however that I did want to add, and that’s syntax that makes people get away from the idea that Unit tests always have to be about specifying code, and can be more about specifying the behaviour. I went to work on NSpecify, as the NUnit add-on is easy and seamless to install, everyone on my team can run it, as it’s in with their code under source control (this allows Continuous integration without too many hassles of versioning and setup etc.). As such I modified NSpecify to add in [Context] to define a “test case” [BeforeAll] to define a “TestFixtureSetUp”, [BeforeEach] to define a “SetUp” [AfterAll] to define a “TestFixtureTearDown” and [AfterEach] to define a “TearDown”. These are all synonyms so they can be used interchangeably with the existing attributes. Also I added the ability to have more complex collection matching. want to specify that a collection of users has a user with a certain Name attribute? easy:
Specify.That(UserCollection).Must.Contain((User user)=>user.Name==expectedName);
Also I added the ability to define expected exceptions on Method calls, but based on Lambdas (as the above is)
Specify.That(() => { MethodThatThrowsACustomException(); }).Must.Be.Disallowed(typeof(CustomException)).WithMessage(%u201CThis method is disallowed in this context%u201D);
the with message is optional, without it the specification will just match the type of exception without much more.
Next I’m going to put in some negative collection matching like above, and also add in the NUnit.Spec extension methods, but make them throw specification exceptions, as just using them out of the box means that the test shows green in NUnit even thought there’s an exception thrown. So at the moment I can do this:[Context]public class SaveCustomerFunctionalityWithAlternativeSyntax{ private Customer customer = null; [BeforeAll] public void FunctionalitySetup() { } [BeforeEach] public void SpecificationSetup() { customer = new Customer(123); } [Specification("The customer id's should be equal")] public void LoadCustomer() { Specify.That(customer.Id).Must.Equal(123, "The customer id's aren't equal"); } [Specification("The customer id's should be equal")] public void LoadCustomerFailed() { Specify.That(customer.Id).Must.Not.Equal(125, "The customer id's aren't equal"); } [AfterEach] public void SpecificationCleanup() { customer = null; } [AfterAll] public void FunctionalityCleanup() { }}
but I want to get to the point where I can do this
[Context]public class WhenANewUserIsCreated{ User user; [BeforeEach] public void CreateTheNewUser() { user = new User(); } [Specification] public void TheUserShouldBeMarkedAsNew() { user.IsNew.Must.Be.True(); } [Specification] public void TheUserShouldNotBeAllowedToBeSaved() { user.Must.Not.Allow(user.Save).With.Error("A user must have a name before being saved"); }}
Attached is the patch:
Context+MethodDissalowed+ComplexMatchingDelegates.patch Just be aware of a couple of things- I’ve updated NUnit to 2.4.6
- The lambda expressions above rely on .net 3.5 so VS2008 is a must.
Enjoy