Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Event Sourcing Facilitates Mock-free Unit Testing

5.00/5 (3 votes)
11 Mar 2015CPOL1 min read 17.9K  
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:

VB.NET
''' <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:

VB.NET
''' <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:

VB.NET
<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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)