- address a new technology (eg. Win Phone 7)
- test mocking (eg. M-V-P presenter tests or MVC controller tests)
- design a series of classes in a domain (eg. 5-day TDD kata)
NOTE The code for this kata is available here on github.
Without Strategy pattern (and more generally, without refactoring to maximum granularity), domain entities tend to grow fat, full of algorithmic business logic which they would be better to call as multiple domain service classes than to possess internally.
In this kata, the Treatment entity represents a treatment for a pet at the veterinarians' office, such as prescription, hydration, dietary advice, referral, etc. The type of treatment is implemented as a TreatmentType property member off of the Treatment class.
In a typical Strategy Pattern before-and-after scenario, the TreatmentType property would originally have encapsulated a switch statement, with different treatment type logic for each case, creating a fat entity.
- NOTE A switch statement in itself is not bad; the point is that it should primarily be used for a very "thin" activity: generating the subclasses of the Strategy hierarchy.
NOTE This kata will test a bit more than just Strategy. For entity classes in the above domain model (Pet and Treatment), we will also test for identity/equality by creating a base class (DomainEntity) which will handle these concerns. While entity identity is not central to Strategy, it's fundamental to the domain. So let's begin.
TDD Kata for Strategy
1) Create a Visual Studio solution with two projects/assemblies:
* Kata.Domain
* Kata.Tests.Unit (add the appropriate references to this project for the testing framework you prefer)
2) In Kata.Tests.Unit, create a test class, DomainEntityTests, and write a test for each of the following:
a) that 2 instances of DomainEntity whose Id property is the same are equal
[Test]
public void TwoInstances_SameIdPropertySet_AreEqual()
{
const int commonId = 32753;
var sut1 = new DomainEntity {Id = commonId};
var sut2 = new DomainEntity {Id = commonId};
sut1.ShouldEqual(sut2);
}
b) that 2 instances of DomainEntity whose Id property is different are not equal
[Test]
public void TwoInstances_DifferentIdPropertySet_AreNotEqual()
{
var sut1 = new DomainEntity { Id = 4275 };
var sut2 = new DomainEntity { Id = 3214 };
sut1.ShouldNotEqual(sut2);
}
3) Create a test class, PetTests, which tests the following:
a) that Pet is an instance of DomainEntity
[Test]
public void Constructor_NoInputParams_IsInstanceOfDomainEntity()
{
var sut = new Pet();
sut.ShouldBeInstanceOf<DomainEntity>();
}
b) that Pet.Treatments association has 0 rows at first.
[Test]
public void TreatmentsProperty_Get_HasCountOf0Initially()
{
var sut = new Pet();
sut.Treatments.Count.ShouldEqual(0);
}
c) that Pet.AddTreatment(treatment) increments the count by 1
[Test]
public void AddTreatmentMethod_PetInputParam_IncremantsTreatmentsCount()
{
var sut = new Pet();
sut.Treatments.Count.ShouldEqual(0);
sut.AddTreatment(new Treatment());
sut.Treatments.Count.ShouldEqual(1);
}
4) Create a test class, TreatmentTests, which tests the following:
a) that Treatment is an instance of DomainEntity.
[Test]
public void Constructor_NoInputParams_IsInstanceOfDomainEntity()
{
var sut = new Treatment();
sut.ShouldBeInstanceOf<DomainEntity>();
}
We'll come back to add additional tests later.
5) Create a test class, TreatmentTypeTests, which tests the following:
a) that TreatmentType has no public constructors (use LINQ)
[Test]
public void Constructors_Public_ShouldBeNone()
{
typeof(TreatmentType).GetConstructors()
.Where(constructorInfo => constructorInfo.IsPublic).Count().ShouldEqual(0);
}
NOTE This is because the base class of a Strategy hierarchy must use a creation method to control class instantiation, to only create subclasses of the base class.
b) that the CreateTreatmentType creation method accepts a TreatmentTypeEnum enumeration, and returns an instance of the subclass specified by name in the enumeration. (For the first test, use Surgery).
[Test]
public void CreationMethod_SurgeryTreatmentEnumInput_ReturnsInstanceOfSurgery()
{
var surgeryTreatment = TreatmentType.CreateTreatment(Treatment.TreatmentEnum.Surgery);
surgeryTreatment.ShouldBeInstanceOf<Surgery>();
surgeryTreatment.ShouldBeInstanceOf<TreatmentType>();
}
c) recreate this test identically for every subclass type, including:
- Dietary
- Hydration
- Prescription
- Referral
- Surgery
public static TreatmentType CreateTreatment(Treatment.TreatmentEnum treatmentEnum)
{
switch(treatmentEnum)
{
case Treatment.TreatmentEnum.Dietary:
return new Dietary();
break;
case Treatment.TreatmentEnum.Hydration:
return new Hydration();
break;
case Treatment.TreatmentEnum.Prescription:
return new Prescription();
break;
case Treatment.TreatmentEnum.Referral:
return new Referral();
break;
case Treatment.TreatmentEnum.Surgery:
return new Surgery();
break;
}
return null;
}
6) Now we come to the CourseOfAction property, which is the first property of the TreatmentType base class, and which is about to bring the Strategy hierarchy into full bloom. The default test will simply assert a value of CourseOfAction on the base class. Therefore:
a) Test that a new property of TreamentType, string::CourseOfAction returns a string that says "do something".
[Test]
public void CourseOfActionProperty_SurgeryTreamentEnumInput_ReturnsSurgeryCourseOfAction()
{
var sut = TreatmentType.CreateTreatment(Treatment.TreatmentEnum.Surgery);
sut.CourseOfAction.ShouldEqual("Do something.");
}
7) Having a single value in the base class, "Do something", is kind of pointless, other than to introduce the property concept. What we really want is polymorphic differentiation for this property, where the output varies for each subclass. To do this, first modify the CourseOfAction property to be abstract:
public abstract string CourseOfAction { get; }
This immediately has two impacts:
a) the compiler forces you to "correct" the class, making it abstract as well.
public abstract class TreatmentType
{
...
}
a) the compiler generates errors, telling you that the abstract property must be implemented in each subclass. This is perfect, as it completes the Strategy. Correct this now:
[Test]
public void CourseOfActionProperty_DietaryTreamentEnumInput_ReturnsDietaryCourseOfAction()
{
var sut = TreatmentType.CreateTreatment(Treatment.TreatmentEnum.Dietary);
sut.CourseOfAction.ShouldEqual("Give dietary advice.");
}
[Test]
public void CourseOfActionProperty_HydrationTreamentEnumInput_ReturnsHydrationCourseOfAction()
{
var sut = TreatmentType.CreateTreatment(Treatment.TreatmentEnum.Hydration);
sut.CourseOfAction.ShouldEqual("Hydrate the pet.");
}
...etc...
8) Finally, we are ready to link TreatmentType up to Treatment. We will fully encapsulate the Strategy pattern by assigning the TreatmentEnum in the constructor of Treatment. The TreatmentType property will then simply return the correct subclass and subclass property values.
a) Therefore, test that treatment.TreatmentType.CourseOfAction property results from a simple chain, given that the constructor is assigned a TreatmentEnum param.
[Test]
public void Constructor_TreatmentEnumParam_ReturnsCorrectCourseOfActionProperty()
{
var sut = new Treatment(Treatment.TreatmentEnum.Dietary);
sut.TreatmentType.CourseOfAction.ShouldEqual("Give dietary advice.");
}
9) Note that changing the Treatment entity constructor will break a few places in your code. Fix each one by assigning an enum to the constructor.
Congratulations! You have now implemented the complete Strategy Pattern as a domain service, completely encapsulated by Treatment. If you wish to compare your code, have a look at the implementation here, on github.
No comments:
Post a Comment