diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f5191e..bf5ea918 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ migrate tests from prior versions. - **[BC]** Move `assert.Should()`, to `testkit.ToSatisfy()` - **[BC]** Move `assert.CommandTypeExecuted()`, to `testkit.ToExecuteCommandOfType()` - **[BC]** Move `assert.EventTypeExecuted()`, to `testkit.ToRecordEventOfType()` +- **[BC]** Move `assert.CommandExecuted()`, to `testkit.ToExecuteCommand()` +- **[BC]** Move `assert.EventExecuted()`, to `testkit.ToRecordEvent()` ### Removed diff --git a/action.call_test.go b/action.call_test.go index 017fb496..d9c42dd4 100644 --- a/action.call_test.go +++ b/action.call_test.go @@ -10,7 +10,6 @@ import ( "github.com/dogmatiq/dogma" . "github.com/dogmatiq/dogma/fixtures" . "github.com/dogmatiq/testkit" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine" "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" @@ -154,7 +153,7 @@ var _ = Describe("func Call()", func() { MessageC1, ) }), - assert.CommandExecuted(MessageC1), + ToExecuteCommand(MessageC1), ) }) @@ -168,7 +167,7 @@ var _ = Describe("func Call()", func() { MessageE1, ) }), - assert.EventRecorded(MessageE1), + ToRecordEvent(MessageE1), ) }) diff --git a/action.dispatch.command_test.go b/action.dispatch.command_test.go index 50b0122b..772df679 100644 --- a/action.dispatch.command_test.go +++ b/action.dispatch.command_test.go @@ -9,7 +9,6 @@ import ( "github.com/dogmatiq/dogma" . "github.com/dogmatiq/dogma/fixtures" . "github.com/dogmatiq/testkit" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine" "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" @@ -119,7 +118,7 @@ var _ = Describe("func ExecuteCommand()", func() { test.Expect( ExecuteCommand(MessageC1), - assert.CommandExecuted(MessageC1), + ToExecuteCommand(MessageC1), ) Expect(t.Failed()).To(BeTrue()) diff --git a/action.dispatch.event_test.go b/action.dispatch.event_test.go index 66ae4468..aa082e7e 100644 --- a/action.dispatch.event_test.go +++ b/action.dispatch.event_test.go @@ -10,7 +10,6 @@ import ( "github.com/dogmatiq/dogma" . "github.com/dogmatiq/dogma/fixtures" . "github.com/dogmatiq/testkit" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine" "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" @@ -121,7 +120,7 @@ var _ = Describe("func RecordEvent()", func() { test.Expect( RecordEvent(MessageE1), - assert.EventRecorded(MessageE1), + ToRecordEvent(MessageE1), ) Expect(t.Failed()).To(BeTrue()) diff --git a/assert/assertion.go b/assert/assertion.go deleted file mode 100644 index 839a4f66..00000000 --- a/assert/assertion.go +++ /dev/null @@ -1,49 +0,0 @@ -package assert - -import ( - "github.com/dogmatiq/testkit/compare" - "github.com/dogmatiq/testkit/engine/fact" -) - -// ExpectOptionSet is a set of options that dictate the behavior of the -// Test.Expect() method. -type ExpectOptionSet struct { - // MessageComparator compares two messages for equality. - MessageComparator compare.Comparator - - // MatchMessagesInDispatchCycle controls whether expectations should match - // messages from the start of a dispatch cycle. - // - // If it is false, only messages produced by handlers within the application - // are matched. - MatchMessagesInDispatchCycle bool -} - -// An Assertion is a predicate for determining whether some specific criteria -// was met during a test. -type Assertion interface { - fact.Observer - - // Banner returns a human-readable banner to display in the logs when this - // expectation is used. - // - // The banner text should be in uppercase, and complete the sentence "The - // application is expected ...". For example, "TO DO A THING". - Banner() string - - // Begin is called to prepare the assertion for a new test. - Begin(o ExpectOptionSet) - - // End is called once the test is complete. - End() - - // Ok returns true if the assertion passed. - Ok() bool - - // BuildReport generates a report about the assertion. - // - // ok is true if the assertion is considered to have passed. This may not be - // the same value as returned from Ok() when this assertion is used as a - // sub-assertion inside a composite. - BuildReport(ok bool) *Report -} diff --git a/assert/common_test.go b/assert/common_test.go deleted file mode 100644 index c9d275da..00000000 --- a/assert/common_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package assert_test - -import ( - "context" - "strings" - - "github.com/dogmatiq/dogma" - . "github.com/dogmatiq/dogma/fixtures" - "github.com/dogmatiq/testkit" - "github.com/dogmatiq/testkit/engine" - "github.com/dogmatiq/testkit/internal/testingmock" - "github.com/onsi/gomega" -) - -func newTestApp() ( - *Application, - *AggregateMessageHandler, - *ProcessMessageHandler, - *IntegrationMessageHandler, -) { - aggregate := &AggregateMessageHandler{ - ConfigureFunc: func(c dogma.AggregateConfigurer) { - c.Identity("", "") - c.ConsumesCommandType(MessageA{}) - c.ProducesEventType(MessageB{}) - }, - RouteCommandToInstanceFunc: func(dogma.Message) string { - return "" - }, - HandleCommandFunc: func( - _ dogma.AggregateRoot, - s dogma.AggregateCommandScope, - m dogma.Message, - ) { - s.RecordEvent( - MessageB{Value: ""}, - ) - }, - } - - process := &ProcessMessageHandler{ - ConfigureFunc: func(c dogma.ProcessConfigurer) { - c.Identity("", "") - c.ConsumesEventType(MessageB{}) - c.ProducesCommandType(MessageC{}) - }, - RouteEventToInstanceFunc: func(context.Context, dogma.Message) (string, bool, error) { - return "", true, nil - }, - HandleEventFunc: func( - _ context.Context, - s dogma.ProcessEventScope, - m dogma.Message, - ) error { - s.Begin() - s.ExecuteCommand( - MessageC{Value: ""}, - ) - - return nil - }, - } - - integration := &IntegrationMessageHandler{ - ConfigureFunc: func(c dogma.IntegrationConfigurer) { - c.Identity("", "") - c.ConsumesCommandType(MessageC{}) - c.ProducesEventType(MessageD{}) - }, - HandleCommandFunc: func( - _ context.Context, - s dogma.IntegrationCommandScope, - m dogma.Message, - ) error { - s.RecordEvent( - MessageD{Value: ""}, - ) - - return nil - }, - } - - app := &Application{ - ConfigureFunc: func(c dogma.ApplicationConfigurer) { - c.Identity("", "") - c.RegisterAggregate(aggregate) - c.RegisterProcess(process) - c.RegisterIntegration(integration) - }, - } - - return app, aggregate, process, integration -} - -func runTest( - app dogma.Application, - op func(*testkit.Test), - options []engine.OperationOption, - expectOk bool, - expectReport []string, -) { - t := &testingmock.T{ - FailSilently: true, - } - - opts := append( - []engine.OperationOption{ - engine.EnableAggregates(true), - engine.EnableProcesses(true), - engine.EnableIntegrations(true), - engine.EnableProjections(true), - }, - options..., - ) - - test := testkit.Begin( - t, - app, - testkit.WithUnsafeOperationOptions(opts...), - ) - - op(test) - - logs := strings.TrimSpace(strings.Join(t.Logs, "\n")) - lines := strings.Split(logs, "\n") - - for i, l := range lines { - if l == "--- TEST REPORT ---" { - gomega.Expect(lines[i:]).To(gomega.Equal(expectReport)) - gomega.Expect(t.Failed()).To(gomega.Equal(!expectOk)) - return - } - } - - gomega.Expect(lines).To(gomega.Equal(expectReport)) - gomega.Expect(t.Failed()).To(gomega.Equal(!expectOk)) -} diff --git a/assert/doc.go b/assert/doc.go deleted file mode 100644 index da6ca8e5..00000000 --- a/assert/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package assert contains the assertions used by the test runner. -package assert diff --git a/assert/ginkgo_test.go b/assert/ginkgo_test.go deleted file mode 100644 index d9c4e486..00000000 --- a/assert/ginkgo_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package assert_test - -import ( - "reflect" - "testing" - - "github.com/onsi/ginkgo" - "github.com/onsi/gomega" -) - -func TestSuite(t *testing.T) { - type tag struct{} - gomega.RegisterFailHandler(ginkgo.Fail) - ginkgo.RunSpecs(t, reflect.TypeOf(tag{}).PkgPath()) -} diff --git a/assert/message_test.go b/assert/message_test.go deleted file mode 100644 index 800b660d..00000000 --- a/assert/message_test.go +++ /dev/null @@ -1,488 +0,0 @@ -package assert_test - -import ( - "errors" - - "github.com/dogmatiq/dogma" - . "github.com/dogmatiq/dogma/fixtures" - "github.com/dogmatiq/testkit" - . "github.com/dogmatiq/testkit/assert" - "github.com/dogmatiq/testkit/engine" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - "github.com/onsi/gomega" -) - -var _ = Context("message assertions", func() { - var ( - app dogma.Application - aggregate *AggregateMessageHandler - process *ProcessMessageHandler - integration *IntegrationMessageHandler - action testkit.Action - options []engine.OperationOption - ) - - BeforeEach(func() { - app, aggregate, process, integration = newTestApp() - action = testkit.ExecuteCommand(MessageA{}) - options = nil - }) - - test := func( - setup func(), - assertion Assertion, - expectOk bool, - expectReport ...string, - ) { - if setup != nil { - setup() - } - - runTest( - app, - func(t *testkit.Test) { - t.Expect(action, assertion) - }, - options, - expectOk, - expectReport, - ) - } - - Describe("func CommandExecuted()", func() { - It("panics if the message is invalid", func() { - gomega.Expect(func() { - CommandExecuted(MessageA{ - Value: errors.New(""), - }) - }).To(gomega.PanicWith("can not assert that this command will be executed, it is invalid: ")) - }) - }) - - Describe("func RecordEvent()", func() { - It("panics if the message is invalid", func() { - gomega.Expect(func() { - EventRecorded(MessageA{ - Value: errors.New(""), - }) - }).To(gomega.PanicWith("can not assert that this event will be recorded, it is invalid: ")) - }) - }) - - DescribeTable( - "func CommandExecuted()", - test, - Entry( - "command executed as expected", - nil, // setup - CommandExecuted(MessageC{Value: ""}), - true, // ok - `--- TEST REPORT ---`, - ``, - `✓ execute a specific 'fixtures.MessageC' command`, - ), - Entry( - "no matching command executed", - nil, // setup - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | none of the engaged handlers executed the expected command`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' process message handler`, - ), - Entry( - "no messages produced at all", - func() { - process.HandleEventFunc = nil - action = testkit.RecordEvent(MessageB{}) - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no messages were produced at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' process message handler`, - ), - Entry( - "no commands produced at all", - func() { - process.HandleEventFunc = nil - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no commands were executed at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' process message handler`, - ), - Entry( - "no matching command executed and all relevant handler types disabled", - func() { - options = append( - options, - engine.EnableProcesses(false), - ) - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no relevant handler types were enabled`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable process handlers using the EnableHandlerType() option`, - ), - Entry( - // Note: the report produced from this test is actually the same as - // the test above because there is only one relevant handler type - // (process) that can be disabled. It is kept for completeness and - // uniformity with the test suite for EventRecorded(). Additionally, - // the assertion report content will likely diverge from the test - // above upon completion of https://github.com/dogmatiq/testkit/issues/66. - "no matching command executed and no relevant handler types engaged", - func() { - options = append( - options, - engine.EnableProcesses(false), - ) - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no relevant handler types were enabled`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable process handlers using the EnableHandlerType() option`, - ), - Entry( - "similar command executed with a different type", - nil, // setup - CommandExecuted(&MessageC{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific '*fixtures.MessageC' command`, - ``, - ` | EXPLANATION`, - ` | a command of a similar type was executed by the '' process message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageC{`, - ` | Value: ""`, - ` | }`, - ), - Entry( - "similar command executed with a different value", - nil, // setup - CommandExecuted(MessageC{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageC' command`, - ``, - ` | EXPLANATION`, - ` | a similar command was executed by the '' process message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the content of the message`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageC{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "expected message recorded as an event rather than executed as a command", - nil, // setup - CommandExecuted(MessageB{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | the expected message was recorded as an event by the '' aggregate message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' aggregate message handler intended to record an event of this type`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ), - Entry( - "similar message with a different value recorded as an event rather than executed as a command", - nil, // setup - CommandExecuted(MessageB{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | a similar message was recorded as an event by the '' aggregate message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' aggregate message handler intended to record an event of this type`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageB{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "similar message with a different type recorded as an event rather than executed as a command", - nil, // setup - CommandExecuted(&MessageB{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific '*fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | a message of a similar type was recorded as an event by the '' aggregate message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' aggregate message handler intended to record an event of this type`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageB{`, - ` | Value: ""`, - ` | }`, - ), - ) - - DescribeTable( - "func EventRecorded()", - test, - Entry( - "event recorded as expected", - nil, // setup - EventRecorded(MessageB{Value: ""}), - true, // ok - `--- TEST REPORT ---`, - ``, - `✓ record a specific 'fixtures.MessageB' event`, - ), - Entry( - "no matching event recorded", - nil, // setup - EventRecorded(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageX' event`, - ``, - ` | EXPLANATION`, - ` | none of the engaged handlers recorded the expected event`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' aggregate message handler`, - ` | • verify the logic within the '' integration message handler`, - ), - Entry( - "no matching event recorded and all relevant handler types disabled", - func() { - options = append( - options, - engine.EnableAggregates(false), - engine.EnableIntegrations(false), - ) - }, - EventRecorded(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageX' event`, - ``, - ` | EXPLANATION`, - ` | no relevant handler types were enabled`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable aggregate handlers using the EnableHandlerType() option`, - ` | • enable integration handlers using the EnableHandlerType() option`, - ), - Entry( - "no matching event recorded and no relevant handler types engaged", - func() { - options = append( - options, - engine.EnableAggregates(false), - ) - }, - EventRecorded(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageX' event`, - ``, - ` | EXPLANATION`, - ` | no relevant handlers (aggregate or integration) were engaged`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable aggregate handlers using the EnableHandlerType() option`, - ` | • check the application's routing configuration`, - ), - Entry( - "no messages produced at all", - func() { - aggregate.HandleCommandFunc = nil - }, - EventRecorded(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageX' event`, - ``, - ` | EXPLANATION`, - ` | no messages were produced at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' aggregate message handler`, - ), - Entry( - "no events recorded at all", - func() { - integration.HandleCommandFunc = nil - action = testkit.RecordEvent(MessageB{}) - }, - EventRecorded(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageX' event`, - ``, - ` | EXPLANATION`, - ` | no events were recorded at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' integration message handler`, - ), - Entry( - "similar event recorded with a different type", - nil, // setup - EventRecorded(&MessageB{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific '*fixtures.MessageB' event`, - ``, - ` | EXPLANATION`, - ` | an event of a similar type was recorded by the '' aggregate message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageB{`, - ` | Value: ""`, - ` | }`, - ), - Entry( - "similar event recorded with a different value", - nil, // setup - EventRecorded(MessageB{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageB' event`, - ``, - ` | EXPLANATION`, - ` | a similar event was recorded by the '' aggregate message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the content of the message`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageB{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "expected message executed as a command rather than recorded as an event", - nil, // setup - EventRecorded(MessageC{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageC' event`, - ``, - ` | EXPLANATION`, - ` | the expected message was executed as a command by the '' process message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' process message handler intended to execute a command of this type`, - ` | • verify that EventRecorded() is the correct assertion, did you mean CommandExecuted()?`, - ), - Entry( - "similar message with a different value executed as a command rather than recorded as an event", - nil, // setup - EventRecorded(MessageC{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific 'fixtures.MessageC' event`, - ``, - ` | EXPLANATION`, - ` | a similar message was executed as a command by the '' process message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' process message handler intended to execute a command of this type`, - ` | • verify that EventRecorded() is the correct assertion, did you mean CommandExecuted()?`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageC{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "similar message with a different type executed as a command rather than recorded as an event", - nil, // setup - EventRecorded(&MessageC{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ record a specific '*fixtures.MessageC' event`, - ``, - ` | EXPLANATION`, - ` | a message of a similar type was executed as a command by the '' process message handler`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that the '' process message handler intended to execute a command of this type`, - ` | • verify that EventRecorded() is the correct assertion, did you mean CommandExecuted()?`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageC{`, - ` | Value: ""`, - ` | }`, - ), - ) -}) diff --git a/assert/messagedispatch_test.go b/assert/messagedispatch_test.go deleted file mode 100644 index d3aad55e..00000000 --- a/assert/messagedispatch_test.go +++ /dev/null @@ -1,291 +0,0 @@ -package assert_test - -import ( - "context" - - "github.com/dogmatiq/dogma" - . "github.com/dogmatiq/dogma/fixtures" - "github.com/dogmatiq/testkit" - . "github.com/dogmatiq/testkit/assert" - "github.com/dogmatiq/testkit/engine" - . "github.com/onsi/ginkgo" - . "github.com/onsi/ginkgo/extensions/table" - "github.com/onsi/gomega" -) - -var _ = Context("message assertions that match messages dispatched directly", func() { - var ( - app dogma.Application - process *ProcessMessageHandler - command, event dogma.Message - options []engine.OperationOption - ) - - BeforeEach(func() { - app, _, process, _ = newTestApp() - command = MessageA{Value: ""} - event = MessageB{Value: ""} - options = nil - }) - - test := func( - setup func(), - assertion Assertion, - expectOk bool, - expectReport ...string, - ) { - if setup != nil { - setup() - } - - runTest( - app, - func(t *testkit.Test) { - t.Expect( - testkit.Call(func() { - if command != nil { - err := t.CommandExecutor().ExecuteCommand( - context.Background(), - command, - ) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - } - - if event != nil { - err := t.EventRecorder().RecordEvent( - context.Background(), - event, - ) - gomega.Expect(err).ShouldNot(gomega.HaveOccurred()) - } - }), - assertion, - ) - }, - options, - expectOk, - expectReport, - ) - } - - DescribeTable( - "func CommandExecuted()", - test, - Entry( - "command executed as expected", - nil, // setup - CommandExecuted(MessageC{Value: ""}), - true, // ok - `--- TEST REPORT ---`, - ``, - `✓ execute a specific 'fixtures.MessageC' command`, - ), - Entry( - "no matching command executed", - nil, // setup - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | nothing executed the expected command`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' process message handler`, - ` | • verify the logic within the code that uses the dogma.CommandExecutor`, - ), - Entry( - "no messages produced at all", - func() { - process.HandleEventFunc = nil - command = nil - event = nil - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no messages were produced at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the code that uses the dogma.CommandExecutor`, - ), - Entry( - "no commands produced at all", - func() { - process.HandleEventFunc = nil - command = nil - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | no commands were executed at all`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify the logic within the '' process message handler`, - ` | • verify the logic within the code that uses the dogma.CommandExecutor`, - ), - Entry( - "no matching command executed and all relevant handler types disabled", - func() { - options = append( - options, - engine.EnableProcesses(false), - ) - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | nothing executed the expected command`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable process handlers using the EnableHandlerType() option`, - ` | • verify the logic within the code that uses the dogma.CommandExecutor`, - ), - Entry( - // Note: the report produced from this test is actually the same as - // the test above because there is only one relevant handler type - // (process) that can be disabled. It is kept for completeness and - // uniformity with the test suite for EventRecorded(). Additionally, - // the assertion report content will likely diverge from the test - // above upon completion of https://github.com/dogmatiq/testkit/issues/66. - "no matching command executed and no relevant handler types engaged", - func() { - options = append( - options, - engine.EnableProcesses(false), - ) - }, - CommandExecuted(MessageX{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageX' command`, - ``, - ` | EXPLANATION`, - ` | nothing executed the expected command`, - ` | `, - ` | SUGGESTIONS`, - ` | • enable process handlers using the EnableHandlerType() option`, - ` | • verify the logic within the code that uses the dogma.CommandExecutor`, - ), - Entry( - "similar command executed with a different type", - nil, // setup - CommandExecuted(&MessageA{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific '*fixtures.MessageA' command`, - ``, - ` | EXPLANATION`, - ` | a command of a similar type was executed via a dogma.CommandExecutor`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageA{`, - ` | Value: ""`, - ` | }`, - ), - Entry( - "similar command executed with a different value", - nil, // setup - CommandExecuted(MessageA{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageA' command`, - ``, - ` | EXPLANATION`, - ` | a similar command was executed via a dogma.CommandExecutor`, - ` | `, - ` | SUGGESTIONS`, - ` | • check the content of the message`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageA{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "expected message recorded as an event rather than executed as a command", - func() { - command = nil - }, - CommandExecuted(MessageB{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | the expected message was recorded as an event via a dogma.EventRecorder`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ), - Entry( - "similar message with a different value recorded as an event rather than executed as a command", - func() { - command = nil - }, - CommandExecuted(MessageB{Value: ""}), - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific 'fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | a similar message was recorded as an event via a dogma.EventRecorder`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ` | `, - ` | MESSAGE DIFF`, - ` | fixtures.MessageB{`, - ` | Value: "<[-differ-]{+valu+}e[-nt-]>"`, - ` | }`, - ), - Entry( - "similar message with a different type recorded as an event rather than executed as a command", - func() { - command = nil - }, - CommandExecuted(&MessageB{Value: ""}), // note: message type is pointer - false, // ok - `--- TEST REPORT ---`, - ``, - `✗ execute a specific '*fixtures.MessageB' command`, - ``, - ` | EXPLANATION`, - ` | a message of a similar type was recorded as an event via a dogma.EventRecorder`, - ` | `, - ` | SUGGESTIONS`, - ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, - ` | • verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?`, - ` | • check the message type, should it be a pointer?`, - ` | `, - ` | MESSAGE DIFF`, - ` | [-*-]fixtures.MessageB{`, - ` | Value: ""`, - ` | }`, - ), - ) -}) diff --git a/assert/report.go b/assert/report.go deleted file mode 100644 index ff4ccf01..00000000 --- a/assert/report.go +++ /dev/null @@ -1,158 +0,0 @@ -package assert - -import ( - "io" - "strings" - - "github.com/dogmatiq/iago/count" - "github.com/dogmatiq/iago/indent" - "github.com/dogmatiq/iago/must" -) - -// Report is a report on the outcome of an assertion. -type Report struct { - // TreeOk is true if the "tree" that the assertion belongs to passed. - TreeOk bool - - // Ok is true if this assertion passed. - Ok bool - - // Criteria is a brief description of the assertion's requirement to pass. - Criteria string - - // Outcome is a brief description of the outcome of the assertion. - Outcome string - - // Explanation is a brief description of what actually happened during the - // test as it relates to this assertion. - Explanation string - - // Sections contains arbitrary "sections" that are added to the report by - // the assertion. - Sections []*ReportSection - - // SubReports contains the reports of any sub-assertions. - SubReports []*Report -} - -// Section adds an arbitrary "section" to the report. -func (r *Report) Section(title string) *ReportSection { - for _, s := range r.Sections { - if s.Title == title { - return s - } - } - - s := &ReportSection{ - Title: title, - } - - r.Sections = append(r.Sections, s) - - return s -} - -// Append adds sr as a sub-report of s. -func (r *Report) Append(sr *Report) { - r.SubReports = append(r.SubReports, sr) -} - -// WriteTo writes the report to the given writer. -func (r *Report) WriteTo(next io.Writer) (_ int64, err error) { - defer must.Recover(&err) - w := count.NewWriter(next) - - if r.Ok { - must.WriteString(w, "✓ ") - } else { - must.WriteString(w, "✗ ") - } - - must.WriteString(w, r.Criteria) - - if r.Outcome != "" { - must.WriteString(w, " (") - must.WriteString(w, r.Outcome) - must.WriteByte(w, ')') - } - - must.WriteByte(w, '\n') - - if len(r.Sections) != 0 || r.Explanation != "" { - must.WriteByte(w, '\n') - - iw := indent.NewIndenter(w, sectionsIndent) - - if r.Explanation != "" { - must.WriteString(iw, "EXPLANATION\n") - - must.WriteString( - iw, - indent.String(r.Explanation, sectionContentIndent), - ) - - must.WriteByte(iw, '\n') - - if len(r.Sections) != 0 { - must.WriteByte(iw, '\n') - } - } - - for i, s := range r.Sections { - must.WriteString(iw, strings.ToUpper(s.Title)) - must.WriteString(iw, "\n") - - must.WriteString( - iw, - indent.String( - strings.TrimSpace(s.Content.String()), - sectionContentIndent, - ), - ) - - must.WriteByte(iw, '\n') - - if i < len(r.Sections)-1 { - must.WriteByte(iw, '\n') - } - } - } - - if len(r.SubReports) != 0 { - iw := indent.NewIndenter(w, subReportsIndent) - for _, sr := range r.SubReports { - must.WriteTo(iw, sr) - } - } - - return int64(w.Count()), nil -} - -// ReportSection is a section of a report containing additional information -// about the assertion. -type ReportSection struct { - Title string - Content strings.Builder -} - -// Append appends a line of text to the section's content. -func (s *ReportSection) Append(f string, v ...interface{}) { - must.Fprintf(&s.Content, f+"\n", v...) -} - -// AppendListItem appends a line of text prefixed with a bullet. -func (s *ReportSection) AppendListItem(f string, v ...interface{}) { - s.Append("• "+f, v...) -} - -var ( - sectionsIndent = []byte(" | ") - sectionContentIndent = " " - subReportsIndent = []byte(" ") -) - -const ( - suggestionsSection = "Suggestions" - messageDiffSection = "Message Diff" - messageTypeDiffSection = "Message Type Diff" -) diff --git a/assert/tracker.go b/assert/tracker.go deleted file mode 100644 index fa418d47..00000000 --- a/assert/tracker.go +++ /dev/null @@ -1,149 +0,0 @@ -package assert - -import ( - "fmt" - "strings" - - "github.com/dogmatiq/configkit" - "github.com/dogmatiq/configkit/message" - "github.com/dogmatiq/testkit/engine/fact" - "github.com/dogmatiq/testkit/internal/inflect" -) - -// tracker is an observer used by assertions that keeps track of common -// information about handlers and the messages they produce. -type tracker struct { - // role is the role that the message is expecting to find. - role message.Role - - // matchDispatchCycle, if true, tracks messages that originate from a - // command executor or event recorder, not just those that originate from - // handlers within the application. - matchDispatchCycle bool - - // cycleBegun is true if at least one dispatch or tick cycle was started. - cycleBegun bool - - // total is the total number of messages that were produced. - total int - - // produced is the number of messages of the expected role that were - // produced. - produced int - - // engagedOrder and engagedType track the set of handlers that *could* have - // produced the expected message. - engagedOrder []string - engagedType map[string]configkit.HandlerType - - // enabled is the set of handler types that are enabled during the test. - enabled map[configkit.HandlerType]bool -} - -// Notify updates the assertion's state in response to a new fact. -func (t *tracker) Notify(f fact.Fact) { - switch x := f.(type) { - case fact.DispatchCycleBegun: - t.cycleBegun = true - t.enabled = x.EnabledHandlerTypes - if t.matchDispatchCycle { - t.messageProduced(x.Envelope.Role) - } - case fact.TickCycleBegun: - t.cycleBegun = true - t.enabled = x.EnabledHandlerTypes - case fact.HandlingBegun: - t.updateEngaged( - x.Handler.Identity().Name, - x.Handler.HandlerType(), - ) - case fact.EventRecordedByAggregate: - t.messageProduced(x.EventEnvelope.Role) - case fact.EventRecordedByIntegration: - t.messageProduced(x.EventEnvelope.Role) - case fact.CommandExecutedByProcess: - t.messageProduced(x.CommandEnvelope.Role) - } -} - -func (t *tracker) updateEngaged(n string, ht configkit.HandlerType) { - if ht.IsProducerOf(t.role) { - if t.engagedType == nil { - t.engagedType = map[string]configkit.HandlerType{} - } - - if _, ok := t.engagedType[n]; !ok { - t.engagedOrder = append(t.engagedOrder, n) - t.engagedType[n] = ht - } - } -} - -func (t *tracker) messageProduced(r message.Role) { - t.total++ - - if r == t.role { - t.produced++ - } -} - -// buildResultNoMatch is a helper used by MessageAssertion and -// MessageTypeAssertion when there is no "best-match". -func buildResultNoMatch(rep *Report, t *tracker) { - s := rep.Section(suggestionsSection) - - allDisabled := true - var relevant []string - - if t.cycleBegun { - for _, ht := range configkit.HandlerTypes { - e := t.enabled[ht] - - if ht.IsProducerOf(t.role) { - relevant = append(relevant, ht.String()) - - if e { - allDisabled = false - } else { - s.AppendListItem( - fmt.Sprintf("enable %s handlers using the EnableHandlerType() option", ht), - ) - } - } - } - - if !t.matchDispatchCycle { - if allDisabled { - rep.Explanation = "no relevant handler types were enabled" - return - } - - if len(t.engagedOrder) == 0 { - rep.Explanation = fmt.Sprintf( - "no relevant handlers (%s) were engaged", - strings.Join(relevant, " or "), - ) - s.AppendListItem("check the application's routing configuration") - return - } - } - } - - if t.total == 0 { - rep.Explanation = "no messages were produced at all" - } else if t.produced == 0 { - rep.Explanation = inflect.Sprint(t.role, "no were at all") - } else if t.matchDispatchCycle { - rep.Explanation = inflect.Sprint(t.role, "nothing the expected ") - } else { - rep.Explanation = inflect.Sprint(t.role, "none of the engaged handlers the expected ") - } - - for _, n := range t.engagedOrder { - s.AppendListItem("verify the logic within the '%s' %s message handler", n, t.engagedType[n]) - } - - if t.matchDispatchCycle { - s.AppendListItem(inflect.Sprint(t.role, "verify the logic within the code that uses the ")) - } -} diff --git a/expectation.composite.go b/expectation.composite.go index 3e4bff3f..5f64b281 100644 --- a/expectation.composite.go +++ b/expectation.composite.go @@ -3,7 +3,6 @@ package testkit import ( "fmt" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine/fact" ) @@ -190,10 +189,10 @@ func (e *compositeExpectation) Ok() bool { // ok is true if the expectation is considered to have passed. This may not be // the same value as returned from Ok() when this expectation is used as a child // of a composite expectation. -func (e *compositeExpectation) BuildReport(ok bool) *assert.Report { +func (e *compositeExpectation) BuildReport(ok bool) *Report { e.Ok() // populate e.ok and e.outcome - rep := &assert.Report{ + rep := &Report{ TreeOk: ok, Ok: *e.ok, Criteria: e.Criteria, diff --git a/expectation.go b/expectation.go index 936f4ee5..8859ec9f 100644 --- a/expectation.go +++ b/expectation.go @@ -1,16 +1,52 @@ package testkit import ( - "github.com/dogmatiq/testkit/assert" + "github.com/dogmatiq/testkit/compare" + "github.com/dogmatiq/testkit/engine/fact" ) // An Expectation is a predicate for determining whether some specific criteria // was met while performing an action. -type Expectation = assert.Assertion +type Expectation interface { + fact.Observer -// ExpectOptionSet is a set of options that dictate the behavior of the -// Test.Expect() method. -type ExpectOptionSet = assert.ExpectOptionSet + // Banner returns a human-readable banner to display in the logs when this + // expectation is used. + // + // The banner text should be in uppercase, and complete the sentence "The + // application is expected ...". For example, "TO DO A THING". + Banner() string + + // Begin is called to prepare the expectation for a new test. + Begin(o ExpectOptionSet) + + // End is called once the test is complete. + End() + + // Ok returns true if the expectation passed. + Ok() bool + + // BuildReport generates a report about the expectation. + // + // ok is true if the expectation is considered to have passed. This may not be + // the same value as returned from Ok() when this expectation is used as a child + // of a composite expectation. + BuildReport(ok bool) *Report +} // ExpectOption is an option that changes the behavior the Test.Expect() method. type ExpectOption func(*ExpectOptionSet) + +// ExpectOptionSet is a set of options that dictate the behavior of the +// Test.Expect() method. +type ExpectOptionSet struct { + // MessageComparator compares two messages for equality. + MessageComparator compare.Comparator + + // MatchMessagesInDispatchCycle controls whether expectations should match + // messages from the start of a dispatch cycle. + // + // If it is false, only messages produced by handlers within the application + // are matched. + MatchMessagesInDispatchCycle bool +} diff --git a/expectation.message.command_test.go b/expectation.message.command_test.go new file mode 100644 index 00000000..a570390b --- /dev/null +++ b/expectation.message.command_test.go @@ -0,0 +1,271 @@ +package testkit_test + +import ( + "context" + + "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/dogma/fixtures" + . "github.com/dogmatiq/testkit" + "github.com/dogmatiq/testkit/engine" + "github.com/dogmatiq/testkit/internal/testingmock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("func ToExecuteCommand()", func() { + var ( + testingT *testingmock.T + app dogma.Application + test *Test + ) + + BeforeEach(func() { + testingT = &testingmock.T{ + FailSilently: true, + } + + app = &Application{ + ConfigureFunc: func(c dogma.ApplicationConfigurer) { + c.Identity("", "") + + c.RegisterAggregate(&AggregateMessageHandler{ + ConfigureFunc: func(c dogma.AggregateConfigurer) { + c.Identity("", "") + c.ConsumesCommandType(MessageR{}) // R = record an event + c.ProducesEventType(MessageN{}) + }, + RouteCommandToInstanceFunc: func(dogma.Message) string { + return "" + }, + HandleCommandFunc: func( + _ dogma.AggregateRoot, + s dogma.AggregateCommandScope, + _ dogma.Message, + ) { + s.RecordEvent(MessageN1) + }, + }) + + c.RegisterProcess(&ProcessMessageHandler{ + ConfigureFunc: func(c dogma.ProcessConfigurer) { + c.Identity("", "") + c.ConsumesEventType(MessageE{}) // E = event + c.ConsumesEventType(MessageN{}) // N = (do) nothing + c.ProducesCommandType(MessageC{}) // C = command + }, + RouteEventToInstanceFunc: func( + context.Context, + dogma.Message, + ) (string, bool, error) { + return "", true, nil + }, + HandleEventFunc: func( + _ context.Context, + s dogma.ProcessEventScope, + m dogma.Message, + ) error { + if _, ok := m.(MessageE); ok { + s.Begin() + s.ExecuteCommand(MessageC1) + } + return nil + }, + }) + }, + } + }) + + DescribeTable( + "expectation behavior", + func( + a Action, + e Expectation, + ok bool, + rm reportMatcher, + options ...TestOption, + ) { + test = Begin(testingT, app, options...) + test.Expect(a, e) + rm(testingT) + Expect(testingT.Failed()).To(Equal(!ok)) + }, + Entry( + "command executed as expected", + RecordEvent(MessageE1), + ToExecuteCommand(MessageC1), + expectPass, + expectReport( + `✓ execute a specific 'fixtures.MessageC' command`, + ), + ), + Entry( + "no matching command executed", + RecordEvent(MessageE1), + ToExecuteCommand(MessageX1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageX' command`, + ``, + ` | EXPLANATION`, + ` | none of the engaged handlers executed the expected command`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the '' process message handler`, + ), + ), + Entry( + "no messages produced at all", + RecordEvent(MessageN1), + ToExecuteCommand(MessageX1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageX' command`, + ``, + ` | EXPLANATION`, + ` | no messages were produced at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the '' process message handler`, + ), + ), + Entry( + "no commands produced at all", + ExecuteCommand(MessageR1), + ToExecuteCommand(MessageC1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | no commands were executed at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the '' process message handler`, + ), + ), + Entry( + "no matching command executed and all relevant handler types disabled", + ExecuteCommand(MessageR1), + ToExecuteCommand(MessageX1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageX' command`, + ``, + ` | EXPLANATION`, + ` | no relevant handler types were enabled`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable process handlers using the EnableHandlerType() option`, + ), + WithUnsafeOperationOptions( + engine.EnableProcesses(false), + ), + ), + Entry( + "similar command executed with a different type", + RecordEvent(MessageE1), + ToExecuteCommand(&MessageC1), // note: message type is pointer + expectFail, + expectReport( + `✗ execute a specific '*fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | a command of a similar type was executed by the '' process message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageC{`, + ` | Value: "C1"`, + ` | }`, + ), + ), + Entry( + "similar command executed with a different value", + RecordEvent(MessageE1), + ToExecuteCommand(MessageC2), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | a similar command was executed by the '' process message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the content of the message`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageC{`, + ` | Value: "C[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "expected message recorded as an event rather than executed as a command", + ExecuteCommand(MessageR1), + ToExecuteCommand(MessageN1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | the expected message was recorded as an event by the '' aggregate message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' aggregate message handler intended to record an event of this type`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ), + ), + Entry( + "similar message with a different value recorded as an event rather than executed as a command", + ExecuteCommand(MessageR1), + ToExecuteCommand(MessageN2), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | a similar message was recorded as an event by the '' aggregate message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' aggregate message handler intended to record an event of this type`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageN{`, + ` | Value: "N[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "similar message with a different type recorded as an event rather than executed as a command", + ExecuteCommand(MessageR1), + ToExecuteCommand(&MessageN2), // note: message type is pointer + expectFail, + expectReport( + `✗ execute a specific '*fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | a message of a similar type was recorded as an event by the '' aggregate message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' aggregate message handler intended to record an event of this type`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageN{`, + ` | Value: "N[-2-]{+1+}"`, + ` | }`, + ), + ), + ) + + It("panics if the message is nil", func() { + Expect(func() { + ToExecuteCommand(nil) + }).To(PanicWith("ToExecuteCommand(): message must not be nil")) + }) +}) diff --git a/expectation.message.commandcall_test.go b/expectation.message.commandcall_test.go new file mode 100644 index 00000000..44b22a5e --- /dev/null +++ b/expectation.message.commandcall_test.go @@ -0,0 +1,282 @@ +package testkit_test + +import ( + "context" + + "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/dogma/fixtures" + . "github.com/dogmatiq/testkit" + "github.com/dogmatiq/testkit/engine" + "github.com/dogmatiq/testkit/internal/testingmock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("func ToExecuteCommand() (when used with the Call() action)", func() { + var ( + testingT *testingmock.T + app dogma.Application + test *Test + ) + + BeforeEach(func() { + testingT = &testingmock.T{ + FailSilently: true, + } + + app = &Application{ + ConfigureFunc: func(c dogma.ApplicationConfigurer) { + c.Identity("", "") + + c.RegisterAggregate(&AggregateMessageHandler{ + ConfigureFunc: func(c dogma.AggregateConfigurer) { + c.Identity("", "") + c.ConsumesCommandType(MessageR{}) // R = record an event + c.ProducesEventType(MessageN{}) + }, + RouteCommandToInstanceFunc: func(dogma.Message) string { + return "" + }, + HandleCommandFunc: func( + _ dogma.AggregateRoot, + s dogma.AggregateCommandScope, + _ dogma.Message, + ) { + s.RecordEvent(MessageN1) + }, + }) + + c.RegisterProcess(&ProcessMessageHandler{ + ConfigureFunc: func(c dogma.ProcessConfigurer) { + c.Identity("", "") + c.ConsumesEventType(MessageE{}) // E = event + c.ConsumesEventType(MessageN{}) // N = (do) nothing + c.ProducesCommandType(MessageC{}) // C = command + }, + RouteEventToInstanceFunc: func( + context.Context, + dogma.Message, + ) (string, bool, error) { + return "", true, nil + }, + HandleEventFunc: func( + _ context.Context, + s dogma.ProcessEventScope, + m dogma.Message, + ) error { + if _, ok := m.(MessageE); ok { + s.Begin() + s.ExecuteCommand(MessageC1) + } + return nil + }, + }) + }, + } + }) + + executeCommandViaExecutor := func(m dogma.Message) Action { + return Call(func() { + err := test.CommandExecutor().ExecuteCommand(context.Background(), m) + Expect(err).ShouldNot(HaveOccurred()) + }) + } + + recordEventViaRecorder := func(m dogma.Message) Action { + return Call(func() { + err := test.EventRecorder().RecordEvent(context.Background(), m) + Expect(err).ShouldNot(HaveOccurred()) + }) + } + + DescribeTable( + "expectation behavior", + func( + a Action, + e Expectation, + ok bool, + rm reportMatcher, + options ...TestOption, + ) { + test = Begin(testingT, app, options...) + test.Expect(a, e) + rm(testingT) + Expect(testingT.Failed()).To(Equal(!ok)) + }, + Entry( + "command executed as expected", + executeCommandViaExecutor(MessageR1), + ToExecuteCommand(MessageR1), + expectPass, + expectReport( + `✓ execute a specific 'fixtures.MessageR' command`, + ), + ), + Entry( + "no matching command executed", + recordEventViaRecorder(MessageE1), + ToExecuteCommand(MessageX1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageX' command`, + ``, + ` | EXPLANATION`, + ` | nothing executed the expected command`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the '' process message handler`, + ` | • verify the logic within the code that uses the dogma.CommandExecutor`, + ), + ), + Entry( + "no messages produced at all", + Call(func() {}), + ToExecuteCommand(MessageC{}), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | no messages were produced at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the code that uses the dogma.CommandExecutor`, + ), + ), + Entry( + "no commands produced at all", + recordEventViaRecorder(MessageN1), + ToExecuteCommand(MessageC{}), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | no commands were executed at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the '' process message handler`, + ` | • verify the logic within the code that uses the dogma.CommandExecutor`, + ), + ), + Entry( + "no matching command executed and all relevant handler types disabled", + executeCommandViaExecutor(MessageR1), + ToExecuteCommand(MessageC1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageC' command`, + ``, + ` | EXPLANATION`, + ` | nothing executed the expected command`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable process handlers using the EnableHandlerType() option`, + ` | • verify the logic within the code that uses the dogma.CommandExecutor`, + ), + WithUnsafeOperationOptions( + engine.EnableProcesses(false), + ), + ), + Entry( + "similar command executed with a different type", + executeCommandViaExecutor(MessageR1), + ToExecuteCommand(&MessageR1), // note: message type is pointer + expectFail, + expectReport( + `✗ execute a specific '*fixtures.MessageR' command`, + ``, + ` | EXPLANATION`, + ` | a command of a similar type was executed via a dogma.CommandExecutor`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageR{`, + ` | Value: "R1"`, + ` | }`, + ), + ), + Entry( + "similar command executed with a different value", + executeCommandViaExecutor(MessageR1), + ToExecuteCommand(MessageR2), // note: message type is pointer + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageR' command`, + ``, + ` | EXPLANATION`, + ` | a similar command was executed via a dogma.CommandExecutor`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the content of the message`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageR{`, + ` | Value: "R[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "expected message recorded as an event rather than executed as a command", + recordEventViaRecorder(MessageN1), + ToExecuteCommand(MessageN1), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | the expected message was recorded as an event via a dogma.EventRecorder`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ), + ), + Entry( + "similar message with a different value recorded as an event rather than executed as a command", + recordEventViaRecorder(MessageN1), + ToExecuteCommand(MessageN2), + expectFail, + expectReport( + `✗ execute a specific 'fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | a similar message was recorded as an event via a dogma.EventRecorder`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageN{`, + ` | Value: "N[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "similar message with a different type recorded as an event rather than executed as a command", + recordEventViaRecorder(MessageN1), + ToExecuteCommand(&MessageN1), // note: message type is pointer + expectFail, + expectReport( + `✗ execute a specific '*fixtures.MessageN' command`, + ``, + ` | EXPLANATION`, + ` | a message of a similar type was recorded as an event via a dogma.EventRecorder`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that an event of this type was intended to be recorded via a dogma.EventRecorder`, + ` | • verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageN{`, + ` | Value: "N1"`, + ` | }`, + ), + ), + ) +}) diff --git a/expectation.message.event_test.go b/expectation.message.event_test.go new file mode 100644 index 00000000..dbe8e6b3 --- /dev/null +++ b/expectation.message.event_test.go @@ -0,0 +1,298 @@ +package testkit_test + +import ( + "context" + + "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/dogma/fixtures" + . "github.com/dogmatiq/testkit" + "github.com/dogmatiq/testkit/engine" + "github.com/dogmatiq/testkit/internal/testingmock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("func ToExecuteCommandOfType()", func() { + var ( + testingT *testingmock.T + app dogma.Application + test *Test + ) + + BeforeEach(func() { + testingT = &testingmock.T{ + FailSilently: true, + } + + app = &Application{ + ConfigureFunc: func(c dogma.ApplicationConfigurer) { + c.Identity("", "") + + c.RegisterAggregate(&AggregateMessageHandler{ + ConfigureFunc: func(c dogma.AggregateConfigurer) { + c.Identity("", "") + c.ConsumesCommandType(MessageR{}) // R = record an event + c.ConsumesCommandType(MessageN{}) // N = do nothing + c.ProducesEventType(MessageE{}) + }, + RouteCommandToInstanceFunc: func(dogma.Message) string { + return "" + }, + HandleCommandFunc: func( + _ dogma.AggregateRoot, + s dogma.AggregateCommandScope, + m dogma.Message, + ) { + if _, ok := m.(MessageR); ok { + s.RecordEvent(MessageE1) + } + }, + }) + + c.RegisterProcess(&ProcessMessageHandler{ + ConfigureFunc: func(c dogma.ProcessConfigurer) { + c.Identity("", "") + c.ConsumesEventType(MessageE{}) // E = execute a command + c.ProducesCommandType(MessageN{}) + }, + RouteEventToInstanceFunc: func( + context.Context, + dogma.Message, + ) (string, bool, error) { + return "", true, nil + }, + HandleEventFunc: func( + _ context.Context, + s dogma.ProcessEventScope, + m dogma.Message, + ) error { + if _, ok := m.(MessageE); ok { + s.Begin() + s.ExecuteCommand(MessageN1) + } + return nil + }, + }) + }, + } + }) + + DescribeTable( + "expectation behavior", + func( + a Action, + e Expectation, + ok bool, + rm reportMatcher, + options ...TestOption, + ) { + test = Begin(testingT, app, options...) + test.Expect(a, e) + rm(testingT) + Expect(testingT.Failed()).To(Equal(!ok)) + }, + Entry( + "event recorded as expected", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageE1), + expectPass, + expectReport( + `✓ record a specific 'fixtures.MessageE' event`, + ), + ), + Entry( + "no matching event recorded", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | none of the engaged handlers recorded the expected event`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the '' aggregate message handler`, + ), + ), + Entry( + "no matching event recorded and all relevant handler types disabled", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | no relevant handler types were enabled`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable aggregate handlers using the EnableHandlerType() option`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ), + WithUnsafeOperationOptions( + engine.EnableAggregates(false), + engine.EnableIntegrations(false), + ), + ), + Entry( + "no matching event recorded and no relevant handler types engaged", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | no relevant handlers (aggregate or integration) were engaged`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable aggregate handlers using the EnableHandlerType() option`, + ` | • check the application's routing configuration`, + ), + WithUnsafeOperationOptions( + engine.EnableAggregates(false), + engine.EnableIntegrations(true), + ), + ), + Entry( + "no messages produced at all", + ExecuteCommand(MessageN1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | no messages were produced at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the '' aggregate message handler`, + ), + ), + Entry( + "no events recorded at all", + RecordEvent(MessageE1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | no events were recorded at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the '' aggregate message handler`, + ), + ), + Entry( + "similar event recorded with a different type", + ExecuteCommand(MessageR1), + ToRecordEvent(&MessageE1), // note: message type is pointer + expectFail, + expectReport( + `✗ record a specific '*fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | an event of a similar type was recorded by the '' aggregate message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageE{`, + ` | Value: "E1"`, + ` | }`, + ), + ), + Entry( + "similar event recorded with a different value", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageE2), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | a similar event was recorded by the '' aggregate message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the content of the message`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageE{`, + ` | Value: "E[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "expected message executed as a command rather than recorded as an event", + RecordEvent(MessageE1), + ToRecordEvent(MessageN1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageN' event`, + ``, + ` | EXPLANATION`, + ` | the expected message was executed as a command by the '' process message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' process message handler intended to execute a command of this type`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ), + ), + Entry( + "similar message with a different value executed as a command rather than recorded as an event", + ExecuteCommand(MessageR1), + ToRecordEvent(MessageN2), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageN' event`, + ``, + ` | EXPLANATION`, + ` | a similar message was executed as a command by the '' process message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' process message handler intended to execute a command of this type`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageN{`, + ` | Value: "N[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "similar message with a different type executed as a command rather than recorded as an event", + ExecuteCommand(MessageR1), + ToRecordEvent(&MessageN1), // note: message type is pointer + expectFail, + expectReport( + `✗ record a specific '*fixtures.MessageN' event`, + ``, + ` | EXPLANATION`, + ` | a message of a similar type was executed as a command by the '' process message handler`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that the '' process message handler intended to execute a command of this type`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageN{`, + ` | Value: "N1"`, + ` | }`, + ), + ), + ) + + It("panics if the message is nil", func() { + Expect(func() { + ToRecordEvent(nil) + }).To(PanicWith("ToRecordEvent(): message must not be nil")) + }) +}) diff --git a/expectation.message.eventcall_test.go b/expectation.message.eventcall_test.go new file mode 100644 index 00000000..7903ef50 --- /dev/null +++ b/expectation.message.eventcall_test.go @@ -0,0 +1,288 @@ +package testkit_test + +import ( + "context" + + "github.com/dogmatiq/dogma" + . "github.com/dogmatiq/dogma/fixtures" + . "github.com/dogmatiq/testkit" + "github.com/dogmatiq/testkit/engine" + "github.com/dogmatiq/testkit/internal/testingmock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("func ToRecordEvent() (when used with the Call() action)", func() { + var ( + testingT *testingmock.T + app dogma.Application + test *Test + ) + + BeforeEach(func() { + testingT = &testingmock.T{ + FailSilently: true, + } + + app = &Application{ + ConfigureFunc: func(c dogma.ApplicationConfigurer) { + c.Identity("", "") + + c.RegisterAggregate(&AggregateMessageHandler{ + ConfigureFunc: func(c dogma.AggregateConfigurer) { + c.Identity("", "") + c.ConsumesCommandType(MessageR{}) // R = record an event + c.ConsumesCommandType(MessageN{}) // N = do nothing + c.ProducesEventType(MessageE{}) + }, + RouteCommandToInstanceFunc: func(dogma.Message) string { + return "" + }, + HandleCommandFunc: func( + _ dogma.AggregateRoot, + s dogma.AggregateCommandScope, + m dogma.Message, + ) { + if _, ok := m.(MessageR); ok { + s.RecordEvent(MessageE1) + } + }, + }) + + c.RegisterProcess(&ProcessMessageHandler{ + ConfigureFunc: func(c dogma.ProcessConfigurer) { + c.Identity("", "") + c.ConsumesEventType(MessageE{}) // E = execute a command + c.ConsumesEventType(MessageA{}) // A = also execute a command + c.ProducesCommandType(MessageN{}) + }, + RouteEventToInstanceFunc: func( + context.Context, + dogma.Message, + ) (string, bool, error) { + return "", true, nil + }, + HandleEventFunc: func( + _ context.Context, + s dogma.ProcessEventScope, + m dogma.Message, + ) error { + if _, ok := m.(MessageE); ok { + s.Begin() + s.ExecuteCommand(MessageN1) + } + return nil + }, + }) + }, + } + }) + + executeCommandViaExecutor := func(m dogma.Message) Action { + return Call(func() { + err := test.CommandExecutor().ExecuteCommand(context.Background(), m) + Expect(err).ShouldNot(HaveOccurred()) + }) + } + + recordEventViaRecorder := func(m dogma.Message) Action { + return Call(func() { + err := test.EventRecorder().RecordEvent(context.Background(), m) + Expect(err).ShouldNot(HaveOccurred()) + }) + } + + DescribeTable( + "expectation behavior", + func( + a Action, + e Expectation, + ok bool, + rm reportMatcher, + options ...TestOption, + ) { + test = Begin(testingT, app, options...) + test.Expect(a, e) + rm(testingT) + Expect(testingT.Failed()).To(Equal(!ok)) + }, + Entry( + "event recorded as expected", + recordEventViaRecorder(MessageE1), + ToRecordEvent(MessageE1), + expectPass, + expectReport( + `✓ record a specific 'fixtures.MessageE' event`, + ), + ), + Entry( + "no matching event recorded", + executeCommandViaExecutor(MessageR1), + ToRecordEvent(MessageX1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageX' event`, + ``, + ` | EXPLANATION`, + ` | nothing recorded the expected event`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the '' aggregate message handler`, + ` | • verify the logic within the code that uses the dogma.EventRecorder`, + ), + ), + Entry( + "no messages produced at all", + Call(func() {}), + ToRecordEvent(MessageE1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | no messages were produced at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify the logic within the code that uses the dogma.EventRecorder`, + ), + ), + Entry( + "no events recorded at all", + executeCommandViaExecutor(MessageN1), + ToRecordEvent(MessageE1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | no events were recorded at all`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the '' aggregate message handler`, + ` | • verify the logic within the code that uses the dogma.EventRecorder`, + ), + ), + Entry( + "no matching event recorded and all relevant handler types disabled", + recordEventViaRecorder(MessageA1), + ToRecordEvent(MessageE1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | nothing recorded the expected event`, + ` | `, + ` | SUGGESTIONS`, + ` | • enable aggregate handlers using the EnableHandlerType() option`, + ` | • enable integration handlers using the EnableHandlerType() option`, + ` | • verify the logic within the code that uses the dogma.EventRecorder`, + ), + WithUnsafeOperationOptions( + engine.EnableAggregates(false), + ), + ), + Entry( + "similar event recorded with a different type", + recordEventViaRecorder(MessageE1), + ToRecordEvent(&MessageE1), // note: message type is pointer + expectFail, + expectReport( + `✗ record a specific '*fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | an event of a similar type was recorded via a dogma.EventRecorder`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageE{`, + ` | Value: "E1"`, + ` | }`, + ), + ), + Entry( + "similar event recorded with a different value", + recordEventViaRecorder(MessageE1), + ToRecordEvent(MessageE2), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageE' event`, + ``, + ` | EXPLANATION`, + ` | a similar event was recorded via a dogma.EventRecorder`, + ` | `, + ` | SUGGESTIONS`, + ` | • check the content of the message`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageE{`, + ` | Value: "E[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "expected message executed as a command rather than recorded as an event", + executeCommandViaExecutor(MessageR1), + ToRecordEvent(MessageR1), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageR' event`, + ``, + ` | EXPLANATION`, + ` | the expected message was executed as a command via a dogma.CommandExecutor`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that a command of this type was intended to be executed via a dogma.CommandExecutor`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ), + ), + Entry( + "similar message with a different value executed as a command rather than recorded as an event", + executeCommandViaExecutor(MessageR1), + ToRecordEvent(MessageR2), + expectFail, + expectReport( + `✗ record a specific 'fixtures.MessageR' event`, + ``, + ` | EXPLANATION`, + ` | a similar message was executed as a command via a dogma.CommandExecutor`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that a command of this type was intended to be executed via a dogma.CommandExecutor`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ` | `, + ` | MESSAGE DIFF`, + ` | fixtures.MessageR{`, + ` | Value: "R[-2-]{+1+}"`, + ` | }`, + ), + ), + Entry( + "similar message with a different type executed as a command rather than recorded as an event", + executeCommandViaExecutor(MessageR1), + ToRecordEvent(&MessageR1), // note: message type is pointer + expectFail, + expectReport( + `✗ record a specific '*fixtures.MessageR' event`, + ``, + ` | EXPLANATION`, + ` | a message of a similar type was executed as a command via a dogma.CommandExecutor`, + ` | `, + ` | SUGGESTIONS`, + ` | • verify that a command of this type was intended to be executed via a dogma.CommandExecutor`, + ` | • verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?`, + ` | • check the message type, should it be a pointer?`, + ` | `, + ` | MESSAGE DIFF`, + ` | [-*-]fixtures.MessageR{`, + ` | Value: "R1"`, + ` | }`, + ), + ), + ) +}) diff --git a/assert/message.go b/expectation.message.go similarity index 50% rename from assert/message.go rename to expectation.message.go index 436ae94f..da327fde 100644 --- a/assert/message.go +++ b/expectation.message.go @@ -1,7 +1,6 @@ -package assert +package testkit import ( - "fmt" "reflect" "github.com/dogmatiq/configkit/message" @@ -13,39 +12,34 @@ import ( "github.com/dogmatiq/testkit/report" ) -// CommandExecuted returns an assertion that passes if m is executed as a -// command. -func CommandExecuted(m dogma.Message) Assertion { - if err := dogma.ValidateMessage(m); err != nil { - panic(fmt.Sprintf( - "can not assert that this command will be executed, it is invalid: %s", - err, - )) +// ToExecuteCommand returns an expectation that passes if a command is executed +// that is equal to m. +func ToExecuteCommand(m dogma.Message) Expectation { + if m == nil { + panic("ToExecuteCommand(): message must not be nil") } - return &messageAssertion{ + return &messageExpectation{ expected: m, role: message.CommandRole, } } -// EventRecorded returns an assertion that passes if m is recorded as an event. -func EventRecorded(m dogma.Message) Assertion { - if err := dogma.ValidateMessage(m); err != nil { - panic(fmt.Sprintf( - "can not assert that this event will be recorded, it is invalid: %s", - err, - )) +// ToRecordEvent returns an expectation that passes if an event is recorded that +// is equal to m. +func ToRecordEvent(m dogma.Message) Expectation { + if m == nil { + panic("ToRecordEvent(): message must not be nil") } - return &messageAssertion{ + return &messageExpectation{ expected: m, role: message.EventRole, } } -// messageAssertion asserts that a specific message is produced. -type messageAssertion struct { +// messageExpectation verifies that a specific message is produced. +type messageExpectation struct { // Expected is the message that is expected to be produced. expected dogma.Message @@ -56,13 +50,13 @@ type messageAssertion struct { // cmp is the comparator used to compare messages for equality. cmp compare.Comparator - // ok is true once the assertion is deemed to have passed, after which no + // ok is true once the expectation is deemed to have passed, after which no // further updates are performed. ok bool - // best is an envelope containing the "best-match" message for an assertion - // that has not yet passed. Note that this message may not have the expected - // role. + // best is an envelope containing the "best-match" message for an + // expectation that has not yet passed. Note that this message may not have + // the expected role. best *envelope.Envelope // sim is a ranking of the similarity between the type of the expected @@ -70,8 +64,8 @@ type messageAssertion struct { sim compare.TypeSimilarity // equal is true if the best-match message compared as equal to the expected - // message. This can occur, and the assertion still fail, if the best-match - // message has an unexpected role. + // message. This can occur, and the expectation still fail, if the + // best-match message has an unexpected role. equal bool // tracker observers the handlers and messages that are involved in the @@ -84,153 +78,152 @@ type messageAssertion struct { // // The banner text should be in uppercase, and complete the sentence "The // application is expected ...". For example, "TO DO A THING". -func (a *messageAssertion) Banner() string { +func (e *messageExpectation) Banner() string { return inflect.Sprintf( - a.role, + e.role, "TO A SPECIFIC '%s' ", - message.TypeOf(a.expected), + message.TypeOf(e.expected), ) } -// Begin is called to prepare the assertion for a new test. -func (a *messageAssertion) Begin(o ExpectOptionSet) { - // reset everything - *a = messageAssertion{ - expected: a.expected, - role: a.role, +// Begin is called to prepare the expectation for a new test. +func (e *messageExpectation) Begin(o ExpectOptionSet) { + *e = messageExpectation{ + expected: e.expected, + role: e.role, cmp: o.MessageComparator, tracker: tracker{ - role: a.role, + role: e.role, matchDispatchCycle: o.MatchMessagesInDispatchCycle, }, } } // End is called once the test is complete. -func (a *messageAssertion) End() { +func (e *messageExpectation) End() { } -// Ok returns true if the assertion passed. -func (a *messageAssertion) Ok() bool { - return a.ok +// Ok returns true if the expectation passed. +func (e *messageExpectation) Ok() bool { + return e.ok } -// BuildReport generates a report about the assertion. +// BuildReport generates a report about the expectation. // -// ok is true if the assertion is considered to have passed. This may not be the -// same value as returned from Ok() when this assertion is used as a -// sub-assertion inside a composite. -func (a *messageAssertion) BuildReport(ok bool) *Report { +// ok is true if the expectation is considered to have passed. This may not be +// the same value as returned from Ok() when this expectation is used as a child +// of a composite expectation. +func (e *messageExpectation) BuildReport(ok bool) *Report { rep := &Report{ TreeOk: ok, - Ok: a.ok, + Ok: e.ok, Criteria: inflect.Sprintf( - a.role, + e.role, " a specific '%s' ", - message.TypeOf(a.expected), + message.TypeOf(e.expected), ), } - if ok || a.ok { + if ok || e.ok { return rep } - if a.best == nil { - buildResultNoMatch(rep, &a.tracker) - } else if a.best.Role == a.role { - a.buildResultExpectedRole(rep) + if e.best == nil { + buildReportNoMatch(rep, &e.tracker) + } else if e.best.Role == e.role { + e.buildReportExpectedRole(rep) } else { - a.buildResultUnexpectedRole(rep) + e.buildReportUnexpectedRole(rep) } return rep } -// Notify updates the assertion's state in response to a new fact. -func (a *messageAssertion) Notify(f fact.Fact) { - if a.ok { +// Notify updates the expectation's state in response to a new fact. +func (e *messageExpectation) Notify(f fact.Fact) { + if e.ok { return } - a.tracker.Notify(f) + e.tracker.Notify(f) switch x := f.(type) { case fact.DispatchCycleBegun: - if a.tracker.matchDispatchCycle { - a.messageProduced(x.Envelope) + if e.tracker.matchDispatchCycle { + e.messageProduced(x.Envelope) } case fact.EventRecordedByAggregate: - a.messageProduced(x.EventEnvelope) + e.messageProduced(x.EventEnvelope) case fact.EventRecordedByIntegration: - a.messageProduced(x.EventEnvelope) + e.messageProduced(x.EventEnvelope) case fact.CommandExecutedByProcess: - a.messageProduced(x.CommandEnvelope) + e.messageProduced(x.CommandEnvelope) } } -// messageProduced updates the assertion's state to reflect the fact that a +// messageProduced updates the expectation's state to reflect the fact that a // message has been produced. -func (a *messageAssertion) messageProduced(env *envelope.Envelope) { - if !a.cmp.MessageIsEqual(env.Message, a.expected) { - a.updateBestMatch(env) +func (e *messageExpectation) messageProduced(env *envelope.Envelope) { + if !e.cmp.MessageIsEqual(env.Message, e.expected) { + e.updateBestMatch(env) return } - a.best = env - a.sim = compare.SameTypes - a.equal = true + e.best = env + e.sim = compare.SameTypes + e.equal = true - if a.role == env.Role { - a.ok = true + if e.role == env.Role { + e.ok = true } } -// updateBestMatch replaces a.best with env if it is a better match. -func (a *messageAssertion) updateBestMatch(env *envelope.Envelope) { +// updateBestMatch replaces e.best with env if it is a better match. +func (e *messageExpectation) updateBestMatch(env *envelope.Envelope) { sim := compare.FuzzyTypeComparison( - reflect.TypeOf(a.expected), + reflect.TypeOf(e.expected), reflect.TypeOf(env.Message), ) - if sim > a.sim { - a.best = env - a.sim = sim + if sim > e.sim { + e.best = env + e.sim = sim } } -// buildResultExpectedRole builds the assertion result when there is a -// "best-match" message available and it is of the expected role. -func (a *messageAssertion) buildResultExpectedRole(rep *Report) { +// buildReportExpectedRole builds a test report when there is a "best-match" +// message available and it is of the expected role. +func (e *messageExpectation) buildReportExpectedRole(rep *Report) { s := rep.Section(suggestionsSection) - if a.sim == compare.SameTypes { - if a.best.Origin == nil { + if e.sim == compare.SameTypes { + if e.best.Origin == nil { rep.Explanation = inflect.Sprint( - a.role, + e.role, "a similar was via a ", ) } else { rep.Explanation = inflect.Sprintf( - a.role, + e.role, "a similar was by the '%s' %s message handler", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, ) } s.AppendListItem("check the content of the message") } else { - if a.best.Origin == nil { + if e.best.Origin == nil { rep.Explanation = inflect.Sprint( - a.role, + e.role, "a of a similar type was via a ", ) } else { rep.Explanation = inflect.Sprintf( - a.role, + e.role, "a of a similar type was by the '%s' %s message handler", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, ) } @@ -240,89 +233,89 @@ func (a *messageAssertion) buildResultExpectedRole(rep *Report) { s.AppendListItem("check the message type, should it be a pointer?") } - a.buildDiff(rep) + e.buildDiff(rep) } // buildDiff adds a "message diff" section to the result. -func (a *messageAssertion) buildDiff(rep *Report) { +func (e *messageExpectation) buildDiff(rep *Report) { report.WriteDiff( &rep.Section("Message Diff").Content, - report.RenderMessage(a.expected), - report.RenderMessage(a.best.Message), + report.RenderMessage(e.expected), + report.RenderMessage(e.best.Message), ) } -// buildResultUnexpectedRole builds the assertion result when there is a -// "best-match" message available but it is of an unexpected role. -func (a *messageAssertion) buildResultUnexpectedRole(rep *Report) { +// buildReportExpectedRole builds a test report when there is a "best-match" +// message available but it is of an unexpected role. +func (e *messageExpectation) buildReportUnexpectedRole(rep *Report) { s := rep.Section(suggestionsSection) - if a.best.Origin == nil { + if e.best.Origin == nil { s.AppendListItem(inflect.Sprint( - a.best.Role, + e.best.Role, "verify that a of this type was intended to be via a ", )) } else { s.AppendListItem(inflect.Sprintf( - a.best.Role, + e.best.Role, "verify that the '%s' %s message handler intended to a of this type", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, )) } - if a.role == message.CommandRole { - s.AppendListItem("verify that CommandExecuted() is the correct assertion, did you mean EventRecorded()?") + if e.role == message.CommandRole { + s.AppendListItem("verify that ToExecuteCommand() is the correct expectation, did you mean ToRecordEvent()?") } else { - s.AppendListItem("verify that EventRecorded() is the correct assertion, did you mean CommandExecuted()?") + s.AppendListItem("verify that ToRecordEvent() is the correct expectation, did you mean ToExecuteCommand()?") } // the "best-match" is equal to the expected message. this means that only // the roles were mismatched. - if a.equal { - if a.best.Origin == nil { + if e.equal { + if e.best.Origin == nil { rep.Explanation = inflect.Sprint( - a.best.Role, + e.best.Role, "the expected message was as a via a ", ) } else { rep.Explanation = inflect.Sprintf( - a.best.Role, + e.best.Role, "the expected message was as a by the '%s' %s message handler", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, ) } return // skip diff rendering } - if a.sim == compare.SameTypes { - if a.best.Origin == nil { + if e.sim == compare.SameTypes { + if e.best.Origin == nil { rep.Explanation = inflect.Sprint( - a.best.Role, + e.best.Role, "a similar message was as a via a ", ) } else { rep.Explanation = inflect.Sprintf( - a.best.Role, + e.best.Role, "a similar message was as a by the '%s' %s message handler", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, ) } } else { - if a.best.Origin == nil { + if e.best.Origin == nil { rep.Explanation = inflect.Sprint( - a.best.Role, + e.best.Role, "a message of a similar type was as a via a ", ) } else { rep.Explanation = inflect.Sprintf( - a.best.Role, + e.best.Role, "a message of a similar type was as a by the '%s' %s message handler", - a.best.Origin.Handler.Identity().Name, - a.best.Origin.HandlerType, + e.best.Origin.Handler.Identity().Name, + e.best.Origin.HandlerType, ) } @@ -332,5 +325,5 @@ func (a *messageAssertion) buildResultUnexpectedRole(rep *Report) { s.AppendListItem("check the message type, should it be a pointer?") } - a.buildDiff(rep) + e.buildDiff(rep) } diff --git a/expectation.messagecommon.go b/expectation.messagecommon.go index c55f16d3..8375cbb5 100644 --- a/expectation.messagecommon.go +++ b/expectation.messagecommon.go @@ -6,14 +6,13 @@ import ( "github.com/dogmatiq/configkit" "github.com/dogmatiq/configkit/message" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine/fact" "github.com/dogmatiq/testkit/internal/inflect" ) // buildReportNoMatch is used by message-related expectations to build a test // report when no "best-match" message is found. -func buildReportNoMatch(rep *assert.Report, t *tracker) { +func buildReportNoMatch(rep *Report, t *tracker) { s := rep.Section(suggestionsSection) allDisabled := true diff --git a/expectation.messagetype.commandcall_test.go b/expectation.messagetype.commandcall_test.go index 8414648f..9a834c93 100644 --- a/expectation.messagetype.commandcall_test.go +++ b/expectation.messagetype.commandcall_test.go @@ -106,11 +106,11 @@ var _ = Describe("func ToExecuteCommandOfType() (when used with the Call() actio }, Entry( "command type executed as expected", - recordEventViaRecorder(MessageE1), - ToExecuteCommandOfType(MessageC{}), + executeCommandViaExecutor(MessageR1), + ToExecuteCommandOfType(MessageR{}), expectPass, expectReport( - `✓ execute any 'fixtures.MessageC' command`, + `✓ execute any 'fixtures.MessageR' command`, ), ), Entry( diff --git a/expectation.messagetype.eventcall_test.go b/expectation.messagetype.eventcall_test.go index d930ce89..d5369a5c 100644 --- a/expectation.messagetype.eventcall_test.go +++ b/expectation.messagetype.eventcall_test.go @@ -109,7 +109,7 @@ var _ = Describe("func ToRecordEventOfType() (when used with the Call() action)" }, Entry( "event type recorded as expected", - executeCommandViaExecutor(MessageR1), + recordEventViaRecorder(MessageE1), ToRecordEventOfType(MessageE{}), expectPass, expectReport( diff --git a/expectation.messagetype.go b/expectation.messagetype.go index 4a92cb6e..59d52d22 100644 --- a/expectation.messagetype.go +++ b/expectation.messagetype.go @@ -3,7 +3,6 @@ package testkit import ( "github.com/dogmatiq/configkit/message" "github.com/dogmatiq/dogma" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/compare" "github.com/dogmatiq/testkit/engine/envelope" "github.com/dogmatiq/testkit/engine/fact" @@ -11,7 +10,7 @@ import ( "github.com/dogmatiq/testkit/report" ) -// ToExecuteCommandOfType returns an expectation that passes a command of the +// ToExecuteCommandOfType returns an expectation that passes if a command of the // same type as m is executed. func ToExecuteCommandOfType(m dogma.Message) Expectation { if m == nil { @@ -102,8 +101,8 @@ func (e *messageTypeExpectation) Ok() bool { // ok is true if the expectation is considered to have passed. This may not be // the same value as returned from Ok() when this expectation is used as a child // of a composite expectation. -func (e *messageTypeExpectation) BuildReport(ok bool) *assert.Report { - rep := &assert.Report{ +func (e *messageTypeExpectation) BuildReport(ok bool) *Report { + rep := &Report{ TreeOk: ok, Ok: e.ok, Criteria: inflect.Sprintf( @@ -169,7 +168,7 @@ func (e *messageTypeExpectation) messageProduced(env *envelope.Envelope) { } // buildDiff adds a "message type diff" section to the result. -func (e *messageTypeExpectation) buildDiff(rep *assert.Report) { +func (e *messageTypeExpectation) buildDiff(rep *Report) { report.WriteDiff( &rep.Section("Message Type Diff").Content, e.expected.String(), @@ -178,8 +177,8 @@ func (e *messageTypeExpectation) buildDiff(rep *assert.Report) { } // buildReportExpectedRole builds a test report when there is a "best-match" -// message available but it is of an unexpected role. -func (e *messageTypeExpectation) buildReportExpectedRole(rep *assert.Report) { +// message available and it is of the expected role. +func (e *messageTypeExpectation) buildReportExpectedRole(rep *Report) { s := rep.Section(suggestionsSection) if e.best.Origin == nil { @@ -205,7 +204,7 @@ func (e *messageTypeExpectation) buildReportExpectedRole(rep *assert.Report) { // buildReportUnexpectedRole builds a test report when there is a "best-match" // message available but it does not have the expected role. -func (e *messageTypeExpectation) buildReportUnexpectedRole(rep *assert.Report) { +func (e *messageTypeExpectation) buildReportUnexpectedRole(rep *Report) { s := rep.Section(suggestionsSection) if e.best.Origin == nil { diff --git a/expectation.satisfy.go b/expectation.satisfy.go index 5f76bfdc..24f723a1 100644 --- a/expectation.satisfy.go +++ b/expectation.satisfy.go @@ -7,7 +7,6 @@ import ( "strings" "sync" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine/fact" ) @@ -89,8 +88,8 @@ func (e *satisfyExpectation) Ok() bool { // ok is true if the expectation is considered to have passed. This may not be // the same value as returned from Ok() when this expectation is used as a child // of a composite expectation. -func (e *satisfyExpectation) BuildReport(ok bool) *assert.Report { - rep := &assert.Report{ +func (e *satisfyExpectation) BuildReport(ok bool) *Report { + rep := &Report{ TreeOk: ok, Ok: e.Ok(), Criteria: e.c, @@ -239,7 +238,7 @@ func (t *SatisfyT) Helper() { } // Log formats its arguments using default formatting, analogous to Println(), -// and records the text in the assertion report. +// and records the text in the test report. func (t *SatisfyT) Log(args ...interface{}) { t.m.Lock() defer t.m.Unlock() @@ -248,7 +247,7 @@ func (t *SatisfyT) Log(args ...interface{}) { } // Logf formats its arguments according to the format, analogous to Printf(), -// and records the text in the assertion report. +// and records the text in the test report. func (t *SatisfyT) Logf(format string, args ...interface{}) { t.m.Lock() defer t.m.Unlock() diff --git a/expectation_test.go b/expectation_test.go index 8326024a..0363214b 100644 --- a/expectation_test.go +++ b/expectation_test.go @@ -2,7 +2,6 @@ package testkit_test import ( . "github.com/dogmatiq/testkit" - "github.com/dogmatiq/testkit/assert" "github.com/dogmatiq/testkit/engine/fact" ) @@ -30,13 +29,13 @@ func (a staticExpectation) Begin(ExpectOptionSet) {} func (a staticExpectation) End() {} func (a staticExpectation) Ok() bool { return bool(a) } func (a staticExpectation) Notify(fact.Fact) {} -func (a staticExpectation) BuildReport(ok bool) *assert.Report { +func (a staticExpectation) BuildReport(ok bool) *Report { c := "" if a { c = "" } - return &assert.Report{ + return &Report{ TreeOk: ok, Ok: bool(a), Criteria: c, diff --git a/report.go b/report.go index 5f0571cd..d8b5e3fa 100644 --- a/report.go +++ b/report.go @@ -1,5 +1,14 @@ package testkit +import ( + "io" + "strings" + + "github.com/dogmatiq/iago/count" + "github.com/dogmatiq/iago/indent" + "github.com/dogmatiq/iago/must" +) + const ( // suggestionsSection is the heading for the section of the test report // where suggestions about how to fix failed tests are shown. @@ -9,3 +18,145 @@ const ( // log messages from user-defined expectations are shown. logSection = "Log Messages" ) + +// Report is a report on the outcome of an expectation. +type Report struct { + // TreeOk is true if the "tree" that the expectation belongs to passed. + TreeOk bool + + // Ok is true if this expectation passed. + Ok bool + + // Criteria is a brief description of the expectation's requirement to pass. + Criteria string + + // Outcome is a brief description of the outcome of the expectation. + Outcome string + + // Explanation is a brief description of what actually happened during the + // test as it relates to this expectation. + Explanation string + + // Sections contains arbitrary "sections" that are added to the report by + // the expectation. + Sections []*ReportSection + + // SubReports contains the reports of any child expectations. + SubReports []*Report +} + +// Section adds an arbitrary "section" to the report. +func (r *Report) Section(title string) *ReportSection { + for _, s := range r.Sections { + if s.Title == title { + return s + } + } + + s := &ReportSection{ + Title: title, + } + + r.Sections = append(r.Sections, s) + + return s +} + +// Append adds sr as a sub-report of s. +func (r *Report) Append(sr *Report) { + r.SubReports = append(r.SubReports, sr) +} + +// WriteTo writes the report to the given writer. +func (r *Report) WriteTo(next io.Writer) (_ int64, err error) { + defer must.Recover(&err) + w := count.NewWriter(next) + + if r.Ok { + must.WriteString(w, "✓ ") + } else { + must.WriteString(w, "✗ ") + } + + must.WriteString(w, r.Criteria) + + if r.Outcome != "" { + must.WriteString(w, " (") + must.WriteString(w, r.Outcome) + must.WriteByte(w, ')') + } + + must.WriteByte(w, '\n') + + if len(r.Sections) != 0 || r.Explanation != "" { + must.WriteByte(w, '\n') + + iw := indent.NewIndenter(w, sectionsIndent) + + if r.Explanation != "" { + must.WriteString(iw, "EXPLANATION\n") + + must.WriteString( + iw, + indent.String(r.Explanation, sectionContentIndent), + ) + + must.WriteByte(iw, '\n') + + if len(r.Sections) != 0 { + must.WriteByte(iw, '\n') + } + } + + for i, s := range r.Sections { + must.WriteString(iw, strings.ToUpper(s.Title)) + must.WriteString(iw, "\n") + + must.WriteString( + iw, + indent.String( + strings.TrimSpace(s.Content.String()), + sectionContentIndent, + ), + ) + + must.WriteByte(iw, '\n') + + if i < len(r.Sections)-1 { + must.WriteByte(iw, '\n') + } + } + } + + if len(r.SubReports) != 0 { + iw := indent.NewIndenter(w, subReportsIndent) + for _, sr := range r.SubReports { + must.WriteTo(iw, sr) + } + } + + return int64(w.Count()), nil +} + +// ReportSection is a section of a report containing additional information +// about the expectation. +type ReportSection struct { + Title string + Content strings.Builder +} + +// Append appends a line of text to the section's content. +func (s *ReportSection) Append(f string, v ...interface{}) { + must.Fprintf(&s.Content, f+"\n", v...) +} + +// AppendListItem appends a line of text prefixed with a bullet. +func (s *ReportSection) AppendListItem(f string, v ...interface{}) { + s.Append("• "+f, v...) +} + +var ( + sectionsIndent = []byte(" | ") + sectionContentIndent = " " + subReportsIndent = []byte(" ") +) diff --git a/report_test.go b/report_test.go index cef3ae92..475916f2 100644 --- a/report_test.go +++ b/report_test.go @@ -17,7 +17,7 @@ func expectReport(expected ...string) reportMatcher { return func(t *testingmock.T) { // Scan through the logs until we find the start of the test report, - // then assert that the remainder of the log content matches our + // then verify that the remainder of the log content matches our // expectation. for i, l := range t.Logs { if l == "--- TEST REPORT ---" { diff --git a/test.go b/test.go index fe73d97f..255444f4 100644 --- a/test.go +++ b/test.go @@ -76,8 +76,8 @@ func BeginContext( return test } -// Prepare performs a group of actions without making any assertions in order -// to place the application into a particular state. +// Prepare performs a group of actions without making any expectations. It is +// used to place the application into a particular state. func (t *Test) Prepare(actions ...Action) *Test { t.t.Helper()