Introducing FSpec

Published on June 06, 2014

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

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:

Example FSpec 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.

Previous: Comparing FSpec to NSpec