Skip to content

Instantly share code, notes, and snippets.

@bwaterschoot
Last active December 22, 2015 10:26
Show Gist options
  • Save bwaterschoot/a8548dd21a9c87a0f712 to your computer and use it in GitHub Desktop.
Save bwaterschoot/a8548dd21a9c87a0f712 to your computer and use it in GitHub Desktop.
Event projector tests to in memory readmodel based on AggregateSource testing
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);
}
}
@yreynhout
Copy link

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.

@bwaterschoot
Copy link
Author

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.

@yreynhout
Copy link

  • 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 :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment