65.9K
CodeProject is changing. Read more.
Home

Event Sourcing Facilitates Mock-free Unit Testing

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3 votes)

Mar 11, 2015

CPOL

1 min read

viewsIcon

17880

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.