tags:

views:

1400

answers:

1

Hi guys,

Just wondering if any MSpec and BDDers out there could give me there thoughts on my first attempt at writing a MSpec spec. Now I've left the specs uncoded, but my context has code in it, I just want to know if I'm heading along the right lines, or if there are any improvements to be made with what I've done.

As this can be subjective, I've marked it community wiki.

Right, first, here is my story and first scenario:

Story: "Blog admin logs in to the system"

As a blog writer
I want to be able to log in to my blog
So that I can write posts and administrate my blog

Scenario: "Logs in from the login page"

Given the user enters in correct credentials for a user in the system
When the user clicks the "Login" button
Then log the user in and redirect to the admin panel with a message 
stating that he logged in correctly

and heres my MSpec code:

using System;
using Machine.Specifications;
using Machine.Specifications.Model;
using Moq;
using MyBlog.Controllers;
using System.Web.Mvc;

using MoqIt = Moq.It;
using ThenIt = Machine.Specifications.It;


[Subject("User tries logging in")]
public class When_user_enters_valid_credentials : With_user_existing_in_membership
{
    protected static ActionResult result;

    Because of = () =>
    {
        result = loginController.Login(validUsername, validPassword);
    };

    ThenIt should_log_the_user_in;
    ThenIt should_redirect_the_user_to_the_admin_panel;
    ThenIt should_show_message_confirming_successful_login;
}

public abstract class With_user_existing_in_membership
{
    protected static Mock<ISiteMembership> membershipMock;
    protected static string validUsername;
    protected static string validPassword;
    protected static LoginController loginController;

    Establish context =()=>
    {
        membershipMock = new Mock<ISiteMembership>();
        validUsername = "ValidUsername";
        validPassword = "ValidPassword";
        //make sure it's treated as valid usernames and password
        membershipMock.Setup<bool>(m => m.Validate(MoqIt.Is<string>(s => s == validUsername), MoqIt.Is<string>(s => s == validPassword)))
                      .Returns(true);
        loginController = new LoginController(membershipMock.Object);
    };

}

Now I've had to do the aliasing of the It class and delegate because of Moq and MSpec conflicting, but I hope it still reads well.

Be happy to hear you thoughts.

+12  A: 

The context looks good. A couple of points, though:

  • The ActionResult can be made private
  • I would refrain from using a "With" base class as it seems unnecessary in the example you gave.
    • "Withs" often lead to a separation of the Arrange and Act phases (as known from general TDD), especially if the "Withs" form a class hierarchy.
    • That said, I mainly use "With" base classes as a container and perform only general initialization there, like setting up mocked dependencies. However, strive to not stub behavior in the "Withs".
    • Becauses in the hierarchy, though not inherited because they're private, are called by MSpec in the right order. This makes the separation of general and context-specific initialization possible.
  • I would not put context-specific information in the base class. In your example the valid username/password combination is context-specific, because, well, that's what your context is named after.
    • It makes it easier to have a second context later that inherits from the same base class but tests the behavior with invalid login credentials.
  • Naming is key (and complicated). You should strive to put no names of systems under test in context class names. This makes your contexts more friendly to refactorings of the system under test.

I like the way you solved the conflicting "Its" with aliases. However, I would argue that the Moq alias can be made more readable. Consider something sentence-like, f.e. Param or Value, unless it does not conflict with other Moq types (I'm no Moq expert).

With that in mind, I suggest writing:

// The "Subject" attribute is used as the "Scenario" in
// in your original story, and gets rendered with your
// MSpec report that is outputted (especially the HTML version).
[Subject("Login Page")]
public class When_user_enters_valid_credentials_for_existing_user 
    : MembershipContext 
{
    static ActionResult result;
    static string username;
    static string password;

    // This is optional, but will inherit the MembershipContext, and 
    // allow for more specific setup of your context for these 
    // grouped specs only.
    Establish context = () =>
    {
        username = "ValidUsername";
        password = "ValidPassword";

        // This is context-specific: Username/password is valid
        membership.Setup<bool>(
            m => m.Validate(
                Param.Is<string>(s => s == username)
                ,Param.Is<string>(s => s == password)
            )
        ).Returns(true);
    };

    Because of = () =>
    {
        result = loginController.Login(username, password);
    };

    It should_log_the_user_in;
    It should_redirect_the_user_to_the_admin_panel;
    It should_show_message_confirming_successful_login;
}

public abstract class MembershipContext 
{
    protected static Mock<ISiteMembership> membership;
    protected static LoginController loginController;

    Establish context = () =>
    {
        membership = new Mock<ISiteMembership>();
        loginController = new LoginController(membershipMock.Object);
    };
}

The code above is an excellent example of establishing a global context MembershipContext and inheriting it in another custom context specific to that spec (the additional Establish).

MSpec's creator, Aaron Jensen, has reverted from using the "With" syntax all together. Since that context class does not show up for any reports, he advises against spending the time to come up with a meaningful context class name. Instead, just call it MembershipContext. In addition, Aaron highly recommends naming your group to have the entire Given in your scenario. E.g. the 'When_user_enters_valid_credentials_for_existing_user' method name above. This will make your MSpec report much more readable, and puts the entire context together (the Given in your original story). And finally, the MSpec report will show your entire Given of your story where using it as a context name, does not show up in the MSpec reports.

Alexander Groß