-
-
Save bwaterschoot/a8548dd21a9c87a0f712 to your computer and use it in GitHub Desktop.
public class DonationProjectorTests | |
{ | |
private Func<IEventStorage, InMemoryDonationProjector> _sutFactory; | |
[Test] | |
public void Donation_readmodel_should_be_initialised_correctly() | |
{ | |
new ProjectorScenarioFor<InMemoryDonationProjector, DonationDataModel>(_sutFactory, DonationStreamName.FromString, "d15f54ee8204445a95cff19e826a9746") | |
.When(new DonationCollectionStarted("d15f54ee8204445a95cff19e826a9746", "09a78a86d78445c58225ab88f0e31aea-v1", | |
"BLA-BLA", DateTimeOffset.Parse("2015-12-15 11:14:01.088"))) | |
.Then((model) => | |
{ | |
model.DonationCollectionId = "d15f54ee8204445a95cff19e826a9746"; | |
model.ReferralCode = "BLA-BLA"; | |
model.DonationStartedTime = DateTimeOffset.Parse("2015-12-15 11:14:01.088"); | |
model.AdditionalData = Maybe<string>.Empty; | |
return model; | |
}) | |
.Assert(); | |
} | |
[Test] | |
public void Donation_to_suspended_cause_should_invalidate_the_readmodel() | |
{ | |
new ProjectorScenarioFor<InMemoryDonationProjector, DonationDataModel>(_sutFactory, DonationStreamName.FromString, "d15f54ee8204445a95cff19e826a9746") | |
.Given( | |
new DonationCollectionStarted( | |
"d15f54ee8204445a95cff19e826a9746", | |
"09a78a86d78445c58225ab88f0e31aea-v1", | |
"BLA-BLA", | |
DateTimeOffset.Parse("2015-12-15 11:14:01.088"))) | |
.When(new DonationToSuspendedCauseAttempted("d15f54ee8204445a95cff19e826a9746", | |
"09a78a86d78445c58225ab88f0e31aea-v1")) | |
.Then((model) => | |
{ | |
model.Invalid = true; | |
model.Reason = "Can't donate to a suspended cause"; | |
return model; | |
}) | |
.Assert(); | |
} | |
[SetUp] | |
public void Setup() | |
{ | |
var additionalDataProvider = new Mock<IAdditionalDataProvider>(); | |
additionalDataProvider.Setup(x => x.GetAsync("d15f54ee8204445a95cff19e826a9746")).Returns(Task.FromResult(Maybe<string>.Empty)); | |
_sutFactory = (eventStore) => new InMemoryDonationProjector(eventStore, additionalDataProvider.Object); | |
} | |
} |
public class ProjectorScenarioFor<TProjector, TModel> | |
where TProjector : IProjectTo<TModel> | |
where TModel : new() | |
{ | |
private readonly Func<IEventStorage, TProjector> _projectorFactory; | |
private readonly string _id; | |
private TModel _beforeProjection; | |
private TModel _afterProjection; | |
private TModel _expectedProjection; | |
private object[] _givenEvents = new object[0]; | |
private readonly string _storageId; | |
public ProjectorScenarioFor(Func<IEventStorage, TProjector> projectorFactory, Func<string, string> storageIdMapper, string id) | |
{ | |
_projectorFactory = projectorFactory; | |
_id = id; | |
_storageId = storageIdMapper(id); | |
_beforeProjection = new TModel(); | |
} | |
public ProjectorScenarioFor<TProjector, TModel> Given(params object[] @events) | |
{ | |
_givenEvents = @events ?? new object[0]; | |
var eventStore = new Mock<IEventStorage>(); | |
eventStore.Setup(e => e.GetAsync(_storageId)).Returns(Task.FromResult(@events.AsEnumerable())); | |
var projector = _projectorFactory(eventStore.Object); | |
_beforeProjection = projector.GetByIdAsync(_id).Result; | |
return this; | |
} | |
public ProjectorScenarioFor<TProjector, TModel> When(params object[] @events) | |
{ | |
var eventStore = new Mock<IEventStorage>(); | |
var eventsToRun = _givenEvents.Concat(@events); | |
eventStore.Setup(e => e.GetAsync(_storageId)).Returns(Task.FromResult(eventsToRun)); | |
var projector = _projectorFactory(eventStore.Object); | |
_afterProjection = projector.GetByIdAsync(_id).Result; | |
return this; | |
} | |
public ProjectorScenarioFor<TProjector, TModel> Then(Func<TModel, TModel> expectation) | |
{ | |
_expectedProjection = expectation(_beforeProjection); | |
return this; | |
} | |
public void Assert() | |
{ | |
_afterProjection.ShouldBeEquivalentTo(_expectedProjection); | |
} | |
} |
I have added the actual code for the ProjectorScenario to add some context.
Some good suggestions there, will take a look at them.
We are doing object comparison, however I only specify what should change to the model when that event occurs.
I think you're limiting yourself when you're limiting your testing fx to 1 stream at a time IF that stream is a partition on the write side (e.g. an aggregate). Conceptually, each projection has its own stream - but it might be a stream projected off of other (typically aggregate) streams. I don't know whether this was a deliberate choice, if not, beware.
Some comments:
- I do like the GWT style, I just read it as:
- Given state x
- When projecting/replaying event y
- Then only the following state changes should have happened
- Which in a sense could be a written as:
- Given state (x+y)
- Then state is expected state
The difference between the 2 approaches is that I need to setup the entire model for the GT approach and only the specific state changes in the GWT?
- It currently is limited to 1 stream as we only have 1 aggregate in this "micro" service, hence the way the projector was build.
- I tend to read it more like this (a matter of preference, I acknowledge)
- Given these things have happened
- Expect the store (memory is a store as well) we projected into to look like this
- You're coupling yourself to implementation details of the projection in the Given state step, IMO. It's worse enough that the Expect/Then step is doing that. I don't worry too much about too much setup since it's a strong test smell indicator.
- If you're aware that you're feeding off of 1 aggregate stream - by all means, have a ball :-)
Couple of things that come to mind:
new DonationDataModel { ... }
? If you're worried about having to compare too many properties, you could use specialized comparers, but honestly, here be dragons. The danger is that you only compare what you're interested in and that the code under test might change in the future and have undesired side effects.when_a_donation_collection_started
,when_a_donator_donated_to_a_suspended_cause