Over the last couple of months, I have been woking on FSpec, a test framework
for .NET, heavily inspired by RSpec.
FSpec at GitHub (More comprehensive documentation found there)
There already exist a couple of test frameworks for the .NET platform, taking a
similar approach to testing as RSpec – I am aware of NSpec and MSpec. But where
RSpec (and Ruby in general) excels in creating beautiful DSLs, I feel that
NSpec and MSpec have not accomplished this.
FSpec takes a different approach. It is written in and for F#, i.e. the test
code is written in F#. But the system under test can be implemented in any
language targeting the .NET framework.
My original intention was to create a test framework with a cleaner syntax for
my C# projects.
Try to see how FSpec compares to other test frameworks
- Comparing FSpec to NSpec
- Comparing FSpec to MSpec (coming soon)
FSpec became self-testing in its 14th commit! I.e. although the form of FSpec
have changed significantly since then, the unit tests for FSpec itself have
been executed by FSpec’s own run function since it’s 14th commit.
If you have a development team working with a different language on the .NET
platform, FSpec could be a great low-risk way of introducing F#.
Features
Currently the following features are supported
- Nested example groups/contexts
- Setup/teardown in example groups
- Test context for storing state (as you don’t have a test class with mutable
fields) - Metadata on individual examples or example groups
- Assertion framework (but you can use anything, e.g. unquote)
- Implicit subject
- One-liner examples verifying against implicit subject.
- Automatically disposing IDisposable instances in context
- Pending examples
General Syntax
Lets walk through an example
let specs = describe "CreateUserFeature" [ before (fun ctx -> // Retrieve email and existing user from example metadata. let email = ctx.metadata?email let userMock = Mock<IUserRepository>() .Setup(fun x -> <@ x.FindByEmail(email) @>) .Returns(ctx.metadata?existing_user) .Create() let input = CreateUserInput ( Email = email ) // The Inject and CreateUser functions are not part of FSpec // but specific tests can easily add extensions to the context ctx.Inject userMock ctx.CreateUser input ) // Setup a collection of metadata. These values will be valid for // all examples in that context. // The operators are a bit strange, improvement suggestions are // welcome ("email" ++ "john.doe@example.com" ||| "existing_user" ++ null) ==> context "when a user doesn't exist" [ it "creates a new user" (fun ctx -> let repo = ctx.GetMock<IUserRepository>() let expectedUser = fun (u:User) -> u.Email = "john.doe@example.com" verify <@ repo.Save(It.is(expectedUser)) @> once ) it "reports success" (fun ctx -> let callback = ctx.GetMock<ICreateUserCallback>() verify <@ callback.UserCreated() @> once ) // We could rely on the email still being setup according to // the meta data specified on the context. By I like that the // data that is important for the outcome of a specific test // is very visible in that test. ("email" ++ "jane.doe@example.com") ==> it "sends an email to the user" (fun ctx -> ctx.verifyMailSentToUser "jane.doe@example.com" ) it "sends only one mail" (fun ctx -> let mailer = ctx.GetMock<IMailer>() verify <@ mailer.SendMailMessage(any()) @> once ) context "password encryption" [ // Pending examples will show up in the test output clearly // marked as "PENDING", but will not fail the test it "creates an encrypted password and salt" pending it "creates a unique password and salt each time" pending it "can validate the password after creation" pending ] ] ("email" ++ "john.doe@example.com" ||| "existing_user" ++ (create_a_user())) ==> context "when the email is already registered" [ it "does not create a new user" (fun ctx -> let userRepository = ctx.GetMock<IUserRepository>() verify <@ userRepository.Save(any()) @> never ) it "reports duplicate user" (fun ctx -> let callback = ctx.GetMock<ICreateUserCallback>() verify <@ callback.UserAlreadyExists() @> once ) ] ]
Running the specs result in the following output:
One thing worthy of note if how meta data applied to a specific example, or
group of examples can be accessed in the general setup, resulting in less code
duplication than other test frameworks on the .NET platform that I’m familiar
with.
This post is just meant as an introduction to FSpec. Over time, I will try to
write more articles describing how to use the tool.
There is better documentation of the tool in the readme on the github
page, so I invite everybody to check it out.
Pingback: F# Weekly #23, 2014 | Sergey Tihon's Blog()