Thursday, April 28, 2005

Tasc Unit Testing Pattern

Often when unit testing you need a concrete class that is a stub for an abstract class you are testing. However, instead of creating a class strictly for the stub purpose you can use your test class and thus have less artifacts to maintain. The class is a [T]est [A]nd [S]tub [C]lass.

Consider an abstract class that generates an id on first access if it has not previously been set:

public abstract class DomainBase
{
private Guid _id;
public Guid Id
{
get
{
if (_id==Guid.Empty)
{
_id=Guid.NewGuid();
}
return _id;
}
set { _id = value }
}
}

The test then becomes:

[TestFixture]
public class DomainBaseTasc : DomainBase
{
[Test]
public void EnsureIdIsGenerated()
{
DomainBaseTasc tasc = new DomainBaseTasc();
Assert.IsTrue(tasc.Id!=Guid.Empty);
}

[Test]
public void EnsureIdIsOnlyGeneratedOnFirstAccess()
{
DomainBaseTasc tasc = new DomainBaseTasc();
Assert.IsTrue(tasc.Id==tasc.Id);
}
}

Wednesday, April 27, 2005

Error Eventing Pattern

C# suffers from a lack of error handling patters for systems with separated layers. Often domain objects contain the business logic needed to identify errors. When an error is identified it often needs to be bubbled to the layer sending messages to the domain objects. This can be done with exceptions; however, the first exception will break the execution and you can only return one error at a time. This can cause the users a great bit of pain, especially if you are developing a web app. Additionally, collection of data for a domain object can span multiple views; therefore, you need to be able to validate only the data you are concerned with and not data that has not yet been entered.

Using events as error notification can solve these issues.

How it works:
A domain object contains an inner class ErrorNotifier. ErrorNotifier contains all the events that correspond to errors concerning the domain object. When the validate method is called on the domain object the business rules are checked, the appropriate error events are fired if errors are found, and any object subscribing to the error events will be notified.

When to use it:
You should use Error Eventing when you are not using a remote domain layer. Additionally, it is helpful when you need to validate only specific rules in a domain object.

Example: Booking a hotel room
Create a Reservation domain object in the domain layer.

public class Reservation
{
public const string GUESTS_INVALID = "Guests Invalid";
public const string CHECK_IN_INVALID = "Check In Invalid";
public const string HOTEL_NAME_INVALID = "Hotel Name Invalid";
private ReservationErrorNotifier _errorNotifier = new ReservationErrorNotifier();
private int _guests;
private DateTime _checkIn;
private string _hotelName;

public ReservationErrorNotifier ErrorNotifier
{
get
{
return _errorNotifier;
}
}

public int Guests
{
get { return _guests; }
set { _guests = value; }
}

public DateTime CheckIn
{
get { return _checkIn; }
set { _checkIn = value; }
}

public string HotelName
{
get { return _hotelName; }
set { _hotelName = value; }
}

public void Validate()
{
_errorNotifier.Validate(this);
}

public class ReservationErrorNotifier
{
public event ErrorEventHandler GuestsInvalid;
public event ErrorEventHandler CheckInInvalid;
public event ErrorEventHandler HotelNameInvalid;

public void Validate(Reservation reservation)
{
if (reservation.Guests <>Page 1 of your search is only going to collect check in date and number of guests. Therefore, the service layer contains a Search command that only accepts and validates check in date and guests
public class SearchCommand : Command
{
private int _guests;
private DateTime _checkIn;

public SearchCommand(int guests, DateTime checkIn)
{
_guests = guests;
_checkIn = checkIn;
}

public void Execute()
{
Reservation reservation = new Reservation();
reservation.ErrorNotifier.GuestsInvalid += delegate { this.ErrorList.Add(Reservation.GUESTS_INVALID); };
reservation.ErrorNotifier.CheckInInvalid += delegate { this.ErrorList.Add(Reservation.CHECK_IN_INVALID); };
reservation.Guests = _guests;
reservation.CheckIn = _checkIn;
reservation.Validate();
this.Result = reservation;
}
}
Luckily, our reservation system is so simple the hotel will be selected on the next page and that will complete the reservation process. Therefore, we need to validate the existing reservation after adding the hotel to it. This is done with the Complete command located in the service layer.

public class CompleteCommand : Command
{
private Reservation _reservation;
private string _hotelName;

public CompleteCommand(Reservation existingReservation, string hotelName)
{
_reservation = existingReservation;
_hotelName = hotelName;
}

public void Execute()
{
Reservation reservation = new Reservation();
reservation.ErrorNotifier.GuestsInvalid += delegate { this.ErrorList.Add(Reservation.GUESTS_INVALID); };
reservation.ErrorNotifier.CheckInInvalid += delegate { this.ErrorList.Add(Reservation.CHECK_IN_INVALID); };
reservation.ErrorNotifier.HotelNameInvalid += delegate { this.ErrorList.Add(Reservation.HOTEL_NAME_INVALID); };
reservation.HotelName = _hotelName;
reservation.Validate();
}
}
Both SearchCommand and CompleteCommand inherit their Result and ErrorList properties from Command

public abstract class Command
{
private List _errorList = new List();
private object _result;

public List ErrorList
{
get
{
return _errorList;
}
}

public object Result
{
get { return _result; }
set { _result = value; }
}
}
Finally, the command objects can be used in the presentation layer to display the errors after execution.

Lastly, the unit tests used to drive this development.

[TestFixture]
public class ReservationTests
{
[Test]
public void NumberOfGuestsMustBeGreaterThanZero()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.Guests = 0;
reservation.ErrorNotifier.GuestsInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}

[Test]
public void ReservationCheckInDateFallAfterToday()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.CheckIn = DateTime.MinValue;
reservation.ErrorNotifier.CheckInInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}

[Test]
public void ReservationHotelCannotBeNull()
{
bool methodCalled = false;
Reservation reservation = new Reservation();
reservation.HotelName = null;
reservation.ErrorNotifier.HotelNameInvalid += delegate { methodCalled = true; };
reservation.Validate();
Assert.IsTrue(methodCalled);
}
}

[TestFixture]
public class SearchCommandTests
{
[Test]
public void InvalidGuestsAndCheckInErrorsAreAdded()
{
SearchCommand cmd = new SearchCommand(0, DateTime.MinValue);
cmd.Execute();
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.GUESTS_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.CHECK_IN_INVALID));
}
}

[TestFixture]
public class CompleteCommandTests
{
[Test]
public void InvalidGuestsAndCheckInErrorsAreAdded()
{
CompleteCommand cmd = new CompleteCommand(new Reservation(),null);
cmd.Execute();
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.GUESTS_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.CHECK_IN_INVALID));
Assert.IsTrue(cmd.ErrorList.Contains(Reservation.HOTEL_NAME_INVALID));
}
}


Source available here (Visual Studio 2005 Beta 2 April release).

Monday, April 25, 2005

Null Coalescing Operator in C# 2.0

In C# 2.0 a new null coalescing operator, ??, is provided. a ?? b is equivalent to a != null ? a : b. The null coalescing operator works on both reference types and nullable types in C# 2.0.

Nullable Type Example:
int? a = null, b = 1;
int x = a ?? b; // x = 1
a = 2;
x = a ?? b; // x = 2

Reference Type Example:
object a = null, b = "foo";
object x = a ?? b; // x.ToString() = "foo"
a = "bar";
x = a ?? b; // x.ToString() = "bar"

Friday, April 22, 2005

Using MSBuild (from Visual Studio 2005 April CTP) with a solution file (.sln)

Previously, I listed the steps you could take to move output files from library builds into the Bin folder of a File System Web Site using post build events. The issue that this process addressed has been fixed in the April CTP of Visual Studio 2005. However, the issue has not been fixed for using MSBuild. To reproduce the error, remove the Bin folder of a File System Web Site and use MSBuild to build the solution from the .sln file. MSBuild will error and the project will not build. This can be resolved by following the same steps I previously listed on 4/13/2005. Be sure to read the last paragraph for information concerning source control management and continuous integration.

My introduction to Ruby

Ruby is very popular these days. OnLamp has a great article on how to get started with Ruby and Ruby on Rails. Also, I've enjoyed Mike Clark's blog, specifically Ruby Learning Test #1: Are You There, World?. There are many other resources out there. Not convinced that a dynamically typed language is viable? Martin Fowler has a nice blog entry comparing dynamic and static typing. The list goes on, my blog wont.

Whether you believe the hype or not, many people I admire are very interested in Ruby. That's reason enough for me to want to explore it. Ignore Ruby at your own risk.

Thursday, April 14, 2005

File System Web Site reference issues in Visual Studio 2005 February CTP

Assume you are working on the following Foo application:



You've used Visual Studio 2005 to add a reference from FooServiceLayer to FooDomainLayer and from FooWebSite to FooServiceLayer.

Your classes are defined as such:







You build your solution in Visual Studio 2005 and no errors are reported. Then the fun begins.

If you try to browse to default.aspx you will get the following error:

Server Error in '/FooWebSite' Application.
Could not load file or assembly 'FooDomainLayer, Version=1.0.1929.14480, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.

Since FooWebSite depends on FooServiceLayer which depends on FooDomainLayer the FooDomainLayer.dll must be in the Bin folder of FooWebSite. This is nothing new from 1.1, except that Visual Studio 2005 no longer does it for you. To fix this issue you can add a Post Build Event by right clicking on FooDomainLayer in the solution explorer, selecting properties, then selecting Build Events from the menu that appears. Add a post build event command such as:



Note: your path may be different; therefore, assume you are in the folder where FooDomainLayer.dll is located and adjust your copy path.

Rebuild and browse to default.aspx and you will see "The method or operation is not implemented.", the exception from the Domain Layer you expected.

If you are using source control and continuous integration you should add the Bin folder of FooWebSite but ignore the contents of it and add a post build event to FooServiceLayer to copy FooServiceLayer.dll into the Bin folder of FooWebSite. While Visual Studio 2005 does copy the dll of a referenced project into the Bin folder of FooWebSite, MSBuild will not do this for you and your build will break.

Clean unit tests with anonymous methods

Assume you are working with the following class that contains the validation rule that an ExpenseReport cannot have a week ending date less than today. The expected behavior is that when Validate is called an event will be fired if any data in the object is invalid. Anyone interested in specific errors can choose to subscribe to a specific event.

public class ExpenseReport
{
private readonly DateTime _weekEndingDate;
public event ErrorEventHandler InvalidWeekEndingDate;
public delegate void ErrorEventHandler();

public ExpenseReport(DateTime weekEndingDate)
{
_weekEndingDate = weekEndingDate;
}

public DateTime WeekEndingDate
{
get { return _weekEndingDate; }
}

public void Validate()
{
if (_weekEndingDate < DateTime.Today)
{
InvalidWeekEndingDate();
}
}
}

Testing this class becomes easy and clean with NUnit and anonymous methods:
[TestFixture]
public class ExpenseReportTests
{
[Test]
public void ValidExpenseReport()
{
ExpenseReport report = new ExpenseReport(DateTime.Today.AddDays(2));
report.InvalidWeekEndingDate += delegate { Assert.Fail(); };
report.Validate();
}

[Test]
public void InvalidWeekEndingDateReport()
{
bool methodCalled = false;
ExpenseReport report = new ExpenseReport(DateTime.Today.AddDays(-2));
report.InvalidWeekEndingDate += delegate { methodCalled = true; };
report.Validate();
Assert.IsTrue(methodCalled);
}
}