Wednesday, March 16, 2011

Solving Kata 16 - Business Rules using BDD - Part 1

The Business Rule coding Kata (Kata Sixteen) deals with a set of changing business rules with an example of writing an order processing application for a large company. The focus is on the payments and in particular on the processing required when a payment was received by the company. The Kata specifies a full set of rule snippets like:
·         If the payment is for a physical product, generate a packing slip for shipping.
·         If the payment is for a book, create a duplicate packing slip for the royalty department.
·         If the payment is for a membership, activate that membership.
·         If the payment is an upgrade to a membership, apply the upgrade.
·         If the payment is for a membership or upgrade, e-mail the owner and inform them of the activation/upgrade.
·         If the payment is for the video "Learning to Ski," add a free "First Aid" video to the packing slip (the result of a court decision in 1997).
·         If the payment is for a physical product or a book, generate a commission payment to the agent.
·         and so on, and so on, for seven long, yellow pages.
We’ll try to solve these rule sets one by one and do this using BDD. I’ll be using the StoryQ framework for writing my BDD test cases.
Part 1: If the payment is for a physical product, generate a packing slip for shipping.
I have created my test case as given below.
[TestMethod]
public void PaymentForAPhysicalProductGeneratesAPackagingSlipForShippingTest()
{
    new Story("payment is for a physical product, generate a packing slip for shipping")
    .InOrderTo("Ship products a packaging slip should be generated")
    .AsA("User")
    .IWant("Packaging slip for shipping my products")
    .WithScenario("Payment for physical product")
    .Given(ProductIsReadyForPayment)
    .When(PaymentIsDoneForProduct)
    .Then(ShouldAlsoGenerateAShippingSlipForTheProduct)
    .ExecuteWithReport(MethodBase.GetCurrentMethod());
}
Now to implement the Given, When, Then scenarios, I’ve use refactoring to generate the methods and variables. My test methods look like.
private void ShouldAlsoGenerateAShippingSlipForTheProduct()
{
    _payment.CompletePayment(PaymentAmount);
    _shippingServiceMock.Verify((x) => x.GenerateShippingSlipForAddress(_physicalProduct.GetShippingAddress()));
}

private void PaymentIsDoneForProduct()
{
    _payment = new Payment(_physicalProduct, ShippingAddress);
    _shippingServiceMock = new Mock<IShippingSlipService>();

    _payment.SetShippingService(_shippingServiceMock.Object);
}

private void ProductIsReadyForPayment()
{
    _physicalProduct = new Product("Blackberry Storm");
    var physicalProduct = _physicalProduct;
    const string shippingAddress = "iSense Bangalore";
    physicalProduct.SetShippingAddress(shippingAddress);
}

I have mocked the IShippingSlipService and set the expectations on the GenerateShippingSlipForAddress method on the service.
Code for the implementation
public class Product
{
    private string _shippingAddress;
    private string _name;
    private readonly string _productCode;

    public Product(string name)
    {
        this._name = name;
        _productCode = Guid.NewGuid().ToString("N");
    }

    public void SetShippingAddress(string shippingAddress)
    {
        this._shippingAddress = shippingAddress;
    }

    public string GetShippingAddress()
    {
        return _shippingAddress;
    }

    public string GetProductCode()
    {
        return _productCode;
    }
}

public class Payment
{
    private readonly Product _physicalProduct;
    private decimal _amount;
    private IShippingSlipService _shippingSlipService;
    private string _shippingAddress;

    public Payment(Product physicalProduct, string shippingAddress)
    {
        if (physicalProduct == null) throw new ArgumentNullException("physicalProduct");
        if (shippingAddress == null) throw new ArgumentNullException("shippingAddress");

        this._physicalProduct = physicalProduct;
        this._shippingAddress = shippingAddress;
    }

    public virtual void CompletePayment(decimal amount)
    {
        _amount = amount;
        if (_physicalProduct != null) _shippingSlipService.GenerateShippingSlipForAddress(_physicalProduct.GetShippingAddress());
    }

    public void SetShippingService(IShippingSlipService shippingSlipService)
    {
        _shippingSlipService = shippingSlipService;
    }
}

public interface IShippingSlipService
{
    void GenerateShippingSlipForAddress(string shippingAddress);
}

Results

2 comments:

Gopi Krishna said...

Hi Prajeesh,

Iam having a webpage with fields.
Name,Age,Salary.

Now I want to apply business rule to my application.

How to do that?

If possible , send me a sample code...to my mail: gopikrsna.ch@gmail.com

Prajeesh said...

Hi Gopi,
You should have your business rules in the domain model. The webpage/ view should be used only to render content to the user and get input from the user.