diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b5d38b..70eb349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The format is based on [Keep a Changelog], and this project adheres to - Fixed issue that caused an empty `SUGGESTIONS` section to be printed on test reports in some circumstances. +- `ToExecuteCommandMatching()`, `ToOnlyExecuteCommandsMatching()`, + `ToRecordEventMatching()` and `ToOnlyRecordEventsMatching()` now properly + report impossible assertions when the predicate's argument type is not a + recognized message. ## [0.17.0] - 2024-08-21 diff --git a/expectation.message.command_test.go b/expectation.message.command_test.go index 8ff60a7..ff513b5 100644 --- a/expectation.message.command_test.go +++ b/expectation.message.command_test.go @@ -40,8 +40,6 @@ var _ = g.Describe("func ToExecuteCommand()", func() { ConfigureFunc: func(c dogma.ApplicationConfigurer) { c.Identity("", "ce773269-4ad7-4c7f-a0ff-cda2e5899743") - // Register a process that will execute the commands about which - // we will make assertions using ToExecuteCommand(). c.RegisterProcess(&ProcessMessageHandlerStub{ ConfigureFunc: func(c dogma.ProcessConfigurer) { c.Identity("", "8b4c4701-be92-4b28-83b6-0d69b97fb451") @@ -90,9 +88,6 @@ var _ = g.Describe("func ToExecuteCommand()", func() { }, }) - // Register an integration so that we can test what happens when - // we expect execution of a command that is never executed by - // any handler (only consumed). c.RegisterIntegration(&IntegrationMessageHandlerStub{ ConfigureFunc: func(c dogma.IntegrationConfigurer) { c.Identity("", "49fa7c5f-7682-4743-bf8a-ed96dee2d81a") diff --git a/expectation.messagematch.command_test.go b/expectation.messagematch.command_test.go index d0c0760..c41b7c6 100644 --- a/expectation.messagematch.command_test.go +++ b/expectation.messagematch.command_test.go @@ -27,6 +27,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { CommandThatIsExecuted = CommandStub[TypeC] CommandThatIsNeverExecuted = CommandStub[TypeX] + CommandThatIsOnlyConsumed = CommandStub[TypeO] TimeoutThatIsScheduled = TimeoutStub[TypeT] ) @@ -97,6 +98,15 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { return nil }, }) + + c.RegisterIntegration(&IntegrationMessageHandlerStub{ + ConfigureFunc: func(c dogma.IntegrationConfigurer) { + c.Identity("", "7cf5a7fe-9f69-46be-8c59-cc12c4825aaf") + c.Routes( + dogma.HandlesCommand[CommandThatIsOnlyConsumed](), + ) + }, + }) }, } }) @@ -129,7 +139,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectPass, expectReport( - `✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:122`, + `✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:132`, ), ), g.Entry( @@ -146,7 +156,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectPass, expectReport( - `✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:139`, + `✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:149`, ), ), g.Entry( @@ -159,7 +169,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:156`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:166`, ``, ` | EXPLANATION`, ` | none of the engaged handlers executed a matching command`, @@ -182,7 +192,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:179`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:189`, ``, ` | EXPLANATION`, ` | none of the engaged handlers executed a matching command`, @@ -201,7 +211,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:198`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:208`, ``, ` | EXPLANATION`, ` | no messages were produced at all`, @@ -220,7 +230,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:217`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:227`, ``, ` | EXPLANATION`, ` | no commands were executed at all`, @@ -239,7 +249,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:236`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:246`, ``, ` | EXPLANATION`, ` | no relevant handler types were enabled`, @@ -261,7 +271,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:258`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:268`, ``, ` | EXPLANATION`, ` | none of the engaged handlers executed a matching command`, @@ -283,7 +293,7 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { ), expectFail, expectReport( - `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:280`, + `✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:290`, ``, ` | EXPLANATION`, ` | none of the engaged handlers executed a matching command`, @@ -314,12 +324,63 @@ var _ = g.Describe("func ToExecuteCommandMatching()", func() { expectFail, expectReport( `✗ none of (1 of the expectations passed unexpectedly)`, - ` ✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:305`, - ` ✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:309`, + ` ✓ execute a command that matches the predicate near expectation.messagematch.command_test.go:315`, + ` ✗ execute a command that matches the predicate near expectation.messagematch.command_test.go:319`, ), ), ) + g.It("fails the test if the message type is unrecognized", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToExecuteCommandMatching( + func(CommandStub[TypeU]) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "a command of type stubs.CommandStub[TypeU] can never be executed, the application does not use this message type", + )) + }) + + g.It("fails the test if the message type is not a command", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToExecuteCommandMatching( + func(EventThatExecutesCommand) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "stubs.EventStub[TypeC] is an event, it can never be executed as a command", + )) + }) + + g.It("fails the test if the message type is not produced by any handlers", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToExecuteCommandMatching( + func(CommandThatIsOnlyConsumed) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "no handlers execute commands of type stubs.CommandStub[TypeO], it is only ever consumed", + )) + }) + g.It("panics if the function is nil", func() { Expect(func() { var fn func(dogma.Command) error diff --git a/expectation.messagematch.commandonly_test.go b/expectation.messagematch.commandonly_test.go index 9cb4ac9..db3fa2f 100644 --- a/expectation.messagematch.commandonly_test.go +++ b/expectation.messagematch.commandonly_test.go @@ -20,7 +20,10 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { type ( EventThatExecutesCommands = EventStub[TypeC] - CommandThatIsExecuted = CommandStub[TypeC] + + CommandThatIsExecuted = CommandStub[TypeC] + CommandThatIsNeverExecuted = CommandStub[TypeX] + CommandThatIsOnlyConsumed = CommandStub[TypeO] ) g.BeforeEach(func() { @@ -38,6 +41,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { c.Routes( dogma.HandlesEvent[EventThatExecutesCommands](), dogma.ExecutesCommand[CommandThatIsExecuted](), + dogma.ExecutesCommand[CommandThatIsNeverExecuted](), ) }, RouteEventToInstanceFunc: func( @@ -58,6 +62,15 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { return nil }, }) + + c.RegisterIntegration(&IntegrationMessageHandlerStub{ + ConfigureFunc: func(c dogma.IntegrationConfigurer) { + c.Identity("", "20bf2831-1887-4148-9539-eb7c294e80b6") + c.Routes( + dogma.HandlesCommand[CommandThatIsOnlyConsumed](), + ) + }, + }) }, } }) @@ -86,7 +99,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), expectPass, expectReport( - `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:84`, + `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:97`, ), ), g.Entry( @@ -99,7 +112,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), expectPass, expectReport( - `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:97`, + `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:110`, ), ), g.Entry( @@ -112,7 +125,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), expectPass, expectReport( - `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:109`, + `✓ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:122`, ), ), g.Entry( @@ -125,7 +138,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), expectFail, expectReport( - `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:122`, + `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:135`, ``, ` | EXPLANATION`, ` | none of the 3 relevant commands matched the predicate`, @@ -155,7 +168,7 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), expectFail, expectReport( - `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:145`, + `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:158`, ``, ` | EXPLANATION`, ` | only 1 of 2 relevant commands matched the predicate`, @@ -172,13 +185,13 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { "no executed commands match, using predicate with a more specific type", RecordEvent(EventThatExecutesCommands{}), ToOnlyExecuteCommandsMatching( - func(m CommandStub[TypeX]) error { + func(m CommandThatIsNeverExecuted) error { panic("unexpected call") }, ), expectFail, expectReport( - `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:175`, + `✗ only execute commands that match the predicate near expectation.messagematch.commandonly_test.go:188`, ``, ` | EXPLANATION`, ` | none of the 3 relevant commands matched the predicate`, @@ -193,6 +206,57 @@ var _ = g.Describe("func ToOnlyExecuteCommandsMatching()", func() { ), ) + g.It("fails the test if the message type is unrecognized", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyExecuteCommandsMatching( + func(CommandStub[TypeU]) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "a command of type stubs.CommandStub[TypeU] can never be executed, the application does not use this message type", + )) + }) + + g.It("fails the test if the message type is not a command", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyExecuteCommandsMatching( + func(EventThatExecutesCommands) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "stubs.EventStub[TypeC] is an event, it can never be executed as a command", + )) + }) + + g.It("fails the test if the message type is not produced by any handlers", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyExecuteCommandsMatching( + func(CommandThatIsOnlyConsumed) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "no handlers execute commands of type stubs.CommandStub[TypeO], it is only ever consumed", + )) + }) + g.It("panics if the function is nil", func() { Expect(func() { var fn func(dogma.Command) error diff --git a/expectation.messagematch.event_test.go b/expectation.messagematch.event_test.go index 27e0559..e4a4421 100644 --- a/expectation.messagematch.event_test.go +++ b/expectation.messagematch.event_test.go @@ -334,6 +334,57 @@ var _ = g.Describe("func ToRecordEventMatching()", func() { ), ) + g.It("fails the test if the message type is unrecognized", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToRecordEventMatching( + func(EventStub[TypeU]) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "an event of type stubs.EventStub[TypeU] can never be recorded, the application does not use this message type", + )) + }) + + g.It("fails the test if the message type is not an event", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToRecordEventMatching( + func(CommandThatRecordsEvent) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "stubs.CommandStub[TypeE] is a command, it can never be recorded as an event", + )) + }) + + g.It("fails the test if the message type is not produced by any handlers", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToRecordEventMatching( + func(EventThatExecutesCommand) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "no handlers record events of type stubs.EventStub[TypeC], it is only ever consumed", + )) + }) + g.It("panics if the function is nil", func() { Expect(func() { var fn func(dogma.Event) error diff --git a/expectation.messagematch.eventonly_test.go b/expectation.messagematch.eventonly_test.go index 928fcf8..be8cab3 100644 --- a/expectation.messagematch.eventonly_test.go +++ b/expectation.messagematch.eventonly_test.go @@ -19,7 +19,10 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { type ( CommandThatRecordsEvent = CommandStub[TypeE] - EventThatIsRecorded = EventStub[TypeE] + + EventThatIsRecorded = EventStub[TypeE] + EventThatIsNeverRecorded = EventStub[TypeX] + EventThatIsOnlyConsumed = EventStub[TypeO] ) g.BeforeEach(func() { @@ -37,6 +40,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { c.Routes( dogma.HandlesCommand[CommandThatRecordsEvent](), dogma.RecordsEvent[EventThatIsRecorded](), + dogma.RecordsEvent[EventThatIsNeverRecorded](), ) }, RouteCommandToInstanceFunc: func(dogma.Command) string { @@ -52,6 +56,15 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { s.RecordEvent(EventE3) }, }) + + c.RegisterProjection(&ProjectionMessageHandlerStub{ + ConfigureFunc: func(c dogma.ProjectionConfigurer) { + c.Identity("", "de708f1d-3651-437e-91ae-275a423ecd15") + c.Routes( + dogma.HandlesEvent[EventThatIsOnlyConsumed](), + ) + }, + }) }, } }) @@ -80,7 +93,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), expectPass, expectReport( - `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:78`, + `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:91`, ), ), g.Entry( @@ -93,7 +106,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), expectPass, expectReport( - `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:91`, + `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:104`, ), ), g.Entry( @@ -106,7 +119,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), expectPass, expectReport( - `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:103`, + `✓ only record events that match the predicate near expectation.messagematch.eventonly_test.go:116`, ), ), g.Entry( @@ -119,7 +132,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), expectFail, expectReport( - `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:116`, + `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:129`, ``, ` | EXPLANATION`, ` | none of the 3 relevant events matched the predicate`, @@ -150,7 +163,7 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), expectFail, expectReport( - `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:140`, + `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:153`, ``, ` | EXPLANATION`, ` | only 1 of 2 relevant events matched the predicate`, @@ -168,13 +181,13 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { "no matching events recorded, using predicate with a more specific type", ExecuteCommand(CommandThatRecordsEvent{}), ToOnlyRecordEventsMatching( - func(m EventStub[TypeX]) error { + func(m EventThatIsNeverRecorded) error { panic("unexpected call") }, ), expectFail, expectReport( - `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:171`, + `✗ only record events that match the predicate near expectation.messagematch.eventonly_test.go:184`, ``, ` | EXPLANATION`, ` | none of the 3 relevant events matched the predicate`, @@ -190,6 +203,57 @@ var _ = g.Describe("func ToOnlyRecordEventsMatching()", func() { ), ) + g.It("fails the test if the message type is unrecognized", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyRecordEventsMatching( + func(EventStub[TypeU]) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "an event of type stubs.EventStub[TypeU] can never be recorded, the application does not use this message type", + )) + }) + + g.It("fails the test if the message type is not an event", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyRecordEventsMatching( + func(CommandThatRecordsEvent) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "stubs.CommandStub[TypeE] is a command, it can never be recorded as an event", + )) + }) + + g.It("fails the test if the message type is not produced by any handlers", func() { + test := Begin(testingT, app) + test.Expect( + noop, + ToOnlyRecordEventsMatching( + func(EventThatIsOnlyConsumed) error { + return nil + }, + ), + ) + + Expect(testingT.Failed()).To(BeTrue()) + Expect(testingT.Logs).To(ContainElement( + "no handlers record events of type stubs.EventStub[TypeO], it is only ever consumed", + )) + }) + g.It("panics if the function is nil", func() { Expect(func() { var fn func(dogma.Event) error diff --git a/expectation.messagematch.go b/expectation.messagematch.go index 1bd8834..ba6e659 100644 --- a/expectation.messagematch.go +++ b/expectation.messagematch.go @@ -3,6 +3,7 @@ package testkit import ( "errors" "fmt" + "reflect" "github.com/dogmatiq/configkit/message" "github.com/dogmatiq/dogma" @@ -152,6 +153,13 @@ func (e *messageMatchExpectation[T]) Caption() string { } func (e *messageMatchExpectation[T]) Predicate(s PredicateScope) (Predicate, error) { + t := message.TypeFor[T]() + if t.ReflectType().Kind() != reflect.Interface { + if err := validateRole(s, t, e.expectedRole); err != nil { + return nil, err + } + } + return &messageMatchPredicate[T]{ pred: e.pred, expectedRole: e.expectedRole,