Testing Deciders
In this step, you test the deciders from Making Decisions – without a database.
Given, When, Then#
A decider receives a command and a state, and the state is built from events. So every test of a decider can be told in three parts: given the events that have happened so far, when a command arrives, then the decider writes these events, or rejects the command. The architecturekittest package spells out tests exactly this way, and it builds the state from the given events itself, without EventSourcingDB.
Create a file deciders_test.go:
package main
import (
"testing"
"github.com/thenativeweb/architecturekit-golang/architecturekit"
"github.com/thenativeweb/architecturekit-golang/architecturekit/architecturekittest"
)
func TestBorrowBook(t *testing.T) {
t.Run("lends an available book", func(t *testing.T) {
architecturekittest.Given(t, borrowBook,
BookAcquired{
Title: "2001 – A Space Odyssey",
Author: "Arthur C. Clarke",
ISBN: "978-0756906788",
},
).
When(BorrowBook{BookID: "42", ReaderID: "23", BorrowedUntil: "2026-10-24"}).
ThenEvents(BookBorrowed{BorrowedBy: "23", BorrowedUntil: "2026-10-24"}).
ThenPreconditions(architecturekittest.OnStateRead())
})
t.Run("rejects a book that is already borrowed", func(t *testing.T) {
architecturekittest.Given(t, borrowBook,
BookAcquired{
Title: "2001 – A Space Odyssey",
Author: "Arthur C. Clarke",
ISBN: "978-0756906788",
},
BookBorrowed{BorrowedBy: "23", BorrowedUntil: "2026-10-24"},
).
When(BorrowBook{BookID: "42", ReaderID: "17", BorrowedUntil: "2026-10-31"}).
ThenRejected("book 42 is already borrowed")
})
t.Run("rejects a book that does not exist", func(t *testing.T) {
architecturekittest.Given(t, borrowBook).
When(BorrowBook{BookID: "42", ReaderID: "23", BorrowedUntil: "2026-10-24"}).
ThenFailed(architecturekit.ErrDomain)
})
}
func TestReturnBook(t *testing.T) {
t.Run("returns a borrowed book", func(t *testing.T) {
architecturekittest.Given(t, returnBook,
BookAcquired{
Title: "2001 – A Space Odyssey",
Author: "Arthur C. Clarke",
ISBN: "978-0756906788",
},
BookBorrowed{BorrowedBy: "23", BorrowedUntil: "2026-10-24"},
).
When(ReturnBook{BookID: "42"}).
ThenEvents(BookReturned{})
})
t.Run("does nothing for a book that is not borrowed", func(t *testing.T) {
architecturekittest.Given(t, returnBook,
BookAcquired{
Title: "2001 – A Space Odyssey",
Author: "Arthur C. Clarke",
ISBN: "978-0756906788",
},
).
When(ReturnBook{BookID: "42"}).
ThenNothing()
})
}
Given takes the test, the decider, and the events that have happened so far, and When takes the command. The functions that start with Then check the outcome, and since each of them returns the outcome again, you can chain them:
ThenEventsexpects exactly the given events, in the given order.ThenRejectedexpects a rejection with exactly the given message, andThenFailedan error of the given category, whatever its message.ThenNothingexpects neither events nor an error.ThenPreconditionsexpects exactly the given preconditions, here theOnStateReadprecondition from Making Decisions.
Running the Tests#
Run the tests. They need neither EventSourcingDB nor the API, so you can stop both first:
go test -v
All five tests pass. The durations vary:
=== RUN TestBorrowBook
=== RUN TestBorrowBook/lends_an_available_book
=== RUN TestBorrowBook/rejects_a_book_that_is_already_borrowed
=== RUN TestBorrowBook/rejects_a_book_that_does_not_exist
--- PASS: TestBorrowBook (0.00s)
--- PASS: TestBorrowBook/lends_an_available_book (0.00s)
--- PASS: TestBorrowBook/rejects_a_book_that_is_already_borrowed (0.00s)
--- PASS: TestBorrowBook/rejects_a_book_that_does_not_exist (0.00s)
=== RUN TestReturnBook
=== RUN TestReturnBook/returns_a_borrowed_book
=== RUN TestReturnBook/does_nothing_for_a_book_that_is_not_borrowed
--- PASS: TestReturnBook (0.00s)
--- PASS: TestReturnBook/returns_a_borrowed_book (0.00s)
--- PASS: TestReturnBook/does_nothing_for_a_book_that_is_not_borrowed (0.00s)
PASS
ok library 0.322s
When a Test Fails#
To see that the tests actually guard the rules, remove the check for a borrowed book from borrowBook in deciders.go:
if book.IsBorrowed {
return nil, architecturekit.NewDomainError("book %s is already borrowed", cmd.BookID)
}
Run the tests again, without -v:
go test
The decider now lends a book that is already borrowed, and the test tells you so. Again, the durations vary:
--- FAIL: TestBorrowBook (0.00s)
--- FAIL: TestBorrowBook/rejects_a_book_that_is_already_borrowed (0.00s)
deciders_test.go:34: expected rejection "book 42 is already borrowed", got 1 event(s): [io.eventsourcingdb.library.book-borrowed]
FAIL
exit status 1
FAIL library 0.362s
Put the check back, and the tests pass again. For more ways to check an outcome, see Testing Deciders, and for why tests like these matter, see Testing Event-Sourced Systems.
Shutting Down#
If they are still running, press Ctrl+C in the terminals of the API and of EventSourcingDB. Since the data directory of EventSourcingDB is temporary, all events are gone afterwards.
Next Steps#
You have built a small library with ArchitectureKit, from events to a tested HTTP API. To go further:
- Testing Projections and Testing Queries test the reading side without a database as well.
- Using Preconditions shows how to check the revision a caller has seen, and how to enforce rules across books, such as acquiring every ISBN only once.
- Setting Up an HTTP API and Handling Commands over HTTP add users, and refuse commands they are not allowed to send.
- Versioning Events changes the shape of an event without changing the events that are already stored.