PageSourceSearch

https://kotest.io/assets/js/2c6b6d7f.3dbc540a.js

js kotest.io collected 2026-10-03 21:53:39 UTC 16,959 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkkotestdocs=globalThis.webpackChunkkotestdocs||[]).push([[5243],{10689(e,n,t){t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>d,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>l});const s=JSON.parse('{"id":"proptest/permutations","title":"Permutations","description":"The permutations DSL is a newer property-testing API introduced in Kotest 6.2. Rather than passing generators as","source":"@site/versioned_docs/version-6.2/proptest/permutations.md","sourceDirName":"proptest","slug":"/proptest/property-test-permutations.html","permalink":"/docs/proptest/property-test-permutations.html","draft":false,"unlisted":false,"editUrl":"https://github.com/kotest/kotest/blob/master/documentation/versioned_docs/version-6.2/proptest/permutations.md","tags":[],"version":"6.2","frontMatter":{"id":"permutations","title":"Permutations","slug":"property-test-permutations.html","sidebar_label":"Permutations"},"sidebar":"proptest","previous":{"title":"Global Configuration","permalink":"/docs/proptest/property-test-global-config.html"},"next":{"title":"Arrow Generators","permalink":"/docs/proptest/property-test-generators-arrow.html"}}');var i=t(74848),r=t(28453);const a={id:"permutations",title:"Permutations",slug:"property-test-permutations.html",sidebar_label:"Permutations"},d=void 0,o={},l=[{value:"Configuration",id:"configuration",level:2},{value:"Shared configuration",id:"shared-configuration",level:2},{value:"Assumptions",id:"assumptions",level:2},{value:"Statistics",id:"statistics",level:2},{value:"Coverage assertions",id:"coverage-assertions",level:3},{value:"Seeds",id:"seeds",level:2},{value:"Manually setting the seed",id:"manually-setting-the-seed",level:3},{value:"Persisted failing seeds",id:"persisted-failing-seeds",level:3},{value:"Failing if a seed is hardcoded",id:"failing-if-a-seed-is-hardcoded",level:3}];function c(e){const n={code:"code",h2:"h2",h3:"h3",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsxs)(n.p,{children:["The permutations DSL is a newer property-testing API introduced in Kotest 6.2. Rather than passing generators as\npositional parameters to ",(0,i.jsx)(n.code,{children:"forAll"})," or ",(0,i.jsx)(n.code,{children:"checkAll"}),", generators are declared inline as named properties using a ",(0,i.jsx)(n.code,{children:"gen { ... }"}),"\ndelegate, and the test body is declared in a ",(0,i.jsx)(n.code,{children:"check { ... }"})," block. This produces a more readable test as the inputs\nhave meaningful names and the configuration is expressed declaratively at the call site."]}),"\n",(0,i.jsx)(n.p,{children:"A simple permutation test that asserts addition is commutative:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:"permutations {\n\n   val a by gen { Arb.int() }\n   val b by gen { Arb.int() }\n\n   iterations = 1000\n\n   check {\n      (a + b) shouldBe (b + a)\n   }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["The permutations DSL is currently marked ",(0,i.jsx)(n.code,{children:"@ExperimentalKotest"})," and the API may change before it stabilises."]}),"\n",(0,i.jsx)(n.h2,{id:"configuration",children:"Configuration"}),"\n",(0,i.jsxs)(n.p,{children:["Every option supported by the DSL is a ",(0,i.jsx)(n.code,{children:"var"})," on ",(0,i.jsx)(n.code,{children:"PermutationConfiguration"})," and may be set inside the ",(0,i.jsx)(n.code,{children:"permutations { }"}),"\nblock. The most common options are:"]}),"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",(0,i.jsxs)(n.table,{children:[(0,i.jsx)(n.thead,{children:(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.th,{children:"Option"}),(0,i.jsx)(n.th,{children:"Default"}),(0,i.jsx)(n.th,{children:"Description"})]})}),(0,i.jsxs)(n.tbody,{children:[(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"iterations"})}),(0,i.jsx)(n.td,{children:"1000"}),(0,i.jsx)(n.td,{children:"Number of permutations to execute when no other constraint is set."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"duration"})}),(0,i.jsx)(n.td,{children:"null"}),(0,i.jsxs)(n.td,{children:["If set, iterations run until this duration elapses (overrides ",(0,i.jsx)(n.code,{children:"iterations"}),")."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"constraints"})}),(0,i.jsx)(n.td,{children:"null"}),(0,i.jsxs)(n.td,{children:["Custom ",(0,i.jsx)(n.code,{children:"Constraints"})," strategy (overrides both ",(0,i.jsx)(n.code,{children:"iterations"})," and ",(0,i.jsx)(n.code,{children:"duration"}),")."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"minSuccess"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"Int.MAX_VALUE"})}),(0,i.jsx)(n.td,{children:"The minimum number of successful permutations required; otherwise the test fails."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"maxFailures"})}),(0,i.jsx)(n.td,{children:"0"}),(0,i.jsx)(n.td,{children:"The number of failing permutations tolerated before the run aborts."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"maxDiscardPercentage"})}),(0,i.jsx)(n.td,{children:"20"}),(0,i.jsxs)(n.td,{children:["The maximum percentage of permutations that may be discarded by ",(0,i.jsx)(n.code,{children:"assume"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"seed"})}),(0,i.jsx)(n.td,{children:"null"}),(0,i.jsx)(n.td,{children:"If set, generators use this seed instead of a random one."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"failOnSeed"})}),(0,i.jsx)(n.td,{children:"false"}),(0,i.jsxs)(n.td,{children:["If true, fails the test when ",(0,i.jsx)(n.code,{children:"seed"})," has been explicitly set."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"writeFailedSeed"})}),(0,i.jsx)(n.td,{children:"true"}),(0,i.jsx)(n.td,{children:"If true, the seed used by a failing test is written to disk so it can be replayed."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"shouldPrintConfig"})}),(0,i.jsx)(n.td,{children:"false"}),(0,i.jsx)(n.td,{children:"Prints a summary of the active configuration before the run."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"shouldPrintGeneratedValues"})}),(0,i.jsx)(n.td,{children:"false"}),(0,i.jsx)(n.td,{children:"Prints the value of each generator on every iteration."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"shouldPrintShrinkSteps"})}),(0,i.jsx)(n.td,{children:"true"}),(0,i.jsx)(n.td,{children:"Prints each step taken while shrinking a counterexample."})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"statisticsReportMode"})}),(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"StatisticsReportMode.ON"})}),(0,i.jsxs)(n.td,{children:["When to print classification statistics: ",(0,i.jsx)(n.code,{children:"ON"}),", ",(0,i.jsx)(n.code,{children:"SUCCESS"}),", ",(0,i.jsx)(n.code,{children:"FAILED"}),", or ",(0,i.jsx)(n.code,{children:"OFF"}),"."]})]}),(0,i.jsxs)(n.tr,{children:[(0,i.jsx)(n.td,{children:(0,i.jsx)(n.code,{children:"edgecasesGenerationProbability"})}),(0,i.jsx)(n.td,{children:"0.02"}),(0,i.jsx)(n.td,{children:"The probability that a generator emits an edge c
1ase rather than a random sample."})]})]})]}),"\n",(0,i.jsx)(n.p,{children:"For example, to run 250 iterations using a fixed seed and print the config:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:"permutations {\n   iterations = 250\n   seed = 4242L\n   shouldPrintConfig = true\n\n   val n by gen { Arb.int(0..100) }\n\n   check {\n      (n * n) shouldBeGreaterThanOrEqual 0\n   }\n}\n"})}),"\n",(0,i.jsx)(n.h2,{id:"shared-configuration",children:"Shared configuration"}),"\n",(0,i.jsxs)(n.p,{children:["When several tests should share the same defaults, build a ",(0,i.jsx)(n.code,{children:"PermutationConfiguration"})," once with ",(0,i.jsx)(n.code,{children:"permconfig"})," and pass it\nto ",(0,i.jsx)(n.code,{children:"permutations(default = ...)"}),". Any options set inside the ",(0,i.jsx)(n.code,{children:"permutations"})," block override those of the shared default."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:'val defaults = permconfig {\n   iterations = 500\n   maxDiscardPercentage = 10\n   shouldPrintConfig = true\n}\n\nclass CommutativityTest : FunSpec({\n\n   test("addition is commutative") {\n      permutations(defaults) {\n         val a by gen { Arb.int() }\n         val b by gen { Arb.int() }\n         check { (a + b) shouldBe (b + a) }\n      }\n   }\n\n   test("multiplication is commutative") {\n      permutations(defaults) {\n         val a by gen { Arb.int() }\n         val b by gen { Arb.int() }\n         // override just the iteration count for this test\n         iterations = 200\n         check { (a * b) shouldBe (b * a) }\n      }\n   }\n})\n'})}),"\n",(0,i.jsx)(n.h2,{id:"assumptions",children:"Assumptions"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"assume"})," is used inside ",(0,i.jsx)(n.code,{children:"check { }"})," to discard a permutation whose generated values are not interesting. A discarded\npermutation does not count as a success or a failure - it simply does not contribute to the run. There are two forms:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:"permutations {\n   val a by gen { Arb.int(0..10) }\n   val b by gen { Arb.int(0..10) }\n\n   check {\n      // boolean form: skip when the predicate is false\n      assume(a != b)\n\n      // function form: skip if the block throws an AssertionError\n      assume { a shouldNotBe b }\n\n      a.compareTo(b) shouldNotBe 0\n   }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["If too many permutations are discarded (more than ",(0,i.jsx)(n.code,{children:"maxDiscardPercentage"}),"), the run aborts with an error. This protects\nagainst accidentally writing an assumption that filters out almost every generated value."]}),"\n",(0,i.jsx)(n.h2,{id:"statistics",children:"Statistics"}),"\n",(0,i.jsxs)(n.p,{children:["Inside ",(0,i.jsx)(n.code,{children:"check { }"}),", calls to ",(0,i.jsx)(n.code,{children:"classify"})," track how often a permutation matched a given classification. Classifications\ncan be grouped under a label so that multiple, independent dimensions can be tracked side by side. Without a label, the\ndefault label ",(0,i.jsx)(n.code,{children:"statistics"})," is used."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:'permutations {\n   val n by gen { Arb.int() }\n\n   check {\n      classify(n % 2 == 0, "even", "odd")                  // default label\n      classify("sign", n >= 0, "non-negative", "negative") // custom label\n   }\n}\n'})}),"\n",(0,i.jsx)(n.p,{children:"When statistics are enabled, the counts and percentages for each label are printed at the end of the run:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:"Statistics: [addition is commutative] (1000 iterations) [sign]\n\npositive                                                     503 (50%)\nnegative                                                     497 (50%)\n\nStatistics: [addition is commutative] (1000 iterations) [parity]\n\neven                                                         512 (51%)\nodd                                                          488 (49%)\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Set ",(0,i.jsx)(n.code,{children:"statisticsReportMode"})," to ",(0,i.jsx)(n.code,{children:"OFF"})," to suppress this output, or to ",(0,i.jsx)(n.code,{children:"SUCCESS"})," / ",(0,i.jsx)(n.code,{children:"FAILED"})," to print it only when the run\npasses or only when it fails."]}),"\n",(0,i.jsx)(n.h3,{id:"coverage-assertions",children:"Coverage assertions"}),"\n",(0,i.jsxs)(n.p,{children:["A ",(0,i.jsx)(n.code,{children:"coverage { }"})," block lets you assert that classifications appeared at least a certain 
1number of times, or at least\na certain percentage of the time, across the run. A failing coverage check fails the test even if every assertion\ninside ",(0,i.jsx)(n.code,{children:"check"})," passed."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:'permutations {\n   iterations = 1000\n\n   val n by gen { Arb.int() }\n\n   check {\n      classify("parity", n % 2 == 0, "even", "odd")\n      classify("sign", n >= 0, "non-negative", "negative")\n   }\n\n   coverage {\n      // at least 400 of the iterations must be classified as \'even\' under the parity label\n      count("parity", "even", 400)\n\n      // at least 40% of iterations must be classified as \'non-negative\' under the sign label\n      percentage("sign", "non-negative", 40.0)\n   }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["The two-argument forms (",(0,i.jsx)(n.code,{children:"count(value, n)"})," / ",(0,i.jsx)(n.code,{children:"percentage(value, p)"}),") apply to the default label, matching the\ntwo-argument form of ",(0,i.jsx)(n.code,{children:"classify"}),"."]}),"\n",(0,i.jsx)(n.h2,{id:"seeds",children:"Seeds"}),"\n",(0,i.jsx)(n.p,{children:"By default each run uses a fresh random seed. The active seed is part of the run's identity - the same seed produces\nthe same sequence of generated values. The DSL provides several knobs around seeds:"}),"\n",(0,i.jsx)(n.h3,{id:"manually-setting-the-seed",children:"Manually setting the seed"}),"\n",(0,i.jsxs)(n.p,{children:["Set ",(0,i.jsx)(n.code,{children:"seed"})," to reproduce a specific run, for example after a failing test reports the seed it used:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:"permutations {\n   seed = 1900646515L\n\n   val a by gen { Arb.int(0..100) }\n   check { a shouldBeLessThan 8 }\n}\n"})}),"\n",(0,i.jsx)(n.h3,{id:"persisted-failing-seeds",children:"Persisted failing seeds"}),"\n",(0,i.jsxs)(n.p,{children:["When a permutation test fails, the seed used by that run is written to disk under the project's seed directory. The\nnext time the same test runs and finds no explicit ",(0,i.jsx)(n.code,{children:"seed"}),", it will read this persisted seed and replay the failing\ninputs. This makes flaky property-test failures easier to investigate. To opt out, set ",(0,i.jsx)(n.code,{children:"writeFailedSeed = false"}),"."]}),"\n",(0,i.jsx)(n.h3,{id:"failing-if-a-seed-is-hardcoded",children:"Failing if a seed is hardcoded"}),"\n",(0,i.jsxs)(n.p,{children:[(0,i.jsx)(n.code,{children:"seed"})," is convenient for debugging, but a hardcoded seed defeats the purpose of property testing in CI. Set\n",(0,i.jsx)(n.code,{children:"failOnSeed = true"})," (typically through global defaults) to fail any permutation test that still has an explicit\n",(0,i.jsx)(n.code,{children:"seed"})," set, helping catch debugging seeds that were forgotten."]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:"permutations {\n   failOnSeed = true\n   seed = 1234L // this will now fail the test\n   check { /* ... */ }\n}\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In practice you usually want to keep hardcoded seeds working locally - so you can reproduce a failure - while\nforbidding them on CI. Gate ",(0,i.jsx)(n.code,{children:"failOnSeed"})," on an environment variable that CI sets:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:'permutations {\n   failOnSeed = System.getenv("CI") != null\n   seed = 1234L // OK locally, fails on CI\n   check { /* ... */ }\n}\n'})}),"\n",(0,i.jsxs)(n.p,{children:["This is typically set once via shared config (",(0,i.jsx)(n.code,{children:"permconfig"}),") so the policy applies to every permutation test in the\nproject:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-kotlin",children:'val ciSafe = permconfig {\n   failOnSeed = System.getenv("CI") != null\n}\n\npermutations(ciSafe) {\n   seed = 1234L\n   check { /* ... */ }\n}\n'})})]})}function h(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},28453(e,n,t){t.d(n,{R:()=>a,x:()=>d});var s=t(96540);const i={},r=s.createContext(i);function a(e){const n=s.useContext(r);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:a(e.components),s.createElement(r.Provider,{value:n},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.