« war stories | Main | kicking and screaming »
Friday
15Feb2008

NSpecify => RSpec... well closer anyway

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 visitor
I 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
end

end

describe 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
end
end

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

Technorati Tags: ,,,,

Reader Comments (2)

Firstly, I'd like to thank you for your contributions to the NSpecify project. I really like the Context, BeforeEach, etc synonyms that you've added. The lambda support you've added is also pretty cool.

In the future I plan to introduce extension methods to allow specifying on the object itself. I actually did a prove of concept a while back. The problems I found was that you can't have the following syntax

user.Must.Not.Allow(user.Save).With.Error("A user must have a name before being saved");

but rather as follows

user.Must().Not.Allow(user.Save).With.Error("A user must have a name before being saved");

which I don't mind.

I'm working on a Auto Test library and Notifier application. I have an almost stable version and if you are interested, let me know. With this application I plan to have the same sort of specdoc to be displayed (as in rspec on TextMate). Maybe in the browser, I'll see when I get there...

My only problem at the moment is I'm stuck in .NET 2.0 land in my day job, so my understanding and exploration of 3.5 features suffers at the moment...

March 5, 2008 | Unregistered Commentermaruismarais

Firstly, I'd like to thank you for your contributions to the NSpecify project. I really like the Context, BeforeEach, etc synonyms that you've added. The lambda support you've added is also pretty cool.

In the future I plan to introduce extension methods to allow specifying on the object itself. I actually did a prove of concept a while back. The problems I found was that you can't have the following syntax

user.Must.Not.Allow(user.Save).With.Error("A user must have a name before being saved");

but rather as follows

user.Must().Not.Allow(user.Save).With.Error("A user must have a name before being saved");

which I don't mind.

I'm working on a Auto Test library and Notifier application. I have an almost stable version and if you are interested, let me know. With this application I plan to have the same sort of specdoc to be displayed (as in rspec on TextMate). Maybe in the browser, I'll see when I get there...

My only problem at the moment is I'm stuck in .NET 2.0 land in my day job, so my understanding and exploration of 3.5 features suffers at the moment...

March 5, 2008 | Unregistered Commentermaruismarais

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>