Event Sourcing Facilitates Mock-free Unit Testing





5.00/5 (3 votes)
How an event sourcing / projection based system allows you to fully unit test the business code the application will use without mocks
Introduction
One of the advantages of an event sourcing (or event stream) based system is that you can very easily put together a unit test for your projection logic that uses all the same classes that will be used in the production application without any mocks at all.
So long as your event classes and your projection classes are written with no dependency on where the event stream is stored (or received from), then you can put together actual instances of the event and feed them into the projection as part of your testing.
For example, I have a system where a User can receive an "Applause
" event which adds to their Reputation cumulatively. The event to record this occurring might look like:
''' <summary>
''' The user has received praise/applause for something they have done
''' </summary>
''' <remarks>
''' This is a general gamification event to reward users for their actions
''' </remarks>
Public NotInheritable Class ApplaudedEvent
Inherits EventBase
Implements IEvent(Of AggregateIdentifiers.UserAggregateIdentity)
''' <summary>
''' The number of applause points added by this event
''' </summary>
''' <remarks>
''' It may be that the points for different types of event get
''' changed over time so we don't refer back to what the
''' event was but rather store the points awarded
''' </remarks>
<EventVersion(1)>
Public Property Points As Integer
''' <summary>
''' A personal message attached to the applause event
''' </summary>
<EventVersion(1)>
Public Property Message As String
''' <summary>
''' Who sent the applause
''' </summary>
<EventVersion(1)>
Public Property SendBy As String
Public Overrides Function ToString() As String
Return "Applause received - " & Points.ToString() _
& " " & Message & " from " & SendBy
End Function
End Class
This event (and others) can then be fed into a User Summary projection that gives a view of the user as at a given point in time. This works by just applying the effect of each event in turn to the "current state". It might look something like:
''' <summary>
''' Projection over the User event stream to summarize the state of a user
''' </summary>
Public NotInheritable Class UserSummaryProjection
Inherits ProjectionBase(Of AggregateIdentifiers.UserAggregateIdentity)
ReadOnly m_identity As AggregateIdentifiers.UserAggregateIdentity
''' <summary>
''' The aggregate identifier of the client to which this projection applies
''' </summary>
Public Overrides ReadOnly Property Identity As AggregateIdentifiers.UserAggregateIdentity
Get
Return m_identity
End Get
End Property
Public Overrides Sub ConsumeEvent(ByVal sequence As Integer, _
ByVal eventToConsume As IEvent(Of AggregateIdentifiers.UserAggregateIdentity))
If IsPriorEvent(sequence) Then
' This projection ignores out-of-sequence events
Return
End If
If (TypeOf (eventToConsume) Is Events.User.CreatedEvent) Then
Dim userCreated As Events.User.CreatedEvent = eventToConsume
m_userIdentifier = userCreated.UserIdentifier
m_emailAddress = userCreated.EmailAddress
End If
If (TypeOf (eventToConsume) Is Events.User.AccountEnabledEvent) Then
m_enabled = True
End If
If (TypeOf (eventToConsume) Is Events.User.AccountDisabledEvent) Then
m_enabled = False
End If
If (TypeOf (eventToConsume) Is Events.User.ApplaudedEvent) Then
Dim userApplauded As Events.User.ApplaudedEvent = eventToConsume
m_reputationPoints += userApplauded.Points
End If
' Update the current sequence after the event is consumed
SetSequence(sequence)
End Sub
''' <summary>
''' The public unique identifier of the user (could be a user name or company employee code etc.)
''' </summary>
Private m_userIdentifier As String
Public ReadOnly Property UserIdentifier As String
Get
Return m_userIdentifier
End Get
End Property
''' <summary>
''' The email address of the user
''' </summary>
Private m_emailAddress As String
Public ReadOnly Property EmailAddress As String
Get
Return m_emailAddress
End Get
End Property
''' <summary>
''' Is the user enabled or not
''' </summary>
''' <remarks>
''' This allows users to be removed from the system without any data integrity issues
''' </remarks>
Private m_enabled As Boolean
Public ReadOnly Property Enabled As Boolean
Get
Return m_enabled
End Get
End Property
Private m_reputationPoints As Integer
Public ReadOnly Property ReputationPoints As Integer
Get
Return m_reputationPoints
End Get
End Property
Public Sub New(ByVal aggregateidentity As AggregateIdentifiers.UserAggregateIdentity)
m_identity = aggregateidentity
End Sub
End Class
This projection has no run-time dependencies outside of the class itself and the events it handles, and this can be unit tested with no mocking or fakes classes something like:
<TestMethod()>
Public Sub ReputationTestMethod()
Dim expected As Integer = 3
Dim actual As Integer = 1
Dim testObj As New Projections.UserSummaryProjection_
(New AggregateIdentifiers.UserAggregateIdentity("test"))
' Run some applauded events in
testObj.ConsumeEvent(1, New [Event].Events.User.ApplaudedEvent()
With {.Points = 1, .Message = "Good job man"})
testObj.ConsumeEvent(2, New [Event].Events.User.ApplaudedEvent()
With {.Points = 2, .Message = "Agreed", .SendBy = "Duncan"})
'Throw in an out of sequence one to test it gets ignored
testObj.ConsumeEvent(0, New [Event].Events.User.ApplaudedEvent()
With {.Points = 2, .Message = "This is out of sequence", .SendBy = "Duncan"})
actual = testObj.ReputationPoints
Assert.AreEqual(expected, actual)
End Sub
This means that you really can have a great deal of confidence in your code as it has all been unit tested.