Skip to content

Core Concepts

Robert Coltheart edited this page May 11, 2020 · 7 revisions

MSpec is called a "context/specification" test framework because of the "grammar" that is used in describing and coding the tests or "specs". That grammar reads roughly like this

When the system is in such a state, and a certain action occurs, it should do such-and-such or be in some end state.

You should be able to see the components of the traditional Arrange-Act-Assert model in there. To support readability and remove as much "noise" as possible, MSpec eschews the traditional attribute-on-method model of test construction. It instead uses custom .NET delegates that you assign anonymous methods and asks you to name them following a certain convention.

Getting started

Use the MSpec namespace to bring in the types needed for your specifications:

using Machine.Specifications;

Subject

The Subject attribute is the first part of a spec class. It describes the "context", which can be the literal Type under test or a broader description. The subject is not required, but it is good practice to add it. Also, the attribute allows ReSharper / Rider to detect context classes such that delegate members will not be regarded as unused.

The class naming convention is to use Sentence_snake_case and to start with the word "When".

[Subject("Authentication")]                           // a description
[Subject(typeof(SecurityService))]                    // the type under test
[Subject(typeof(SecurityService), "Authentication")]  // or a combo!
class When_authenticating_a_user { ... }              // remember: you can only use one Subject Attribute!

Tags

The Tags attribute is used to organize your spec classes for inclusion or exclusion in test runs. You can identify tests that hit the database by tagging them "Slow" or tests for special reports by tagging them "AcceptanceTest".

Tags can be used to [include or exclude certain contexts during a spec run, see Command-line.

[Tags("RegressionTest")]  // this attribute supports any number of tags via a params string[] argument!
[Subject(typeof(SecurityService), "Authentication")]
class When_authenticating_a_user { ... }

Establish

The Establish delegate is the "Arrange" part of the spec class. The Establish will only run once, so your assertions should not mutate any state.

[Subject("Authentication")]
class When_authenticating_a_new_user
{
    static SecurityService subject;

    Establish context = () =>
    {
        // ... any mocking, stubbing, or other setup ...
        subject = new SecurityService(foo, bar);
    };
}

Establish can be also used with inheritance and nested classes for even greater control, see Inheritance.

Cleanup

The pair to Establish is Cleanup, which is also called once after all of the specs have been run.

[Subject("Authentication")]
class When_authenticating_a_user
{
    static SecurityService subject;

    Establish context = () =>
        subject = new SecurityService(foo, bar);

    Cleanup after = () =>
        subject.Dispose();
}

Because

The Because delegate is the "Act" part of the spec class. It should be the single action for this context, the only part that mutates state, against which all of the assertions can be made. Most Because statements are only one line, which allows you to leave off the squiggly brackets!

[Subject("Authentication")]
class When_authenticating_a_user
{
    static SecurityService subject;

    Establish context = () =>
        subject = new SecurityService(foo, bar);

    Because of = () =>
        subject.Authenticate("username", "password");
}

If you have a multi-line Because statement, you probably need to identify which of those lines are actually setup and move them into the Establish. Alternatively your spec may be concerned with too many contexts and needs to be split, or the subject-under-test needs to be refactored.

It

The It delegate is the "Assert" part of the spec class. It may appear one or more times in your spec class. Each statement should contain a single assertion, so that the intent and failure reporting is crystal clear. Like Because statements, It statements are usually one-liners and do not need to have squiggly brackets.

[Subject("Authentication")]
class When_authenticating_an_admin_user
{
    static SecurityService subject;
    static UserToken user_token;

    Establish context = () =>
        subject = new SecurityService(foo, bar);

    Because of = () =>
        user_token = subject.Authenticate("username", "password");

    It should_indicate_the_users_role = () =>
        user_token.Role.ShouldEqual(Roles.Admin);

    It should_have_a_unique_session_id = () =>
        user_token.SessionId.ShouldNotBeNull();
}

An It statement without an assignment will be reported by the test runner in the "Not implemented" state. You may find that "stubbing" your assertions like this helps you practice TDD.

It should_list_your_authorized_actions;

Assertion

As you can see above, the It assertions make use of (ShouldEqual, ShouldNotBeNull) Should extension methods. They encourage readability and a good flow to your assertions when read aloud or on paper. You should use them wherever possible, just "dot" off of your object and browse the IntelliSense!

It's good practice to make your own Should assertion extension methods for complicated custom objects or domain concepts.

Ignore

Every test framework lets you ignore incomplete or failing specs. MSpec provides the Ignore attribute for just that. Just leave a note describing the reason that you ignored this spec.

[Ignore("We are switching out the session ID factory for a better implementation")]
It should_have_a_unique_session_id = () =>
    user_token.SessionId.ShouldNotBeNull();

Catch

When testing that exceptions are thrown from the "action" you should use a Catch statement. This prevents thrown exceptions from escaping the spec and failing the test run. You can inspect the exception's expected properties in your assertions.

[Subject("Authentication")]
class When_authenticating_a_user_fails_due_to_bad_credentials
{
    static SecurityService subject;
    static Exception exception;

    Establish context = () =>
        subject = new SecurityService(foo, bar);

    Because of = () =>
        exception = Catch.Exception(() => subject.Authenticate("username", "password"));

    It should_fail = () =>
        exception.ShouldBeOfExactType<AuthenticationFailedException>();

    It should_have_a_specific_reason = () =>
        exception.Message.ShouldContain("credentials");
}