{"id":25862804,"url":"https://github.com/hbomb79/go-chanassert","last_synced_at":"2025-08-24T02:18:04.504Z","repository":{"id":226843106,"uuid":"769782591","full_name":"hbomb79/go-chanassert","owner":"hbomb79","description":"Declarative, flexible, and asynchronous assertions for channels in Go 🎉 Zero dependencies","archived":false,"fork":false,"pushed_at":"2024-04-18T04:17:32.000Z","size":115,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-18T12:01:44.176Z","etag":null,"topics":["asynchronous","go","golang","testing","testing-library","testing-tools"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hbomb79.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-03-10T03:26:17.000Z","updated_at":"2024-04-18T04:24:30.000Z","dependencies_parsed_at":"2024-04-17T10:41:41.786Z","dependency_job_id":null,"html_url":"https://github.com/hbomb79/go-chanassert","commit_stats":null,"previous_names":["hbomb79/go-chanassert"],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbomb79%2Fgo-chanassert","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbomb79%2Fgo-chanassert/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbomb79%2Fgo-chanassert/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hbomb79%2Fgo-chanassert/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hbomb79","download_url":"https://codeload.github.com/hbomb79/go-chanassert/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241439764,"owners_count":19963100,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asynchronous","go","golang","testing","testing-library","testing-tools"],"created_at":"2025-03-01T23:56:48.427Z","updated_at":"2025-03-01T23:56:49.323Z","avatar_url":"https://github.com/hbomb79.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Chan Assert\n#### Asynchronous Channel Assertion Library\n[![Go Reference](https://pkg.go.dev/badge/github.com/hbomb79/go-chanassert.svg)](https://pkg.go.dev/github.com/hbomb79/go-chanassert)\n![coverage](https://raw.githubusercontent.com/hbomb79/go-chanassert/badges/.badges/main/coverage.svg)\n\nChan Assert is a declartive library designed to help you when writing tests which need to assert messages arriving through a channel. It's completely\ngeneric over the type of the messages, and has an intuitive API for declaring the behaviour you expect. Additionally, extending chanassert to cover\ncomplex testing demands is easy.\n\n#### Usage\nWith chanassert, you declare your expectations of the channel beforehand. Then, after your test has finished doing it's 'work', you ask the expecter if it's satisfied.\n\nIf the expecter did not see the messages it expected to see (or saw messages it did NOT expect to see), your test will be failed with a detailed error message of what went wrong.\n\n###### Example\nIf we wanted to setup an expecter which wants to see both `\"hello\"` and `\"world\"` strings pass through the expecter, you may declare your expecter like so\n\n```golang\nfunc Test_Xyz(t *testing.T) {\n    expecter := chanassert.NewChannelExpecter(ch).\n        Expect(chanassert.AllOf(\n            chanassert.MatchEqual(\"hello\"),\n            chanassert.MatchEqual(\"world\"),\n        ))\n    expecter.Listen()\n    defer expecter.AssertSatisfied(t, time.Second)\n\n    // Your test code here\n}\n```\n\nIf the above expecter sees messages it does not recognise over the `ch` channel, or did not see *both* `\"hello\"` and `\"world\"`, then the expecter will\nnot be satisfied, and will cause the test to fail.\n\nLet's breakdown what's going on here:\n - `Expect` defines a new 'layer', which is a concept in chanassert which enables you to define an ordering to your expectations.\n - `AllOf` is a 'combiner', which allows you to combine multiple matchers together.\n - `MatchEqual` is an example of a matcher. It takes in a value and will 'accept' a message only if it equals the value you provide (only available if your expecter is generic over a `comparable` type).\n - `Listen` starts the expecter, which will start a goroutine which consumes messages from the channel until the expecter closes (more on that in 'Lifecycle of the Expecter')\n - `AssertSatisfied` will wait for the expecter to close (or force-close it after the timeout provided), and checks for any errors recorded by the expecter. If any are found, the given `testing.T` will be failed.\n\n#### Key Concepts\nThe example above introduces a lot of the concepts which you'll need to understand to deploy chanassert effectively in your tests. Let's take a moment to cover them in some more detail.\n\n##### Layers\nLayers allow you to define an ordering to your expectations. Only one layer is active at a time, and the first layer is made active when the expecter starts. All layers accept an arbritrary number of combiners, and will become satisfied differently depending on the type of layer you're using.\n\nLayers can be defined using 4 methods on your expecter:\n- `Expect(combiners...)`, which will become satisfied when all the combiners provided are satisifed,\n- `ExpectAny(combiners...)`, which will become satisfied when any of the combiners provided are satisfied,\n- `ExpectTimeout(timeout, combiners...)` which is the same as `Expect`, but with a timeout,\n- `ExpectAnyTimeout(timeout, combiners...)`, which is the same as `ExpectAny`, but with a timeout.\n\n\u003e [!IMPORTANT]\n\u003e A layers 'timeout' (if any) only starts once the layer becomes active. You do not need to compensate for timeouts from previous layers.\n\nMost of the time you may only need one layer, however multiple layers can be added to an expecter for times when you need to establish 'and then...' semantics to your expectations.\n\nIt's important to re-iterate: messages are _only_ delivered to the **active layer**. Subsequent layers will only be used once the current layer is satisfied, and a layer will never be used\nby the expecter once it's become satisfied.\n\n---\n##### Combiners\nCombiners provide a way to combine multiple matchers together using a number of flexible and powerful behaviours. Combiners take\nin some matchers (and some additional paramaters, depending on the combiner), and perform combination logic on your matchers.\n\n\u003e[!TIP]\n\u003e This is a non-exhaustive look at the high-level of combiners. The [Go Reference](https://pkg.go.dev/github.com/hbomb79/go-chanassert), [testing code](combiner_test.go), and [source code](combiner.go) are all excellent resources for understanding how each combiner behaves in detail.\n\nFor example, say I expect to see `\"hello\"` come over the channel at least 5 times, but no more than 7 times, this is as simple\nas using the `BetweenNOf()` combiner like so:\n\n```golang\nchanassert.NewChannelExpecter(ch).Expect(\n    chanassert.BetweenNOf(5, 7, chanassert.MatchEqual(\"hello\")),\n)\n```\n\nAll combiners in chanassert fall in to one of three modes: `sum`, `each` and `any`. These modes dictate how a combiner becomes _satisfied_ and _saturated_.\n\n###### Satisfied and Saturated\nSo far we've been talking a lot about when a combiner becomes 'satisfied', but combiners also track a concept called _saturation_.\n\nEssentially, if satisfied means the combiner has seen the minimum quantity of messages to satisfy it's requirements, then saturated is an indication of whether the combiner can match any _more_\nmessages. This means satisfied is related to the `min` of a combiner, whereas saturated is all about it's `max`.\n\nOnce saturated, the combiner will simply _reject_ all incoming messages. This means that combiners, once satisfied, _stay\nsatisfied_ as they cannot exceed their maximum... If a combiner rejects a message due to being saturated, and no subsequent combiners can match it, then the message will be rejected and\nthe expecter will fail (which is a good thing, as it indicates your channel did NOT meet the expectations you set).\n\nA combiners satisfaction and saturation state are recorded in the [expecter trace](#tracing) so that you can debug exactly why a message was rejected.\n\n###### Sum Combiners\nSum combiners are perhaps the simplest type to understand, they become satisfied based on the **cumulative sum** of all matches it's seen.\n\nTo identify a sum combiner, you can look at it's name. Any combiner that ends in `NOf` is a sum-type combiner.\n\n\u003e[!TIP]\n\u003e The `OneOf(matcher...)` combiner is actually shorthand for the `ExactlyNOf(1, matcher...)` combiner, so `OneOf` is _also_ a sum-type combiner.\n\nAs an example of the 'sum' semantics, say I had a combiner like `BetweenNOf(5,7, MatchEqual(\"foo\"), MatchEqual(\"bar\"))`. This combiner\nwould become satisfied after seeing any combination of 5 messages match against it's matchers. That is to say that all of these scenarios would make the combiner satisfied:\n- `\"foo\"`, `\"bar\"`, `\"foo\"`, `\"bar\"`, `\"foo\"`\n- `\"foo\"`, `\"foo\"`, `\"foo\"`, `\"foo\"`, `\"foo\"`\n- `\"bar\"`, `\"bar\"`, `\"bar\"`, `\"bar\"`, `\"bar\"`\n\nWhile 5 matches satisfies the combiner, two more messages (either `\"foo\"` or `\"bar\"`) could be sent before the combiner hits it's maximum number\nof matches (7 in this case) and stops accepting more messages (i.e. becomes saturated).\n\n###### Each Combiners\nNext up, _each_ type combiners. These allow you to set requirements for the number of message matches that must occur for _each_ of the matchers\nyou provide. You can identify each-type combiners by looking at the name; any combiner which ends in `NOfEach` is an each-type combiner.\n\n\u003e[!TIP]\n\u003e The `AllOf(matcher...)` combiner is actually shorthand for `ExactlyNOfEach(1, matcher...)`, so `AllOf` is also an each-type combiner.\n\nAs an example, let's say I have a combiner like `AtLeastNOfEach(2, MatchEqual(\"hello\"), MatchEqual(\"world\"))`, this combiner will only be satisfied\nonce it's seen 2 or more matches for both `\"hello\"` _and_ `\"world\"`. The order that these messages arrive is not important.\n\nEach-type combiners also track saturation. Take this combiner for example: `BetweenNOfEach(1,2,MatchEqual(\"hello\"), MatchEqual(\"world\"))`, once\na matcher has matched against it's maximum number of messages (2), it will not be allowed to match against any more.\n\n###### Any Combiners\nFinally, _any_ type combiners. If you think of each type combiners as being 'AND', then any type combiners are like an 'OR'. That is to say,\nthe rules are basically the same, except that it becomes satisfied once _any_ of the matchers have matched against the minimum number\nof messages.\n\nAdditionally, an any-type combiner will become saturated once _any_ of the matchers have matched against their maximum number of messages.\n\nYou can identify any-type combiners by looking at the name (I hope by now you can see the pattern). If a combiner ends in `NOfAny`, then you've got\nyourself an any-type combiner.\n\n---\n##### Matchers\nMatchers are the building block of your assertions. They are used in conjunction with combiners and layers to define your expectations.\n\nChanassert comes with many matchers, here's a few of the common ones:\n- `MatchEqual`, matches messages which are 'equal' (using `==` comparison, and so only available if your expecter is generic over a `comparable` type).\n- `MatchStruct`, matches messages using deep equality (via reflection),\n- `MatchPredicate`, matches messages using the predicate function,\n- `MatchStructPartial`, matches messages by comparing all _non-zero values_ in the provided struct and checking that the values match those found in the message,\n\nTo see the full set of matchers, check out the [documentation](https://pkg.go.dev/github.com/hbomb79/go-chanassert), or the [matcher test suite](matcher_test.go). If a matcher which behaves how you need isn't available,\ncrafting your own custom matcher is trivially easy to do.\n\n---\n##### Ignore\n`Ignore()` allows you to define matchers on the expecter which are checked for each incoming message over the channel. If the\nmessage matches any of the matchers, it is discarded.\n\n---\n##### Lifecycle of an Expecter\nNow that we understand the fundamental concepts, we can explain how they all link together by discussing the lifecycle of your expecter.\n\nA freshly created expecter (`NewChannelExpecter`) starts 'asleep'. It does not listen to the channel you've provided. First, you must\ncall `.Listen` on the expecter, which starts a goroutine to listen to the channel you provided.\n\nOnce an expecter is listening, it will make it's first layer 'active'. Any time a message is received over the channel, it will be sent to active layer to see if the message matches.\n\nIf the message was _accepted_ by the active layer, we check if the layer is satisfied (meaning all combiners it contains are satisfied);\nif it is, then we select the _next layer_ until there are no more layers left (and thereby making the entire expecter satisfied).\n\nThis loop will continue to run until:\n- the expecter is satisfied (i.e. all layers are satisfied),\n- the expecter is terminated due to exceeding the timeout when using either of `AwaitSatisfied(timeout)` or `AssertSatisfied(t, timeout)`,\n- the channel closes.\n\n\u003e [!NOTE]\n\u003e An expecter becomes satisfied when it's seen all the messages it _expected_ to see, however this does not mean the expecter is without errors. A satisfied expecter may have seen messages it did *not* expect, which is not mutually exclusive with seeing all the messages it *did* expect.\n\n```mermaid\nflowchart TD\n    START[Call .Listen]--\u003e|Select first layer| LISTEN(Wait for message on channel)\n    LISTEN---\u003eIGNORE{Does Match\\nAny Ignores?}\n    IGNORE---\u003e|YES|LISTEN\n    IGNORE---\u003e|NO|ACTION{Message Matches\\n Active Layer?}\n    ACTION--\u003e|REJECT|LISTEN\n    ACTION--\u003e|ACCEPT|CHECK{Is Layer Satisfied?}\n    CHECK--\u003e|NO|LISTEN\n    CHECK--\u003e|YES|INC{Is there\\nanother layer?}\n    INC--\u003e|No More Layers|CLOSE\n    INC--\u003e|Yes|SELECT(Select Next Layer)\n    SELECT--\u003eLISTEN\n    LISTEN--\u003e|Channel Closed|CLOSE(STOP)\n```\n\n---\n##### Tracing\nWhen crafting complex assertions it can start to become difficult to figure out why your test may be failed. Is it because your channel is misbehaving, or is it because your\nassertions aren't quite right...\n\nBy default, Chanassert will print out detailed errors when using `AssertSatisfied(t *testing.T, timeout time.Duration)`. These errors will describe\n*why* the expecter was unhappy, including the trace of any messages which were rejected by the expecter.\n\n\u003e[!TIP]\n\u003e You can also use `AwaitSatisfied(timeout time.Duration)` to get manually access the errors without any automatic error printing.\n\nChanassert provides very detailed tracing capabilities which allow you to view the path each message took as it was processed by\nthe expecter. There are a number of ways to see this trace:\n- When using `AssertSatisfied`, the trace for specific message rejections will be printed using `(*testing.T).Log` automatically,\n    - You can also enable debug-mode by calling `.Debug()` on the expecter, which will print out the entire trace when any failures occur,\n- `PrintTrace` on the expecter (prints formatted trace to stdout),\n- `FPrintTrace`, to print formatted trace to a given `io.Writer`,\n- Access the trace data directly using `ProcessedMessages`.\n\nYou can see some examples of the trace chanassert outputs in the [testdata](/testdata/traces/).\n\n#### More Examples\nPlease check out the testing code, especially for the [layers](layer_test.go) and [expecters](expecter_test.go). You'll find plenty\nof complex examples in there.\n\n##### Motivation\nTesting channel responses can be tricky in certain scenarios, especially when integration testing. This library started out as a\nhelper package for integration testing the \"activity stream\" websocket for [Thea](http://github.com/hbomb79/Thea), with the intent\nof allowing tests to make declarative assertions about what messages come through a specific channel.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhbomb79%2Fgo-chanassert","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhbomb79%2Fgo-chanassert","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhbomb79%2Fgo-chanassert/lists"}