The intent of this entry is to show how to do TDD (Test Driven Development) in F# with the help of a dynamic mock object framework such as RhinoMocks. Along the way, we will explore how to implement an object oriented design in F# with features including namespaces, classes, interfaces, properties, and members. This example includes several best practices, but is by no means production ready. Most specifically, it is missing error handling and validation. In addition, certain patterns and practices have been intentionally ignored in order to make the example a little easier to understand.
This is an implementation of a simplified customer service that might exist in any standard LOB (Line of Business) application. In the end, we have a service that allows the current balance of a customer to be returned with a specified discount applied.
As always, we write the tests first (Note: As is my usual style, the tests are in C# and the code is in F#):
C# Customer Entity Test:
[TestMethod]
public void CanCreateCustomer()
{
FSharpMockExample.Entities.ICustomer customer = new FSharpMockExample.Entities.Customer(1, "ABC, Corp.", 20);
Assert.AreEqual(customer.Id, 1);
Assert.AreEqual(customer.Name, "ABC, Corp.");
}
[TestMethod]
public void CanCalculateBalanceWith10PercentDiscount()
{
FSharpMockExample.Entities.ICustomer customer = new FSharpMockExample.Entities.Customer(1, "ABC, Corp.", 20);
decimal newBalance = customer.CalculateBalanceWithDiscount(.1M);
Assert.AreEqual(newBalance, 18);
}
F# Customer Entity:
Signature File:
#light
namespace FSharpMockExample.Entities
type ICustomer = interface
abstract Id: int with get
abstract Name: string with get
abstract CalculateBalanceWithDiscount: decimal -> decimal
end
type Customer = class
new: int*string*decimal -> Customer
interface ICustomer
end
Source File:
#light
namespace FSharpMockExample.Entities
type ICustomer = interface
abstract Id: int with get
abstract Name: string with get
abstract CalculateBalanceWithDiscount: decimal -> decimal
end
type Customer = class
val id: int
val name: string
val balance: decimal
new(id, name, balance) =
{id = id; name = name; balance = balance}
interface ICustomer with
member this.Id
with get () = this.id
member this.Name
with get () = this.name
member this.CalculateBalanceWithDiscount discount =
this.balance - (discount * this.balance)
end
end
C# Customer Data Access Object Test:
[TestMethod]
public void CanGetCustomerById()
{
FSharpMockExample.Data.ICustomerDao customerDao = new FSharpMockExample.Data.CustomerDao();
FSharpMockExample.Entities.ICustomer customer = customerDao.GetById(1);
Assert.AreEqual(customer.Id, 1);
Assert.AreEqual(customer.Name, "ABC Company");
}
F# Customer Data Access Object:
Signature File:
#light
namespace FSharpMockExample.Data
open FSharpMockExample.Entities
type ICustomerDao = interface
abstract GetById: int -> ICustomer
end
type CustomerDao = class
new: unit -> CustomerDao
interface ICustomerDao
end
Source File:
#light
namespace FSharpMockExample.Data
open FSharpMockExample.Entities
type ICustomerDao = interface
abstract GetById: int -> ICustomer
end
type CustomerDao = class
new()={}
interface ICustomerDao with
member this.GetById id =
new Customer(id, "ABC Company", 20.00M) :> ICustomer
end
C# Customer Service Test:
[TestMethod]
public void CanCalculateBalance()
{
var mocks = new MockRepository();
var customerDao = mocks.CreateMock<FSharpMockExample.Data.ICustomerDao>();
FSharpMockExample.Entities.ICustomer customer = new FSharpMockExample.Entities.Customer(1, "XYZ Company", 50);
using (mocks.Record())
{
Expect.Call(customerDao.GetById(1)).IgnoreArguments().Return(customer);
}
using (mocks.Playback())
{
int customerId = 1;
decimal discount = .1M;
FSharpMockExample.Services.ICustomerService customerService = new FSharpMockExample.Services.CustomerService(customerDao);
var balanceWithDiscount = customerService.CalculateBalaceWithDiscount(customerId, discount);
Assert.AreEqual(balanceWithDiscount, 45);
}
}
F# Customer Service:
Signature File:
#light
namespace FSharpMockExample.Services
open FSharpMockExample.Data
open FSharpMockExample.Entities
type ICustomerService = interface
abstract CalculateBalaceWithDiscount: int*decimal -> decimal
end
type CustomerService = class
new: unit -> CustomerService
new: ICustomerDao -> CustomerService
end
Source File:
#light
namespace FSharpMockExample.Services
open FSharpMockExample.Data
open FSharpMockExample.Entities
type ICustomerService = interface
abstract CalculateBalaceWithDiscount: int*decimal -> decimal
end
type CustomerService = class
val customerDao: ICustomerDao
new (customerDao) =
{customerDao = customerDao}
new () =
{customerDao = new CustomerDao()}
interface ICustomerService with
member this.CalculateBalaceWithDiscount (customerId, discount) =
let customer =
this.customerDao.GetById(customerId)
customer.CalculateBalanceWithDiscount(discount)
end
end
Hopefully, this example will inspire you to go out an give this amazing language a try.