{"id":40545027,"url":"https://github.com/mooreryan/gleam_qcheck","last_synced_at":"2026-01-20T23:36:34.380Z","repository":{"id":233640629,"uuid":"787100682","full_name":"mooreryan/gleam_qcheck","owner":"mooreryan","description":"QuickCheck-inspired property testing with integrated shrinking for Gleam","archived":false,"fork":false,"pushed_at":"2025-11-03T15:50:22.000Z","size":1217,"stargazers_count":36,"open_issues_count":3,"forks_count":5,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-11-03T17:22:44.384Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/qcheck/","language":"Gleam","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mooreryan.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-04-15T22:21:20.000Z","updated_at":"2025-11-03T15:15:50.000Z","dependencies_parsed_at":"2024-04-28T03:52:45.964Z","dependency_job_id":"71ad36e5-a146-434f-ab51-7ba8c702a15e","html_url":"https://github.com/mooreryan/gleam_qcheck","commit_stats":null,"previous_names":["mooreryan/gleam_qcheck"],"tags_count":11,"template":false,"template_full_name":null,"purl":"pkg:github/mooreryan/gleam_qcheck","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mooreryan%2Fgleam_qcheck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mooreryan%2Fgleam_qcheck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mooreryan%2Fgleam_qcheck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mooreryan%2Fgleam_qcheck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mooreryan","download_url":"https://codeload.github.com/mooreryan/gleam_qcheck/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mooreryan%2Fgleam_qcheck/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28618803,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-20T22:24:05.405Z","status":"ssl_error","status_checked_at":"2026-01-20T22:20:31.342Z","response_time":117,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2026-01-20T23:36:33.523Z","updated_at":"2026-01-20T23:36:34.374Z","avatar_url":"https://github.com/mooreryan.png","language":"Gleam","funding_links":[],"categories":[],"sub_categories":[],"readme":"# qcheck\n\nQuickCheck-inspired property-based testing with integrated shrinking for [Gleam](https://gleam.run/).\n\nRather than specifying test cases manually, you describe the invariants that values of a given type must satisfy (\"properties\"). Then, generators generate lots of values (test cases) on which the properties are checked. Finally, if a value is found for which a given property does not hold, that value is \"shrunk\" in order to find an nice, informative counter-example that is presented to you.\n\nWhile there are a ton of great articles introducing quickcheck or property-based testing, here are a couple general resources that you may enjoy:\n\n- [An introduction to property based testing](https://fsharpforfunandprofit.com/pbt/)\n- [What is Property Based Testing?](https://hypothesis.works/articles/what-is-property-based-testing/)\n\nYou might also be interested in checking out [this project](https://github.com/mooreryan/gleam_stdlib_testing) that uses qcheck to test Gleam's stdlib.\n\n## Usage \u0026 Examples\n\n- See the API docs for detailed usage,\n- See [qcheck_viewer](https://mooreryan.github.io/gleam_qcheck/) to visualize the distributions of some of the qcheck generators.\n\n### Basic example\n\nHere is a short example to get you started. It assumes you are using [gleeunit](https://github.com/lpil/gleeunit) to run the tests, but any test runner that reasonably handles panics will do.\n\n```gleam\nimport qcheck\n\npub fn int_addition_commutativity__test() {\n  use n \u003c- qcheck.given(qcheck.small_non_negative_int())\n  assert n + 1 == 1 + n\n}\n\npub fn int_addition_commutativity__failures_shrink_to_zero__test() {\n  use n \u003c- qcheck.given(qcheck.small_non_negative_int())\n  assert n + 1 != 1 + n\n}\n```\n\nLet's go through the code:\n\n- `qcheck.given` sets up the test\n  - If a property holds for all generated values, then `qcheck.given` returns `Nil`.\n  - If a property does not hold for all generated values, then `qcheck.given` will panic.\n- `qcheck.small_non_negative_int()` generates small integers greater than or equal to zero.\n- `assert n + 1 == 1 + n` is the property being tested in the first test.\n  - It should be true for all generated values.\n  - The return value of `qcheck.given` will be `Nil`, because the property does hold for all generated values.\n  - You can use Gleam's `assert` keyword to get nicer error messages\n- `assert n + 1 != 1 + n` is the property being tested in the second test.\n  - It should be false for any generated values.\n  - `qcheck.given` will be panic, because the property does not hold for any generated values.\n\nIf you run it, that second example will fail with an error that may look something like this if you are targeting Erlang.\n\n```\npanic src/qcheck.gleam:2833\n test: examples@basic_example_test.int_addition_commutativity__failures_shrink_to_zero__test\n info: a property was falsified!\nqcheck assert test/examples/basic_example_test.gleam:12\n code: assert n + 1 != 1 + n\n left: 4\nright: 4\n info: Assertion failed.\nqcheck shrinks\n orig: 3\nshrnk: 0\nsteps: 1\n```\n\nThe error message gives some info to help you diagnose the issue. Since we used `assert`, we get some info about the code that was run, it's location in the test file, the values on the left and right side.\nAdditionally, there is some info about the \"shrinking\". You will see:\n\n- `orig`, which lists the original generated value that triggered the error\n- `shrnk`, which shows the \"shrunk\" value.\n  - I.e., a value that is usually easiear to interpret.\n  - In this example `3` and `0` aren't too bad, but you will often see shrunk values that look like this: `#(Some(0), None, None, None, None)` which is much easier to figure out what the issue might be than something like this: `#(Some(1371457222), Some(369023.1034975077), Some(True), Some(\"2x\"), None)`.\n  - (That is a real result from a property test I intionally broke for illustration from the [bsql3](https://github.com/mooreryan/bsql3/blob/b0d60113e83a8a30b00660b1190b0334be6a04a7/test/bsql3_test.gleam#L245) repository.)\n- `steps`, which shows the number of \"shrink steps\" it took to get to the shrunk value.\n  - If this is really high, you may need to adjust your generation process.\n\n### In-depth example\n\nHere is a more in-depth example. We will create a simple `Point` type, write some serialization functions, and then check that the serializing round-trips.\n\nFirst, here is some code to define a `Point`, and a `to_string` function that makes a string representation like this: `(x y)`.\n\n```gleam\ntype Point {\n  Point(Int, Int)\n}\n\nfn point_to_string(point: Point) -\u003e String {\n  let Point(x, y) = point\n  \"(\" \u003c\u003e int.to_string(x) \u003c\u003e \" \" \u003c\u003e int.to_string(y) \u003c\u003e \")\"\n}\n```\n\nNext, let's write a function that parses the string representation into a `Point`. The string representation is pretty simple, `Point(1, 2)` would be represented by the following string: `(1 2)`.\n\nHere is one possible way to parse that string representation into a `Point`. (Note that this implementation is intentionally broken for illustration.)\n\n```gleam\nfn point_from_string(string: String) -\u003e Result(Point, String) {\n  // Create the regex.\n  use re \u003c- result.try(\n    regex.from_string(\"\\\\((\\\\d+) (\\\\d+)\\\\)\")\n    |\u003e result.map_error(string.inspect),\n  )\n\n  // Ensure there is a single match.\n  use submatches \u003c- result.try(case regex.scan(re, string) {\n    [Match(_content, submatches)] -\u003e Ok(submatches)\n    _ -\u003e Error(\"expected a single match\")\n  })\n\n  // Ensure both submatches are present.\n  use xy \u003c- result.try(case submatches {\n    [Some(x), Some(y)] -\u003e Ok(#(x, y))\n    _ -\u003e Error(\"expected two submatches\")\n  })\n\n  // Try to parse both x and y values as integers.\n  use xy \u003c- result.try(case int.parse(xy.0), int.parse(xy.1) {\n    Ok(x), Ok(y) -\u003e Ok(#(x, y))\n    Error(Nil), Ok(_) -\u003e Error(\"failed to parse x value\")\n    Ok(_), Error(Nil) -\u003e Error(\"failed to parse y value\")\n    Error(Nil), Error(Nil) -\u003e Error(\"failed to parse x and y values\")\n  })\n\n  Ok(Point(xy.0, xy.1))\n}\n```\n\nNow we would like to test our implementation. Of course, we could make some examples and test it like so:\n\n```gleam\npub fn roundtrip_test() {\n  let point = Point(1, 2)\n  let parsed_point = point |\u003e point_to_string |\u003e point_of_string\n\n  assert point == parsed_point\n}\n```\n\nThat's good, and you can imagine taking some corner cases like putting in `0` or `-1` or the max and min values for integers on your selected target. I think you should still test some interesting cases manually to \"anchor\" your test suite. But, let's think of a property to test.\n\nI mention round-tripping, but how can you write a property to test it. Something like, \"given a valid point, when serializing it to a string, and then deserializing that string into another point, both points should always be equal\".\n\nOkay, first we need to write a generator of valid points. In this case, it isn't too tricky as any integer can be used for both `x` and `y` values of the point. So we can use `generator.map2` like so:\n\n```gleam\nfn point_generator() {\n  qcheck.map2(qcheck.uniform_int(), qcheck.uniform_int(), Point)\n}\n```\n\nFor illustration, you could also utilize the `use` syntax. You could write:\n\n```gleam\nfn point_generator() {\n  use x, y \u003c- qcheck.map2(qcheck.uniform_int(), qcheck.uniform_int())\n  Point(x, y)\n}\n```\n\nNow that we have the point generator, we can write a property test.\n\n```gleam\npub fn point_serialization_roundtripping__test() {\n  use generated_point \u003c- qcheck.given(point_generator())\n\n  let assert Ok(parsed_point) =\n    generated_point |\u003e point_to_string |\u003e point_from_string\n\n  assert generated_point == parsed_point\n}\n```\n\nLet's try and run the test. You should see an error that looks something like this:\n\n```\npanic src/qcheck.gleam:2833\n test: examples@parsing_example_test.point_serialization_roundtripping__test\n info: a property was falsified!\nqcheck let assert test/examples/parsing_example_test.gleam:62\n code: let assert Ok(parsed_point) =\n    generated_point |\u003e point_to_string |\u003e point_from_string\nvalue: Error(\"expected a single match\")\n info: Pattern match failed, no pattern matched the value.\nqcheck shrinks\n orig: Point(-1827478708, -274074946)\nshrnk: Point(0, -1)\nsteps: 29\n```\n\nHere are the important parts to highlight.\n\n- original value: `Point(-1827478708, -274074946)`\n  - This is the original counter-example that causes the test to fail.\n- shrunk value: `Point(0, -1)`\n  - Because `qcheck` generators have integrated shrinking, that counter-example \"shrinks\" to this simpler example.\n  - The \"shrunk\" examples can help you better identify what the problem may be.\n- `Error(\"expected a single match\")`\n  - Here is the error message that actually caused the failure.\n\nLet me point out that you could write this test in a slightly different way and get what I think is a bit better output:\n\n```gleam\npub fn point_serialization_roundtripping__test() {\n  use generated_point \u003c- qcheck.given(point_generator())\n\n  let parsed_point = generated_point |\u003e point_to_string |\u003e point_from_string\n\n  assert Ok(generated_point) == parsed_point\n}\n```\n\nWhich would output:\n\n```\npanic src/qcheck.gleam:2833\n test: examples@parsing_example_test.point_serialization_roundtripping__assert__test\n info: a property was falsified!\nqcheck assert test/examples/parsing_example_test.gleam:56\n code: assert Ok(generated_point) == parsed_point\n left: Ok(Point(-694965642, -939456627))\nright: Error(\"expected a single match\")\n info: Assertion failed.\nqcheck shrinks\n orig: Point(-694965642, -939456627)\nshrnk: Point(0, -1)\nsteps: 30\n```\n\nEither way is fine, but I think the second way gives a bit more clarity.\n\nAnyway, back to interpreting the failure. So we see a failure with `Point(0, -1)`, which means it probably has something to do with the negative number. Also, we see that the `Error(\"expected a single match\")` is what triggered the failure. That error comes about when `regex.scan` fails in the `point_from_string` function.\n\nGiven those two pieces of information, we can infer that the issue is probably in our regular expression definition: `regex.from_string(\"\\\\((\\\\d+) (\\\\d+)\\\\)\")`. And now we may notice that we are not allowing for negative numbers in the regular expression. To fix it, change that line to the following:\n\n```gleam\n    regex.from_string(\"\\\\((-?\\\\d+) (-?\\\\d+)\\\\)\")\n```\n\nThat is allowing an optional `-` sign in front of the integers. Now when you rerun the `gleam test`, everything passes.\n\nYou could imagine combining a property test like the one above, with a few well chosen examples to anchor everything, into a nice little test suite that exercises the serialization of points in a small amount of test code.\n\n(The full code for this example can be found in `test/examples/parsing_example_test.gleam`.)\n\n### Applicative style\n\nThe applicative style provides a nice interface for creating generators for custom types. When you have independent generators, this way can often given better shrinking then using `bind` when you don't need it.\n\n```gleam\nimport qcheck\n\n/// A simple Box type with position (x, y) and dimensions (width, height).\ntype Box {\n  Box(x: Int, y: Int, w: Int, h: Int)\n}\n\nfn box_generator() {\n  // Lift the Box creating function into the Generator structure.\n  qcheck.return({\n    use x \u003c- qcheck.parameter\n    use y \u003c- qcheck.parameter\n    use w \u003c- qcheck.parameter\n    use h \u003c- qcheck.parameter\n    Box(x:, y:, w:, h:)\n  })\n  // Set the `x` generator.\n  |\u003e qcheck.apply(qcheck.bounded_int(-100, 100))\n  // Set the `y` generator.\n  |\u003e qcheck.apply(qcheck.bounded_int(-100, 100))\n  // Set the `width` generator.\n  |\u003e qcheck.apply(qcheck.bounded_int(1, 100))\n  // Set the `height` generator.\n  |\u003e qcheck.apply(qcheck.bounded_int(1, 100))\n}\n```\n\nYou could also write this example using `map4`:\n\n```gleam\nfn x_gen() {\n  qcheck.bounded_int(-100, 100)\n}\n\nfn y_gen() {\n  qcheck.bounded_int(-100, 100)\n}\n\nfn w_gen() {\n  qcheck.bounded_int(1, 100)\n}\n\nfn h_gen() {\n  qcheck.bounded_int(1, 100)\n}\n\nfn box_generator_with_map4() {\n  use x, y, w, h \u003c- qcheck.map4(x_gen(), y_gen(), w_gen(), h_gen())\n  Box(x:, y:, w:, h:)\n}\n```\n\nFor more info about this, see this [issue](https://github.com/mooreryan/gleam_qcheck/issues/13).\n\n### Integrating with testing frameworks\n\nYou don't have to do anything special to integrate `qcheck` with a testing framework like [gleeunit](https://github.com/lpil/gleeunit). The only thing required is that your testing framework of choice be able to handle panics/exceptions.\n\n_Note: [startest](https://github.com/maxdeviant/startest) should be fine. (I last checked it on Jan 6, 2026.)_\n\nYou may also be interested in [qcheck_gleeunit_utils](https://github.com/mooreryan/qcheck_gleeunit_utils) for running your tests in parallel and controlling test timeouts when using gleeunit and targeting Erlang.\n\n## Acknowledgements\n\nVery heavily inspired by the [qcheck](https://github.com/c-cube/qcheck) and [base_quickcheck](https://github.com/janestreet/base_quickcheck) OCaml packages.\n\nCheck out the `licenses` directory to view their licenses.\n\n## Contributing\n\nThank you for your interest in the project!\n\n- Bug reports, feature requests, suggestions and ideas are welcomed. Please open an [issue](https://github.com/mooreryan/gleam_qcheck/issues/new/choose) to start a discussion.\n- External contributions will generally not be accepted without prior discussion.\n  - If you have an idea for a new feature, please open an issue for discussion prior to working on a pull request.\n  - Small pull requests for bug fixes, typos, or other changes with limited scope may be accepted. If in doubt, please open an issue for discussion first.\n\n## License\n\n[![license MIT or Apache\n2.0](https://img.shields.io/badge/license-MIT%20or%20Apache%202.0-blue)](https://github.com/mooreryan/gleam_qcheck)\n\nCopyright (c) 2024 - 2026 Ryan M. Moore\n\nLicensed under the Apache License, Version 2.0 or the MIT license, at your option. This program may not be copied, modified, or distributed except according to those terms.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmooreryan%2Fgleam_qcheck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmooreryan%2Fgleam_qcheck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmooreryan%2Fgleam_qcheck/lists"}