Loading...
Loading...
Marco Franssen

The project I'm currently working on uses CQRS and the ncqrs framework. Today, I reached a point where I needed to mock some of my domain services. I chose FakeItEasy and started by adding a reference to my DomainScenario project through a NuGet package.
In some of my more complex scenarios, I ran into an issue that took about 45 minutes to fix in one test. I then had to apply the same trick to the other failing tests.
Before I explain the issue with FakeItEasy, I should mention that I use my aggregate roots to prepare the event store. The problem is related to this particular setup.
For example, I have a Participant aggregate root like this:
public class Participant : BisAggregateRoot
{
private readonly IUniqueParticipantNrGuard _uniqueFormattedParticipantNrVerifier = NcqrsEnvironment.Get<IUniqueParticipantNrGuard>();
//other members left for convenience
private string _participantNrFormat;
private int _participantNr;
private Gender _gender;
private PartialDate _birthDate;
private string _remark;
public Guid ParticipantId { get { return EventSourceId; } }
// ReSharper disable UnusedMember.Local
private Participant()
// ReSharper restore UnusedMember.Local
{
}
public Participant(Guid participantId, ScientificStudy scientificStudy, string participantNrFormat, int participantNr, Gender gender, PartialDate birthdate)
: base(participantId)
{
_uniqueFormattedParticipantNrVerifier.GuardIsUnique(scientificStudy.ScientificStudyId, participantId,
string.Format(participantNrFormat, participantNr));
ApplyEvent(new ParticipantAddedEvent(Credentials)
{
ParticipantId = participantId,
ParticipantNrFormat = participantNrFormat,
ParticipantNr = participantNr,
ScientificStudyId = scientificStudy.ScientificStudyId,
Gender = gender.ActualValue,
Birthdate = birthdate.ActualValue
});
}
public void Edit(ScientificStudy scientificStudy, string participantNrFormat, int participantNr, Gender gender, PartialDate birthdate, string remark)
{
//Some other business rules
_uniqueFormattedParticipantNrVerifier.GuardIsUnique(scientificStudy.ScientificStudyId, ParticipantId,
string.Format(participantNrFormat, participantNr));
//Some other business rules
ApplyEvent(new ParticipantEditedEvent(Credentials)
{
ParticipantId = ParticipantId,
ParticipantNrFormat = participantNrFormat,
ParticipantNr = participantNr,
Gender = gender.ActualValue,
Birthdate = birthdate.ActualValue
});
}
protected void OnParticipantAdded(ParticipantAddedEvent e)
{
_claimedAliquots = new List<ClaimedAliquot>();
_participantNrFormat = e.ParticipantNrFormat;
_participantNr = e.ParticipantNr;
_gender = new Gender(e.Gender);
_birthDate = new PartialDate(e.Birthdate);
}
protected void OnParticipantEdited(ParticipantEditedEvent e)
{
_participantNrFormat = e.ParticipantNrFormat;
_participantNr = e.ParticipantNr;
_gender = new Gender(e.Gender);
_birthDate = new PartialDate(e.Birthdate);
_remark = e.Remark;
}
}As you can see, both the constructor and the edit method use the same domain service to check whether the participant number is unique. Keep this in mind: it's important because of how I prepare my event store.
To prepare the event store, I use a fluent builder I wrote that uses domain objects to generate the events.
public static class New
{
public static ScientificStudy ScientificStudy(string nr = "M001", string name = "Maastricht Study", string participantNrFormat = "P{0}", string boxNameFormat = "B{0}")
{
return new ScientificStudy(Default.Id.For.ScientificStudy, nr, name, participantNrFormat, boxNameFormat);
}
public static Participant Participant(ScientificStudy scientificStudy, string participantNrFormat = "P{0}", int participantNr = 1, int gender = 0, string birthdate = "19861117")
{
var realGender = new Gender(gender);
var realBirthDate = new PartialDate(birthdate);
return new Participant(Default.Id.For.Participant, scientificStudy, participantNrFormat, participantNr, realGender, realBirthDate);
}
//other members left for convenience
}Here's how I use this builder in my tests. Notice that GetChanges is an extension method on my aggregate roots.
protected override void SetupDependencies()
{
base.SetupDependencies();
var eventStore = NcqrsEnvironment.Get<IEventStore>();
var scientificStudy = New.ScientificStudy();
var scientificStudyEvent = scientificStudy.TrackChanges().GetChanges();
var participantEvents = New.Participant(scientificStudy).GetChanges();
eventStore.Store(Prepare.Events(scientificStudyEvent).ForSourceUncomitted(Default.Id.For.ScientificStudy, Default.Id.Random()));
eventStore.Store(Prepare.Events(participantEvents).ForSourceUncomitted(Default.Id.For.Participant, Default.Id.Random()));
}Initially, I registered my fakes as shown below, which made the test fail. I'm trying to check whether editing a participant throws a 'FormattedParticipantNrShouldBeUniqueException' when the participantNr is changed to one that already exists.
protected override void RegisterFakesInConfiguration(EnvironmentConfigurationWrapper configuration)
{
var commandService = new CommandService();
commandService.RegisterExecutor(new EditParticipantCommandExecutor());
var uniqueParticipantGuard = A.Fake<IUniqueParticipantNrGuard>();
A.CallTo(uniqueParticipantGuard).DoesNothing().Once();
A.CallTo(uniqueParticipantGuard).Throws(
new FormattedParticipantNrShouldBeUniqueException(_formattedParticipantNr));
configuration.Register<ICommandService>(commandService);
configuration.Register(uniqueParticipantGuard);
//other fakes left for convenience
base.RegisterFakesInConfiguration(configuration);
}After 45 minutes of searching and discussing it with my colleague, we found a solution.
protected override void RegisterFakesInConfiguration(EnvironmentConfigurationWrapper configuration)
{
var commandService = new CommandService();
commandService.RegisterExecutor(new EditParticipantCommandExecutor());
var uniqueParticipantGuard = A.Fake<IUniqueParticipantNrGuard>();
A.CallTo(uniqueParticipantGuard).Throws(
new FormattedParticipantNrShouldBeUniqueException(_formattedParticipantNr));
A.CallTo(uniqueParticipantGuard).DoesNothing().Once();
configuration.Register<ICommandService>(commandService);
configuration.Register(uniqueParticipantGuard);
//other fakes left for convenience
base.RegisterFakesInConfiguration(configuration);
}As you can see, the solution was simple, but hard to find: debugging didn't reveal that the configuration order was the issue.
Just switch the order in which you configure the mocked/faked object.
Last but not least, here's my actual test method.
[Then]
public void it_should_throw_an_formatted_participant_nr_should_be_unique_exception_containing_the_formatted_participant_nr()
{
var exception = (FormattedParticipantNrShouldBeUniqueException)CaughtException;
exception.FormattedParticipantNr.Should().Be(_formattedParticipantNr);
}I'm not sure whether I was doing it wrong or whether FakeItEasy needs a fix to prevent this issue. I hope I've explained it clearly and saved you some time if you encounter the same problem.
Marco Franssen
Try Windows 8 Developer Preview in VirtualBox with this walkthrough of hardware requirements, virtual machine settings, and the installation wizard.
Marco Franssen
Build country, state, and city dropdowns with Knockout.js observables and jQuery JSON requests, with an ASP.NET MVC 3 server-side example.
Marco Franssen
Lessons from applying DDD and CQRS in a Scrum project: bounded contexts, aggregate roots, domain services, guard methods, and command-based tests.
Marco Franssen
Configure Castle Windsor dependency injection in ASP.NET MVC 3 with an installer, controller factory, constructor injection, and repository disposal.
Marco Franssen
My notes from Greg Young's DDD/CQRS course on aggregates, entities, value objects, command-query separation, and consistency through event merging.