Spec4Net /

Filename Size Date modified Message
src
33 B
1.3 KB
13.5 KB

About Spec4Net

Spec4Net is a small library for writing BDD-style executable specifications in C#/.NET. Is is designed to be easy to use and to not restrict you in any way. It is inspired by ScalaTest's WordSpec trait.

With it you can write code like this

"Given a user service, a user name and a password,".When(() =>
        "the user logs in".Then(() =>
            {
                "a session cookie is generated".When(() =>
                    "the user name exists in the system and the password is correct".In(() => { }));
                "an exception is thrown".When(() =>
                    {
                        "the user name is unknown".In(() => { });
                        "the password is incorrect".In(() => { });
                    });
            }));

"If a user logs in".With(() =>
        "a user name".That(() =>
            {
                "does not exist it".Should(() =>
                    "result in an exception".In(() => { }));
                "exists and a password".That(() =>
                    {
                        "is correct a session cookie".Must(() =>
                            "be generated".In(() => { }));
                        "is incorrect".Should(() =>
                            "result in an exception".In(() => { }));
                    });
            }));

"A stack".When(()=>
    {
        "empty".Should(() =>
            {
                "have not elements".In(() => { });
                "throw an exception when calling peek".In(() => { });
                "throw an exception when calling pop".In(() => { });
            });
        "not empty".Should(() =>
            {
                "return the top element when calling peek".In(() => { });
                "not remove the top element when calling peek".In(() => { });
                "return the top element when calling pop".In(() => { });
                "remove the top element when calling pop".In(() => { });
            });
    });

inside test methods or in your own specification classes. You can have as many specifications inside each method as you like, and the style of writing is entirely up to you.

Download

Download Spec4Net as a nuget package or get the source as a zip file.

Writing Specifications

Verbs

Verbs are used to combine phrases into sentences. However, there are absolutely no rules regarding grammar. In order for Spec4Net to be as non-restrictive as possible it will not validate the structure of sentences.

The verbs are:

  • Should
  • Must
  • With
  • When
  • Then
  • That
  • ShouldNot
  • MustNot
  • Which
  • Can
  • Will
  • Who
  • While
  • And
  • Or
  • In (completes a sentence)

A Simple Specification

To write a simple specification your class should inherit Specification. Each method containing specifications shoulds be annotated with either [SpecificationMethod] or [Feature("<description>")].

class MySpecifications : Specification
{
    [Feature("My feature")] // or [SpecificationMethod]
    void MyScenarios()
    {
        "A stack".When(() =>
            {
                "empty".Should(() =>
                    {
                        "have no elements".In(() => { });
                    });
                "not empty".Should(() =>
                    {
                        "have at least one element".In(() => { });
                    });
            });
    }
}

A Specification can be directly verified

var specs = new MySpecifications();
specs.Verify();

Setup and Cleanup

Every now and then you will need to do some setup and/or cleanup after each sentence. In Spec4Net a sentence is complete when an In-verb is encountered, and everytime this happens the framework will look for methods registered as before- and after-methods.

You can have multiple before- and after-methods in each nested sentence. The order is defined by where in the nested phrase hierarchy they are defined.

Use the static methods Specification.DoBefore() and Specification.DoAfter() to register the methods:

class MySpecifications : Specification
{
    void Before() { }
    void After() { }
    void AnotherBefore() { }

    [SpecificationMethod]
    void MyScenarios()
    {
        "A stack".When(() =>
            {
                DoBefore(Before);
                DoAfter(After);

                "empty".Should(() =>
                    {
                        "have no elements".In(() => { });
                    });
                "not empty".Should(() =>
                    {

                        DoBefore(AnotherBefore);

                        "have at least one element".In(() => { });
                    });
            });

    }
}

This will call the methods Before and After before and after the first In, and the methods AnotherBefore and After again after the second In.

Most of the time using DoBefore and DoAfter is not necessary since every phrase will be called once for every of its nested subphrases. The following example shows this:

"Given 'a' equals 1".And(() =>
    {
        var a = 1;
        "you add 2 then 'a' should equal 3".In(() =>
            {
                a += 2;
                a.ShouldBe(3);
            });
        "you add 7 then 'a' should equal 8".In(() =>
            {
                a += 7;
                a.ShouldBe(8);
            });
    });

As you can see a will be reset to 1 for each phrase nested below. Previously this had to be done using a DoBefore method.

Combining With Other Unit Test Frameworks

The following code show a simple unit test for MS Test. There is both explicit specifications and a verification of another specification class ((new StackSpecifications()).Verify();).

[TestClass]
public class SomeUnitTests
{
    [TestMethod]
    public void SomeScenariosWithBeforeAndAfter()
    {
        "A stack".When(() =>
            {
                "empty".Should(() =>
                    {
                        "have no elements".In(() => { });
                        "throw an exception when calling pop".In(() => { });
                        "throw an exception when calling peek".In(() => { });
                    });
            }
    }

    [TestMethod]
    public void VerifyOtherSpecifications()
    {
        (new StackSpecifications()).Verify();
    }
}

An Working Example

The following code is an example of a specification that can be directly verified and be a unit test for xUnit.

public class StackSpecifications : Specification
{
    [Fact]
    [Feature("Stack features")]
    void StackScenarios()
    {
        "A stack".When(() =>
            {
                Stack<int> stack;
                "empty".Should(() =>
                {
                    stack = new Stack<int>();
                    "have no elements".In(() =>
                        stack.Count().ShouldBe(0));
                    "throw an exception when calling pop".In(() =>
                        An<InvalidOperationException>.ShouldBeThrownBy(() => stack.Pop()));
                    "throw an exception when calling peek".In(() =>
                        An<InvalidOperationException>.ShouldBeThrownBy(() => stack.Peek()));
                });

                "not empty".Should(() =>
                {
                    stack = new Stack<int>(new[] { 1, 2, 3 });
                    "return the top element when calling peek".In(() =>
                        stack.Peek().ShouldBe(3));
                    "not remove the top element when calling peek".In(() =>
                        {
                            stack.Peek();
                            stack.Count().ShouldBe(3);
                        });
                    "return the top element when calling pop".In(() =>
                        stack.Pop().ShouldBe(3));
                    "remove the top element when calling pop".In(() =>
                        {
                            stack.Pop();
                            stack.Count().ShouldBe(2);
                        });
                });
            });

        "Notice".That(() =>
            "you can have multiple specifications inside each method".In(() => { }));
    }
}

As you might notice in the above example every outer phrase is executed once for each inner phrase. This means that you can write specifications and code that follow each other more fluently. In the above example the line stack = new Stack<int>(new[] { 1, 2, 3 }); is called 3 times, one for each inner phrase following it. This yields a more fluent language in your specifications and makes them more readable.

The same functionality is normally written using dedicated Setup and Cleanup/Teardown methods in other test frameworks (including Spec4Net until version 3.0), but that has a tendency to remove setup-functionality from the actual test logic making the tests harder to read and reason about.

You can convince yourself that this is what happens by adding a few breakpoints to the above test and step through it.

Keep Your Specifications Separate

While Spec4Net lets you write specifications and unit tests just as you like we recommend that specifications are kept in a separate class and the unit tests just calls .Verify() on those. This makes it easier to reuse the same specifications for integration- and unit tests.

Verifying Specifications

Using the Build-in Runner

Spec4Net has a build-in console runner located in the Spec4Net.ConsoleRunner namespace. With it you can easily make a project that does not use any unit test framework but still has specifications. This is good for quick validation with a nice output.

Here is an example:

class Program
{
    static void Main(string[] args)
    {
        var runner = new Runner();
        runner.Verify(typeof(StackSpecs).Assembly);
        runner.Verify<StackSpecs>();
    }
}

Here we tell the runner to verify all specifications inside the assembly containing the class StackSpecs before telling the runner to verify the specifications in the particular class StackSpecs.

The runner has four Verify-methods:

  • public void Verify(Specification specification)
  • public void Verify(Type type)
  • public void Verify<T>() where T : Specification, new()
  • public void Verify(Assembly assembly)

The first method takes an instance of a specification. This is needed when you have specifications that have no default constructor, for example if dependencies are injected.

the next two methods take a type as an argument. This type has to inherit from Specification and have a default constructor (parameterless).

The final method scans an entire assembly for classes inheriting Specification and a default constructor, and verifies these specifications.

Direct Verification

As previously shown any specification inheriting Specification can be directly verified by calling

try
{
    var specs = new MySpecifications();
    specs.Verify();
}
catch(SpecificationFailedException sfe)
{
    throw;
}

Assertions

If you write specifications inside another unit test framework you can use the Assert-capabilities of that framework. However, Spec4Net has an Assert-class as well which can be used when writing specifications outside a normal unit test framework. It is located in the Spec4Net.Assertions namespace.

There is a third option: use the extensions provided in Spec4Net to have a more fluent syntax.

// check for equality
"a string".ShouldBe("a string");
1234.5.ShouldNotBe(1234);
someVariable.ShouldBeNull();
someOtherVariable.ShouldNotBeNull();

// check for type assignability
"a string".ShouldBeInstanceOf<string>();
132.ShouldNotBeInstanceOf(typeof(string));

// check other properties of some variable
someArray.Length.ShouldBe(3);
(someArray.Length>5).ShouldBe(false);

// check equality of floats, doubles, decimals and dates using a specified accuracy
2.3333.ShouldApproximate(2.333, 0.001);
2.3333f.ShouldApproximate(2.333f, 0.001f);
2.3333m.ShouldApproximate(2.333m, 0.001m);
firstDate.ShouldApproximate(secondDate, 2.milliseconds());

Expecting Exceptions

Often you write tests where you expect an exception. Spec4Net has two classes for that, An and A. The following shows the usage:

An<InvalidOperationException>.ShouldBeThrownBy(() => { });  
A<DivideByZeroException>.ShouldBeThrownBy(() => { });

Checking Properties of Exceptions

If you need to validate that the thrown exception complies with some given rule, say that the message must be a specific value, use the ThatCompliesWith method. This takes a function that either returns a boolean or nothing at all.

The verification succeeds if

  • the function returns true if it is a Func<TException, bool>
  • the function does not throw an exception if it is a Action<TException>.

Compliance using booleans

An<ArgumentException>
    .ThatCompliesWith(e => e.Message == "my message") // using a function that returns true or false
    .ShouldBeThrownBy(() => myServiceCall());

Compliance using asserts

An<ArgumentException>
    .ThatCompliesWith(e => e.Message.ShouldBe("my message")) // using an action that returns nothing and throws exception only if the assertions fails
    .ShouldBeThrownBy(() => myServiceCall());

Happy testing!