Repository: dubzzz/fast-check
Pull request #1689 (2021-04-05 07:59:31)

⬆️ Bump JamesIves/github-pages-deploy-action from 4.1.0 to 4.1.1
Bumps [JamesIves/github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action) from 4.1.0 to 4.1.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/JamesIves/github-pages-deploy-action/releases">JamesIves/github-pages-deploy-action's releases</a>.</em></p>
<blockquote>
<h2>4.1.1</h2>
<h2>Minor Changes</h2>
<ul>
<li>Dependency/security updates provided through Dependabot.</li>
<li>Minor tooling changes, <code>format</code> is now <code>lint:format</code> for consistency.</li>
<li>Removed <code>^</code> keyword in all dependencies within package.json.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/164583b9e44b4fc5910e78feb607ea7c98d3c7b9"><code>164583b</code></a> Deploy Production Code for Commit 8e80bb1dd1b858d1126c5704ebd5ddb61ff47577 🚀</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/8e80bb1dd1b858d1126c5704ebd5ddb61ff47577"><code>8e80bb1</code></a> Merge branch 'dev' into releases/v4</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/34bd9b9faf8b9f2fb348c400390c32f77ec2b814"><code>34bd9b9</code></a> Command updates</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/7ce04f044a73d8ef4e64a950ee2b72e8f9698003"><code>7ce04f0</code></a> Bump y18n from 4.0.0 to 4.0.1 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/656">#656</a>)</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/e1037f3155d66fa63b6fdbf10efedbe2a589b11f"><code>e1037f3</code></a> Bump <code>@​types/node</code> from 14.14.35 to 14.14.37 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/655">#655</a>)</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/4c9fca22c0c28369ec9b78ce8c28f6093584b620"><code>4c9fca2</code></a> Bump eslint from 7.22.0 to 7.23.0 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/654">#654</a>)</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/e0bcd8a37921d95fd559f4fff1a302b8595675b1"><code>e0bcd8a</code></a> Bump <code>@​types/jest</code> from 26.0.21 to 26.0.22 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/652">#652</a>)</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/176796a2cd9c8ee1a6ea69d63a5ed29980883e48"><code>176796a</code></a> Update devcontainer.json</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/23b9a031b21a7d1786197523ee6fae3228efb5a3"><code>23b9a03</code></a> Bump <code>@​types/node</code> from 14.14.31 to 14.14.35 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/640">#640</a>)</li>
<li><a href="https://github.com/JamesIves/github-pages-deploy-action/commit/703d120b015888161fe5cb9f3087799f4ea26f48"><code>703d120</code></a> Bump eslint-plugin-jest from 24.1.5 to 24.3.2 (<a href="https://github-redirect.dependabot.com/JamesIves/github-pages-deploy-action/issues/641">#641</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/JamesIves/github-pages-deploy-action/compare/4.1.0...164583b9e44b4fc5910e78feb607ea7c98d3c7b9">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codesandbox[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dubzzz/fast-check/pr/1689/builds/116364) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 7eb3b3544000fb87dcccf958a24ba41ad8a71054:

|Sandbox| Source |
|--|--|
|[Vanilla](https://codesandbox.io/s/vanilla-himzf)| Configuration |
|[fast-check-examples](https://codesandbox.io/s/fast-check-examples-yjp8p)| Configuration |

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=h1) Report
> Merging [#1689](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=desc) (7eb3b35) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/99e9b3b1c918a627d92138fc7e00f190ea99fecb?el=desc) (99e9b3b) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1689/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730)](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             main    #1689   +/-   ##
=======================================
  Coverage   97.04%   97.04%           
=======================================
  Files          98       98           
  Lines        3484     3484           
  Branches      680      680           
=======================================
  Hits         3381     3381           
  Misses         99       99           
  Partials        4        4           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.04% <ø> (ø)` | |
| unit-tests-10.x-Linux | `?` | |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `97.04% <ø> (+0.02%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.


------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=footer). Last update [99e9b3b...7eb3b35](https://codecov.io/gh/dubzzz/fast-check/pull/1689?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

dubzzz: @dependabot rebase

Patch:
diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml
index 953aa605..da5b465e 100644
--- a/.github/workflows/build-status.yml
+++ b/.github/workflows/build-status.yml
@@ -350,7 +350,7 @@ jobs:
           name: docs
           path: docs/
       - name: Publish to gh-pages
-        uses: JamesIves/github-pages-deploy-action@4.1.0
+        uses: JamesIves/github-pages-deploy-action@4.1.1
         with:
           branch: gh-pages
           folder: docs
diff --git a/README.md b/README.md
index fce47dc1..ea9fd203 100644
--- a/README.md
+++ b/README.md
@@ -142,7 +142,7 @@ m.stringify({bar: ['a', null, 'b']}, {arrayFormat: 'bracket'}) //=> "bar[]=a&bar
 m.parse('bar[]=a&bar&bar[]=b', {arrayFormat: 'bracket'})       //=> {bar: [null, 'b']}
 ```
 
-**[MORE: Issues detected thanks of fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)**
+**[MORE: Issues detected thanks to fast-check](https://github.com/dubzzz/fast-check/blob/main/documentation/IssuesDiscovered.md)**
 
 ## Credits
 
diff --git a/src/check/arbitrary/TupleArbitrary.ts b/src/check/arbitrary/TupleArbitrary.ts
index eef6d3e6..674d5f10 100644
--- a/src/check/arbitrary/TupleArbitrary.ts
+++ b/src/check/arbitrary/TupleArbitrary.ts
@@ -1,16 +1,18 @@
 import { Random } from '../../random/generator/Random';
 import { Stream } from '../../stream/Stream';
-import { cloneMethod } from '../symbols';
+import { cloneIfNeeded, cloneMethod, WithCloneMethod } from '../symbols';
 import { Arbitrary } from './definition/Arbitrary';
-import { Shrinkable } from './definition/Shrinkable';
+import { convertFromNext, convertToNext } from './definition/Converters';
+import { NextArbitrary } from './definition/NextArbitrary';
+import { NextValue } from './definition/NextValue';
 
 /** @internal */
-type ArbsArray<Ts extends unknown[]> = { [K in keyof Ts]: Arbitrary<Ts[K]> };
+type ArbsArray<Ts extends unknown[]> = { [K in keyof Ts]: NextArbitrary<Ts[K]> };
 /** @internal */
-type ShrinksArray<Ts extends unknown[]> = { [K in keyof Ts]: Shrinkable<Ts[K]> };
+type ValuesArray<Ts extends unknown[]> = { [K in keyof Ts]: NextValue<Ts[K]> };
 
 /** @internal */
-export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
+export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<Ts> {
   constructor(readonly arbs: ArbsArray<Ts>) {
     super();
     for (let idx = 0; idx !== arbs.length; ++idx) {
@@ -19,53 +21,69 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
         throw new Error(`Invalid parameter encountered at index ${idx}: expecting an Arbitrary`);
     }
   }
-  private static makeItCloneable<Ts>(vs: Ts[], shrinkables: Shrinkable<Ts>[]) {
-    (vs as any)[cloneMethod] = () => {
-      const cloned = [];
-      for (let idx = 0; idx !== shrinkables.length; ++idx) {
-        cloned.push(shrinkables[idx].value); // push potentially cloned values
-      }
-      GenericTupleArbitrary.makeItCloneable(cloned, shrinkables);
-      return cloned;
-    };
-    return vs;
+  private static makeItCloneable<TValue>(vs: TValue[], values: NextValue<TValue>[]): WithCloneMethod<TValue[]> {
+    return Object.defineProperty(vs, cloneMethod, {
+      value: () => {
+        const cloned = [];
+        for (let idx = 0; idx !== values.length; ++idx) {
+          cloned.push(values[idx].value); // push potentially cloned values
+        }
+        GenericTupleArbitrary.makeItCloneable(cloned, values);
+        return cloned;
+      },
+    });
   }
-  private static wrapper<Ts extends unknown[]>(shrinkables: ShrinksArray<Ts>): Shrinkable<Ts> {
+  private static wrapper<Ts extends unknown[]>(values: ValuesArray<Ts>): NextValue<Ts> {
     let cloneable = false;
-    const vs = ([] as unknown) as Ts;
-    for (let idx = 0; idx !== shrinkables.length; ++idx) {
-      const s = shrinkables[idx];
-      cloneable = cloneable || s.hasToBeCloned;
-      vs.push(s.value);
+    const vs = ([] as unknown) as Ts & unknown[];
+    const ctxs: unknown[] = [];
+    for (let idx = 0; idx !== values.length; ++idx) {
+      const v = values[idx];
+      cloneable = cloneable || v.hasToBeCloned;
+      vs.push(v.value);
+      ctxs.push(v.context);
     }
     if (cloneable) {
-      GenericTupleArbitrary.makeItCloneable(vs, shrinkables);
+      GenericTupleArbitrary.makeItCloneable(vs, values);
     }
-    return new Shrinkable(vs, () => GenericTupleArbitrary.shrinkImpl(shrinkables).map(GenericTupleArbitrary.wrapper));
+    return new NextValue(vs, ctxs);
+  }
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<Ts> {
+    return GenericTupleArbitrary.wrapper<Ts>(this.arbs.map((a) => a.generate(mrng, biasFactor)) as ValuesArray<Ts>);
   }
-  generate(mrng: Random): Shrinkable<Ts> {
-    return GenericTupleArbitrary.wrapper<Ts>(this.arbs.map((a) => a.generate(mrng)) as ShrinksArray<Ts>);
+  canGenerate(value: unknown): value is Ts {
+    if (!Array.isArray(value) || value.length !== this.arbs.length) {
+      return false;
+    }
+    for (let index = 0; index !== this.arbs.length; ++index) {
+      if (!this.arbs[index].canGenerate(value[index])) {
+        return false;
+      }
+    }
+    return true;
   }
-  private static shrinkImpl<Ts extends unknown[]>(value: ShrinksArray<Ts>): Stream<ShrinksArray<Ts>> {
+  shrink(value: Ts, context?: unknown): Stream<NextValue<Ts>> {
     // shrinking one by one is the not the most comprehensive
     // but allows a reasonable number of entries in the shrink
-    let s = Stream.nil<ShrinksArray<Ts>>();
-    for (let idx = 0; idx !== value.length; ++idx) {
-      s = s.join(
-        value[idx].shrink().map(
-          (v) =>
-            value
-              .slice(0, idx)
-              .concat([v])
-              .concat(value.slice(idx + 1)) as ShrinksArray<Ts>
-        )
-      );
+    let s = Stream.nil<NextValue<Ts>>();
+    const safeContext: unknown[] = Array.isArray(context) ? context : [];
+    for (let idx = 0; idx !== this.arbs.length; ++idx) {
+      const shrinksForIndex: Stream<NextValue<Ts>> = this.arbs[idx]
+        .shrink(value[idx], safeContext[idx])
+        .map((v) => {
+          const nextValues: NextValue<unknown>[] = value.map(
+            (v, idx) => new NextValue(cloneIfNeeded(v), safeContext[idx])
+          );
+          return nextValues
+            .slice(0, idx)
+            .concat([v])
+            .concat(nextValues.slice(idx + 1));
+        })
+        .map((values) => GenericTupleArbitrary.wrapper(values) as NextValue<Ts>);
+      s = s.join(shrinksForIndex);
     }
     return s;
   }
-  withBias(freq: number): Arbitrary<Ts> {
-    return new GenericTupleArbitrary<Ts>(this.arbs.map((a) => a.withBias(freq)) as ArbsArray<Ts>);
-  }
 }
 
 /**
@@ -78,7 +96,8 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends Arbitrary<Ts> {
  * @public
  */
 function genericTuple<Ts extends unknown[]>(arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
-  return new GenericTupleArbitrary<Ts>(arbs);
+  const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
+  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
 }
 
 /**
@@ -90,7 +109,8 @@ function genericTuple<Ts extends unknown[]>(arbs: { [K in keyof Ts]: Arbitrary<T
  * @public
  */
 function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
-  return new GenericTupleArbitrary<Ts>(arbs);
+  const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
+  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
 }
 
 export { genericTuple, tuple };


Test patch:
diff --git a/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts
deleted file mode 100644
index 4e7b051a..00000000
--- a/test/unit/check/arbitrary/TupleArbitrary.generic.spec.ts
+++ /dev/null
@@ -1,124 +0,0 @@
-import * as fc from '../../../../lib/fast-check';
-import * as prand from 'pure-rand';
-import { dummy } from './TupleArbitrary.properties';
-
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { context } from '../../../../src/check/arbitrary/ContextArbitrary';
-import { integer, nat } from '../../../../src/check/arbitrary/IntegerArbitrary';
-import { genericTuple } from '../../../../src/check/arbitrary/TupleArbitrary';
-import { Shrinkable } from '../../../../src/check/arbitrary/definition/Shrinkable';
-import { hasCloneMethod, cloneMethod } from '../../../../src/check/symbols';
-import { stream } from '../../../../src/stream/Stream';
-import { Random } from '../../../../src/random/generator/Random';
-
-import * as genericHelper from './generic/GenericArbitraryHelper';
-import * as stubRng from '../../stubs/generators';
-
-describe('TupleArbitrary', () => {
-  describe('genericTuple', () => {
-    genericHelper.isValidArbitrary((mins: number[]) => genericTuple(mins.map((m) => integer(m, m + 10))), {
-      seedGenerator: fc.array(fc.nat(1000)),
-      isStrictlySmallerValue: (g1: number[], g2: number[]) => g1.findIndex((v, idx) => v < g2[idx]) !== -1,
-      isValidValue: (g: number[], mins: number[]) => {
-        // right size
-        if (g.length !== mins.length) return false;
-        // values in the right range
-        for (let idx = 0; idx !== g.length; ++idx) {
-          if (g[idx] < mins[idx]) return false;
-          if (g[idx] > mins[idx] + 10) return false;
-        }
-        return true;
-      },
-    });
-    it('Should throw on null arbitrary', () =>
-      expect(() => genericTuple([dummy(1), dummy(2), (null as any) as Arbitrary<string>])).toThrowError());
-    it('Should throw on invalid arbitrary', () =>
-      expect(() => genericTuple([dummy(1), dummy(2), {} as Arbitrary<any>])).toThrowError());
-    it('Should produce cloneable tuple if one cloneable children', () =>
-      fc.assert(
-        fc.property(fc.nat(50), fc.nat(50), (before, after) => {
-          const arbsBefore = [...Array(before)].map(() => integer(0, 0));
-          const arbsAfter = [...Array(after)].map(() => integer(0, 0));
-          const arbs: Arbitrary<unknown>[] = [...arbsBefore, context(), ...arbsAfter];
-          const mrng = stubRng.mutable.counter(0);
-          const g = genericTuple(arbs).generate(mrng).value;
-          return hasCloneMethod(g);
-        })
-      ));
-    it('Should not produce cloneable tuple if no cloneable children', () =>
-      fc.assert(
-        fc.property(fc.nat(100), (num) => {
-          const arbs = [...Array(num)].map(() => integer(0, 0));
-          const mrng = stubRng.mutable.counter(0);
-          const g = genericTuple(arbs).generate(mrng).value;
-          return !hasCloneMethod(g);
-        })
-      ));
-    it('Should not clone on generate', () => {
-      let numCallsToClone = 0;
-      const withClonedAndCounter = new (class extends Arbitrary<any> {
-        generate() {
-          const v = {
-            [cloneMethod]: () => {
-              ++numCallsToClone;
-              return v;
-            },
-          };
-          return new Shrinkable(v);
-        }
-      })();
-      const arbs = [withClonedAndCounter];
-      const mrng = stubRng.mutable.counter(0);
-      genericTuple(arbs).generate(mrng);
-      expect(numCallsToClone).toEqual(0);
-    });
-  });
-  it('Should use the cloneable instance for a single run only', () => {
-    class CloneableInstance {
-      static SharedId = 0;
-      readonly id: number;
-      constructor() {
-        this.id = ++CloneableInstance.SharedId;
-      }
-      [cloneMethod] = () => new CloneableInstance();
-    }
-    const cloneableArbitrary = new (class extends Arbitrary<CloneableInstance> {
-      generate() {
-        function* g() {
-          yield new Shrinkable(new CloneableInstance());
-          yield new Shrinkable(new CloneableInstance());
-        }
-        return new Shrinkable(new CloneableInstance(), () => stream(g()));
-      }
-    })();
-    const arbs = genericTuple([nat(16), cloneableArbitrary, nat(16)] as Arbitrary<any>[]);
-    const extractId = (shrinkable: Shrinkable<[number, CloneableInstance, number]>) => shrinkable.value_[1].id;
-    fc.assert(
-      fc.property(fc.integer().noShrink(), fc.infiniteStream(fc.nat()), (seed, shrinkPath) => {
-        // Reset SharedId in order to have reproducible runs
-        CloneableInstance.SharedId = 0;
-
-        // Generate the first shrinkable
-        const it = shrinkPath[Symbol.iterator]();
-        const mrng = new Random(prand.xorshift128plus(seed));
-        let shrinkable: Shrinkable<[number, CloneableInstance, number]> | null = arbs.generate(mrng) as any;
-
-        // Traverse the shrink tree in order to detect already seen ids
-        const alreadySeenIds: { [id: number]: boolean } = {
-          [extractId(shrinkable!)]: true,
-        };
-        while (shrinkable !== null) {
-          shrinkable = shrinkable
-            .shrink()
-            .map((nextShrinkable) => {
-              const id = extractId(nextShrinkable);
-              if (alreadySeenIds[id]) throw new Error(`Already encountered id ${id}`);
-              alreadySeenIds[id] = true;
-              return nextShrinkable;
-            })
-            .getNthOrLast(it.next().value);
-        }
-      })
-    );
-  });
-});
diff --git a/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts
new file mode 100644
index 00000000..418754dd
--- /dev/null
+++ b/test/unit/check/arbitrary/TupleArbitrary.itest.spec.ts
@@ -0,0 +1,231 @@
+import { tuple as tupleOld } from '../../../../src/check/arbitrary/TupleArbitrary';
+
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../src/random/generator/Random';
+import { Stream } from '../../../../src/stream/Stream';
+
+import * as stubRng from '../../stubs/generators';
+import { buildNextShrinkTree, renderTree, walkTree } from './generic/ShrinkTree';
+import {
+  assertGenerateProducesCorrectValues,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesCorrectValues,
+  assertShrinkProducesSameValueWithoutInitialContext,
+  assertShrinkProducesStrictlySmallerValue,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from './generic/NextArbitraryAssertions';
+import { FakeIntegerArbitrary } from './generic/NextArbitraryHelpers';
+import { cloneMethod } from '../../../../src/check/symbols';
+
+const mrngNoCall = stubRng.mutable.nocall();
+function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: NextArbitrary<Ts[K]> }): NextArbitrary<Ts> {
+  const oldArbs = arbs.map((arb) => convertFromNext(arb)) as { [K in keyof Ts]: Arbitrary<Ts[K]> };
+  return convertToNext(tupleOld<Ts>(...oldArbs));
+}
+
+describe('tuple', () => {
+  const tupleBuilder = () => tuple(new FakeIntegerArbitrary(), new FakeIntegerArbitrary(), new FakeIntegerArbitrary());
+  it('should generate the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(tupleBuilder);
+  });
+
+  it('should only generate correct values', () => {
+    assertGenerateProducesCorrectValues(tupleBuilder, (value) => Array.isArray(value) && value.length === 3);
+  });
+
+  it('should recognize values that would have been generated using it during generate', () => {
+    assertGenerateProducesValuesFlaggedAsCanGenerate(tupleBuilder);
+  });
+
+  it('should shrink towards the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(tupleBuilder);
+  });
+
+  it('should be able to shrink without any context if underlyings do', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(tupleBuilder);
+  });
+
+  it('should only shrink towards correct values', () => {
+    assertShrinkProducesCorrectValues(tupleBuilder, (value) => Array.isArray(value) && value.length === 3);
+  });
+
+  it('should recognize values that would have been generated using it during shrink', () => {
+    assertShrinkProducesValuesFlaggedAsCanGenerate(tupleBuilder);
+  });
+
+  it('should preserve strictly smaller ordering in shrink (underlyings do)', () => {
+    assertShrinkProducesStrictlySmallerValue(tupleBuilder, (t1, t2) => t1.findIndex((v, idx) => v < t2[idx]) !== -1);
+  });
+
+  it('should produce the right shrinking tree', () => {
+    // Arrange
+    const arb = tuple(new FirstArbitrary(), new SecondArbitrary());
+
+    // Act
+    const g = arb.generate(mrngNoCall, undefined);
+    const renderedTree = renderTree(buildNextShrinkTree(arb, g)).join('\n');
+
+    // Assert
+    expect(g.hasToBeCloned).toBe(false);
+    expect(g.value).toBe(g.value_);
+    expect(g.value).toEqual([expectedFirst, expectedSecond]);
+    expect(renderedTree).toMatchInlineSnapshot(`
+      "[4,97]
+      ├> [2,97]
+      |  ├> [0,97]
+      |  |  ├> [0,99]
+      |  |  └> [0,98]
+      |  |     └> [0,100]
+      |  ├> [2,99]
+      |  |  └> [0,99]
+      |  └> [2,98]
+      |     ├> [0,98]
+      |     |  └> [0,100]
+      |     └> [2,100]
+      |        └> [0,100]
+      ├> [3,97]
+      |  ├> [0,97]
+      |  |  ├> [0,99]
+      |  |  └> [0,98]
+      |  |     └> [0,100]
+      |  ├> [1,97]
+      |  |  ├> [1,99]
+      |  |  └> [1,98]
+      |  |     └> [1,100]
+      |  ├> [3,99]
+      |  |  ├> [0,99]
+      |  |  └> [1,99]
+      |  └> [3,98]
+      |     ├> [0,98]
+      |     |  └> [0,100]
+      |     ├> [1,98]
+      |     |  └> [1,100]
+      |     └> [3,100]
+      |        ├> [0,100]
+      |        └> [1,100]
+      ├> [4,99]
+      |  ├> [2,99]
+      |  |  └> [0,99]
+      |  └> [3,99]
+      |     ├> [0,99]
+      |     └> [1,99]
+      └> [4,98]
+         ├> [2,98]
+         |  ├> [0,98]
+         |  |  └> [0,100]
+         |  └> [2,100]
+         |     └> [0,100]
+         ├> [3,98]
+         |  ├> [0,98]
+         |  |  └> [0,100]
+         |  ├> [1,98]
+         |  |  └> [1,100]
+         |  └> [3,100]
+         |     ├> [0,100]
+         |     └> [1,100]
+         └> [4,100]
+            ├> [2,100]
+            |  └> [0,100]
+            └> [3,100]
+               ├> [0,100]
+               └> [1,100]"
+    `);
+  });
+
+  it('should not re-use twice the same instance of cloneable', () => {
+    // Arrange
+    const alreadySeenCloneable = new Set<unknown>();
+    const arb = tuple(new FirstArbitrary(), new CloneableArbitrary(), new SecondArbitrary());
+
+    // Act
+    const g = arb.generate(mrngNoCall, undefined);
+    const treeA = buildNextShrinkTree(arb, g);
+    const treeB = buildNextShrinkTree(arb, g);
+
+    // Assert
+    walkTree(treeA, ([_first, cloneable, _second]) => {
+      expect(alreadySeenCloneable.has(cloneable)).toBe(false);
+      alreadySeenCloneable.add(cloneable);
+    });
+    walkTree(treeB, ([_first, cloneable, _second]) => {
+      expect(alreadySeenCloneable.has(cloneable)).toBe(false);
+      alreadySeenCloneable.add(cloneable);
+    });
+  });
+});
+
+// Helpers
+
+const expectedFirst = 4;
+const expectedSecond = 97;
+
+class FirstArbitrary extends NextArbitrary<number> {
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(expectedFirst, { step: 2 });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('step' in context)) {
+      throw new Error('Invalid context for FirstArbitrary');
+    }
+    if (value <= 0) {
+      return Stream.nil();
+    }
+    const currentStep = (context as { step: number }).step;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+      ...(value - currentStep + 1 >= 0 ? [new NextValue(value - currentStep + 1, { step: nextStep })] : [])
+    );
+  }
+}
+class SecondArbitrary extends NextArbitrary<number> {
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(expectedSecond, { step: 2 });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('step' in context)) {
+      throw new Error('Invalid context for SecondArbitrary');
+    }
+    if (value >= 100) {
+      return Stream.nil();
+    }
+    const currentStep = (context as { step: number }).step;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value + currentStep <= 100 ? [new NextValue(value + currentStep, { step: nextStep })] : []),
+      ...(value + currentStep - 1 <= 100 ? [new NextValue(value + currentStep - 1, { step: nextStep })] : [])
+    );
+  }
+}
+
+class CloneableArbitrary extends NextArbitrary<number> {
+  private instance() {
+    return Object.defineProperty([], cloneMethod, { value: () => this.instance() });
+  }
+  generate(_mrng: Random): NextValue<number> {
+    return new NextValue(this.instance(), { shrunkOnce: false });
+  }
+  canGenerate(_value: unknown): _value is number {
+    throw new Error('No call expected in that scenario');
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (typeof context !== 'object' || context === null || !('shrunkOnce' in context)) {
+      throw new Error('Invalid context for CloneableArbitrary');
+    }
+    const safeContext = context as { shrunkOnce: boolean };
+    if (safeContext.shrunkOnce) {
+      return Stream.nil();
+    }
+    return Stream.of(new NextValue(this.instance(), { shrunkOnce: true }));
+  }
+}
diff --git a/test/unit/check/arbitrary/TupleArbitrary.properties.ts b/test/unit/check/arbitrary/TupleArbitrary.properties.ts
deleted file mode 100644
index 51281eeb..00000000
--- a/test/unit/check/arbitrary/TupleArbitrary.properties.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
-import { integer } from '../../../../src/check/arbitrary/IntegerArbitrary';
-import { Shrinkable } from '../../../../src/fast-check';
-import { Random } from '../../../../src/random/generator/Random';
-
-export class DummyArbitrary extends Arbitrary<string> {
-  constructor(public id: number) {
-    super();
-  }
-  generate(mrng: Random): Shrinkable<string> {
-    return integer()
-      .generate(mrng)
-      .map((v) => `key${this.id}_${v}`);
-  }
-  withBias(freq: number): Arbitrary<string> {
-    return new DummyArbitrary(2 * this.id + freq);
-  }
-}
-function dummy(id: number): Arbitrary<string> {
-  return new DummyArbitrary(id);
-}
-
-export { dummy };
diff --git a/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts b/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts
new file mode 100644
index 00000000..93ad38d6
--- /dev/null
+++ b/test/unit/check/arbitrary/TupleArbitrary.utest.spec.ts
@@ -0,0 +1,236 @@
+import { tuple as tupleOld } from '../../../../src/check/arbitrary/TupleArbitrary';
+
+import { convertFromNext, convertToNext } from '../../../../src/check/arbitrary/definition/Converters';
+import { Arbitrary } from '../../../../src/check/arbitrary/definition/Arbitrary';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+
+import * as stubRng from '../../stubs/generators';
+
+import { fakeNextArbitrary } from './generic/NextArbitraryHelpers';
+import { cloneMethod, hasCloneMethod } from '../../../../src/check/symbols';
+import { Stream } from '../../../../src/stream/Stream';
+
+const mrngNoCall = stubRng.mutable.nocall();
+function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: NextArbitrary<Ts[K]> }): NextArbitrary<Ts> {
+  const oldArbs = arbs.map((arb) => convertFromNext(arb)) as { [K in keyof Ts]: Arbitrary<Ts[K]> };
+  return convertToNext(tupleOld<Ts>(...oldArbs));
+}
+
+describe('tuple', () => {
+  describe('generate', () => {
+    it('should merge results coming from underlyings and call them with the exact same inputs as the received ones', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const vA = Symbol();
+      const vB = Symbol();
+      const vC = Symbol();
+      const { instance: instanceA, generate: generateA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, generate: generateB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, generate: generateC } = fakeNextArbitrary<symbol>();
+      generateA.mockReturnValueOnce(new NextValue(vA));
+      generateB.mockReturnValueOnce(new NextValue(vB));
+      generateC.mockReturnValueOnce(new NextValue(vC));
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+
+      // Assert
+      expect(g.value).toEqual([vA, vB, vC]);
+      expect(generateA).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+      expect(generateB).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+      expect(generateC).toHaveBeenCalledWith(mrngNoCall, expectedBiasFactor);
+    });
+
+    it('should produce a cloneable instance if provided one cloneable underlying', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: jest.fn() })));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB);
+      const g = arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(g.hasToBeCloned).toBe(true);
+      expect(hasCloneMethod(g.value)).toBe(true);
+    });
+
+    it('should not produce a cloneable instance if no cloneable underlyings', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryNotCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue([]));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryNotCloneableB);
+      const g = arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(g.hasToBeCloned).toBe(false);
+      expect(hasCloneMethod(g.value)).toBe(false);
+    });
+
+    it('should not clone cloneable on generate', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA } = fakeNextArbitrary<string[]>();
+      const { instance: fakeArbitraryCloneableB, generate: generateB } = fakeNextArbitrary<string[]>();
+      const cloneMethodImpl = jest.fn();
+      generateA.mockReturnValue(new NextValue([]));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB);
+      arb.generate(mrngNoCall, undefined);
+
+      // Assert
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('canGenerate', () => {
+    it.each`
+      canA     | canB     | canC
+      ${false} | ${false} | ${false}
+      ${false} | ${true}  | ${true}
+      ${true}  | ${false} | ${true}
+      ${true}  | ${true}  | ${false}
+      ${true}  | ${true}  | ${true}
+    `(
+      'should merge results coming from underlyings for canGenerate if received array has the right size',
+      ({ canA, canB, canC }) => {
+        // Arrange
+        const vA = Symbol();
+        const vB = Symbol();
+        const vC = Symbol();
+        const { instance: instanceA, canGenerate: canGenerateA } = fakeNextArbitrary<symbol>();
+        const { instance: instanceB, canGenerate: canGenerateB } = fakeNextArbitrary<symbol>();
+        const { instance: instanceC, canGenerate: canGenerateC } = fakeNextArbitrary<symbol>();
+        canGenerateA.mockReturnValueOnce(canA);
+        canGenerateB.mockReturnValueOnce(canB);
+        canGenerateC.mockReturnValueOnce(canC);
+
+        // Act
+        const arb = tuple(instanceA, instanceB, instanceC);
+        const out = arb.canGenerate([vA, vB, vC]);
+
+        // Assert
+        expect(out).toBe(canA && canB && canC);
+        expect(canGenerateA).toHaveBeenCalledWith(vA);
+        if (canA) expect(canGenerateB).toHaveBeenCalledWith(vB);
+        else expect(canGenerateB).not.toHaveBeenCalled();
+        if (canA && canB) expect(canGenerateC).toHaveBeenCalledWith(vC);
+        else expect(canGenerateC).not.toHaveBeenCalled();
+      }
+    );
+
+    it('should not call underlyings on canGenerate if size is invalid', () => {
+      // Arrange
+      const { instance: instanceA, canGenerate: canGenerateA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, canGenerate: canGenerateB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, canGenerate: canGenerateC } = fakeNextArbitrary<symbol>();
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const out = arb.canGenerate([Symbol(), Symbol(), Symbol(), Symbol()]);
+
+      // Assert
+      expect(out).toBe(false);
+      expect(canGenerateA).not.toHaveBeenCalled();
+      expect(canGenerateB).not.toHaveBeenCalled();
+      expect(canGenerateC).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('shrink', () => {
+    it('should call back arbitraries on shrink with the initially returned contextq', () => {
+      // Arrange
+      const expectedBiasFactor = 48;
+      const vA = Symbol();
+      const vB = Symbol();
+      const vC = Symbol();
+      const contextA = Symbol();
+      const contextB = Symbol();
+      const contextC = Symbol();
+      const { instance: instanceA, generate: generateA, shrink: shrinkA } = fakeNextArbitrary<symbol>();
+      const { instance: instanceB, generate: generateB, shrink: shrinkB } = fakeNextArbitrary<symbol>();
+      const { instance: instanceC, generate: generateC, shrink: shrinkC } = fakeNextArbitrary<symbol>();
+      generateA.mockReturnValueOnce(new NextValue(vA, contextA));
+      generateB.mockReturnValueOnce(new NextValue(vB, contextB));
+      generateC.mockReturnValueOnce(new NextValue(vC, contextC));
+      const shrinkA1 = Symbol();
+      const shrinkA2 = Symbol();
+      const shrinkB1 = Symbol();
+      const shrinkC1 = Symbol();
+      const shrinkC2 = Symbol();
+      const shrinkC3 = Symbol();
+      shrinkA.mockReturnValueOnce(Stream.of(new NextValue(shrinkA1 as symbol), new NextValue(shrinkA2)));
+      shrinkB.mockReturnValueOnce(Stream.of(new NextValue(shrinkB1 as symbol)));
+      shrinkC.mockReturnValueOnce(
+        Stream.of(new NextValue(shrinkC1 as symbol), new NextValue(shrinkC2), new NextValue(shrinkC3))
+      );
+
+      // Act
+      const arb = tuple(instanceA, instanceB, instanceC);
+      const g = arb.generate(mrngNoCall, expectedBiasFactor);
+      const shrinks = [...arb.shrink(g.value, g.context)];
+
+      // Assert
+      expect(shrinks).toHaveLength(2 /* A */ + 1 /* B */ + 3 /* C */);
+      expect(shrinks.map((v) => v.value)).toEqual([
+        [shrinkA1, vB, vC],
+        [shrinkA2, vB, vC],
+        [vA, shrinkB1, vC],
+        [vA, vB, shrinkC1],
+        [vA, vB, shrinkC2],
+        [vA, vB, shrinkC3],
+      ]);
+      expect(shrinkA).toHaveBeenCalledWith(vA, contextA);
+      expect(shrinkB).toHaveBeenCalledWith(vB, contextB);
+      expect(shrinkC).toHaveBeenCalledWith(vC, contextC);
+    });
+
+    it('should clone cloneable on shrink', () => {
+      // Arrange
+      const { instance: fakeArbitraryNotCloneableA, generate: generateA, shrink: shrinkA } = fakeNextArbitrary<
+        string[]
+      >();
+      const { instance: fakeArbitraryCloneableB, generate: generateB, shrink: shrinkB } = fakeNextArbitrary<string[]>();
+
+      const { instance: fakeArbitraryNotCloneableC, generate: generateC, shrink: shrinkC } = fakeNextArbitrary<
+        string[]
+      >();
+      const cloneMethodImpl = jest
+        .fn()
+        .mockImplementation(() => Object.defineProperty([], cloneMethod, { value: cloneMethodImpl }));
+      generateA.mockReturnValue(new NextValue([]));
+      shrinkA.mockReturnValue(Stream.of(new NextValue([]), new NextValue([])));
+      generateB.mockReturnValue(new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })));
+      shrinkB.mockReturnValue(
+        Stream.of(
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })),
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl })),
+          new NextValue(Object.defineProperty([], cloneMethod, { value: cloneMethodImpl }))
+        )
+      );
+      generateC.mockReturnValue(new NextValue([]));
+      shrinkC.mockReturnValue(Stream.of(new NextValue([]), new NextValue([]), new NextValue([]), new NextValue([])));
+
+      // Act
+      const arb = tuple(fakeArbitraryNotCloneableA, fakeArbitraryCloneableB, fakeArbitraryNotCloneableC);
+      const g = arb.generate(mrngNoCall, undefined);
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+      const shrinkLazy = arb.shrink(g.value, g.context);
+      expect(cloneMethodImpl).not.toHaveBeenCalled();
+      const shrinks = [...shrinkLazy];
+
+      // Assert
+      expect(shrinks).toHaveLength(2 /* A */ + 3 /* B */ + 4 /* C */);
+      expect(cloneMethodImpl).toHaveBeenCalledTimes(shrinks.length);
+    });
+  });
+});
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
new file mode 100644
index 00000000..f7f5134b
--- /dev/null
+++ b/test/unit/check/arbitrary/generic/NextArbitraryAssertions.ts
@@ -0,0 +1,267 @@
+import * as prand from 'pure-rand';
+import * as fc from '../../../../../lib/fast-check';
+
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../../src/random/generator/Random';
+
+// Minimal requirements
+// > The following assertions are supposed to be fulfilled by any of the arbitraries
+// > provided by fast-check.
+
+export function assertGenerateProducesSameValueGivenSameSeed<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    isEqual,
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(fc.integer().noShrink(), biasFactorArbitrary(), extra, (seed, biasFactor, extraParameters) => {
+      // Arrange
+      const arb = arbitraryBuilder(extraParameters);
+
+      // Act
+      const g1 = arb.generate(randomFromSeed(seed), biasFactor);
+      const g2 = arb.generate(randomFromSeed(seed), biasFactor);
+
+      // Assert
+      assertEquality(isEqual, g1.value, g2.value, extraParameters);
+    }),
+    assertParameters
+  );
+}
+
+export function assertShrinkProducesSameValueGivenSameSeed<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    noInitialContext?: boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    isEqual,
+    noInitialContext,
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(
+      fc.integer().noShrink(),
+      biasFactorArbitrary(),
+      fc.array(fc.nat(100), { minLength: 1 }),
+      extra,
+      (seed, biasFactor, shrinkPath, extraParameters) => {
+        // Arrange
+        const arb = arbitraryBuilder(extraParameters);
+
+        // Act / Assert
+        let g1: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        let g2: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        if (noInitialContext) {
+          const originalG2 = g2!;
+          g2 = new NextValue(originalG2.value_, undefined, () => originalG2.value);
+        }
+        let id = 0;
+        while (g1 !== null && g2 !== null) {
+          assertEquality(isEqual, g1.value, g2.value, extraParameters);
+          g1 = arb.shrink(g1.value, g1.context).getNthOrLast(id);
+          g2 = arb.shrink(g2.value, g2.context).getNthOrLast(id);
+          id = (id + 1) % shrinkPath.length;
+        }
+        expect(g1).toBe(null);
+        expect(g2).toBe(null);
+      }
+    ),
+    assertParameters
+  );
+}
+
+// Optional requirements
+// > The following requirements are optional as they do not break the design of fast-check when they are not totally ensured
+// > But some of them are really recommended to build valid arbitraries that can be used.
+
+export function assertGenerateProducesCorrectValues<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(fc.integer().noShrink(), biasFactorArbitrary(), extra, (seed, biasFactor, extraParameters) => {
+      // Arrange
+      const arb = arbitraryBuilder(extraParameters);
+
+      // Act
+      const g = arb.generate(randomFromSeed(seed), biasFactor);
+
+      // Assert
+      assertCorrectness(isCorrect, g.value, extraParameters, arb);
+    }),
+    assertParameters
+  );
+}
+
+export function assertShrinkProducesCorrectValues<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const {
+    extraParameters: extra = fc.constant((undefined as unknown) as U) as fc.Arbitrary<U>,
+    assertParameters,
+  } = options;
+  fc.assert(
+    fc.property(
+      fc.integer().noShrink(),
+      biasFactorArbitrary(),
+      fc.array(fc.nat(100), { minLength: 1 }),
+      extra,
+      (seed, biasFactor, shrinkPath, extraParameters) => {
+        // Arrange
+        const arb = arbitraryBuilder(extraParameters);
+
+        // Act / Assert
+        let g: NextValue<T> | null = arb.generate(randomFromSeed(seed), biasFactor);
+        let id = 0;
+        while (g !== null) {
+          assertCorrectness(isCorrect, g.value, extraParameters, arb);
+          g = arb.shrink(g.value, g.context).getNthOrLast(id);
+          id = (id + 1) % shrinkPath.length;
+        }
+        expect(g).toBe(null);
+      }
+    ),
+    assertParameters
+  );
+}
+
+// Extra requirements
+// The assertions above can be configured to push generators even further. They ensure more complex invariants.
+// Following assertions are mostly derived from the one above.
+// > assertShrinkProducesSameValueGivenSameSeed with option {noInitialContext:true}
+// > assertGenerateProducesCorrectValues with option isCorrect: (v, _, arb) => arb.canGenerate(v)
+// > assertShrinkProducesCorrectValues with option (v, _, arb) => arb.canGenerate(v)
+
+export function assertShrinkProducesSameValueWithoutInitialContext<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    isEqual?: (v1: T, v2: T, extraParameters: U) => void | boolean;
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertShrinkProducesSameValueGivenSameSeed(arbitraryBuilder, { ...options, noInitialContext: true });
+}
+
+export function assertGenerateProducesValuesFlaggedAsCanGenerate<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertGenerateProducesCorrectValues(arbitraryBuilder, (v, _, arb) => arb.canGenerate(v), options);
+}
+
+export function assertShrinkProducesValuesFlaggedAsCanGenerate<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  return assertShrinkProducesCorrectValues(arbitraryBuilder, (v, _, arb) => arb.canGenerate(v), options);
+}
+
+export function assertShrinkProducesStrictlySmallerValue<T, U = never>(
+  arbitraryBuilder: (extraParameters: U) => NextArbitrary<T>,
+  isStrictlySmaller: (vNew: T, vOld: T, extraParameters: U) => void | boolean,
+  options: {
+    extraParameters?: fc.Arbitrary<U>;
+    assertParameters?: fc.Parameters<unknown>;
+  } = {}
+): void {
+  const previousValue: { value?: T } = {};
+  function arbitraryBuilderInternal(...args: Parameters<typeof arbitraryBuilder>) {
+    delete previousValue.value;
+    return arbitraryBuilder(...args);
+  }
+  function isStrictlySmallerInternal(v: T, extraParameters: U) {
+    try {
+      if (!('value' in previousValue)) {
+        return true;
+      }
+      const vNew = v;
+      const vOld = previousValue.value!;
+      try {
+        const out = isStrictlySmaller(vNew, vOld, extraParameters);
+        expect(out).not.toBe(false);
+      } catch (err) {
+        throw new Error(
+          `Expect: ${fc.stringify(vNew)} to be strictly smaller than ${fc.stringify(vOld)}\n\nGot error: ${err}`
+        );
+      }
+    } finally {
+      previousValue.value = v;
+    }
+  }
+  return assertShrinkProducesCorrectValues(arbitraryBuilderInternal, isStrictlySmallerInternal, options);
+}
+
+// Various helpers
+
+function randomFromSeed(seed: number): Random {
+  return new Random(prand.xorshift128plus(seed));
+}
+
+function biasFactorArbitrary() {
+  return fc.option(fc.integer({ min: 2 }), { freq: 2, nil: undefined });
+}
+
+function assertEquality<T, U>(
+  isEqual: ((v1: T, v2: T, extraParameters: U) => void | boolean) | undefined,
+  v1: T,
+  v2: T,
+  extraParameters: U
+): void {
+  if (isEqual) {
+    try {
+      const out = isEqual(v1, v2, extraParameters);
+      expect(out).not.toBe(false);
+    } catch (err) {
+      throw new Error(`Expect: ${fc.stringify(v1)} to be equal to ${fc.stringify(v2)}\n\nGot error: ${err}`);
+    }
+  } else expect(v1).toStrictEqual(v2);
+}
+
+function assertCorrectness<T, U>(
+  isCorrect: (v: T, extraParameters: U, arb: NextArbitrary<T>) => void | boolean,
+  v: T,
+  extraParameters: U,
+  arb: NextArbitrary<T>
+): void {
+  try {
+    const out = isCorrect(v, extraParameters, arb);
+    expect(out).not.toBe(false);
+  } catch (err) {
+    throw new Error(`Expect: ${fc.stringify(v)} to be a correct value\n\nGot error: ${err}`);
+  }
+}
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
new file mode 100644
index 00000000..a3842461
--- /dev/null
+++ b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
@@ -0,0 +1,73 @@
+import { mocked } from 'ts-jest';
+import { MaybeMocked, MockWithArgs } from 'ts-jest/dist/utils/testing';
+import { NextArbitrary } from '../../../../../src/check/arbitrary/definition/NextArbitrary';
+import { NextValue } from '../../../../../src/check/arbitrary/definition/NextValue';
+import { Random } from '../../../../../src/random/generator/Random';
+import { Stream } from '../../../../../src/stream/Stream';
+
+/**
+ * Generate a fake Class inheriting from NextArbitrary with all methods being mocked
+ */
+export function fakeNextArbitraryClass<T = any>(): { Class: new () => NextArbitrary<T> } & MaybeMocked<
+  NextArbitrary<T>
+> {
+  const generate = jest.fn();
+  const canGenerate = (jest.fn() as any) as ((value: unknown) => value is T) &
+    MockWithArgs<(value: unknown) => value is T>;
+  const shrink = jest.fn();
+  const filter = jest.fn();
+  const map = jest.fn();
+  const chain = jest.fn();
+  const noShrink = jest.fn();
+  const noBias = jest.fn();
+  mocked;
+  class FakeNextArbitrary extends NextArbitrary<T> {
+    generate = generate;
+    canGenerate = canGenerate;
+    shrink = shrink;
+    filter = filter;
+    map = map;
+    chain = chain;
+    noShrink = noShrink;
+    noBias = noBias;
+  }
+  return { Class: FakeNextArbitrary, generate, canGenerate, shrink, filter, map, chain, noShrink, noBias };
+}
+
+/**
+ * Generate a fake instance inheriting from NextArbitrary with all methods being mocked
+ */
+export function fakeNextArbitrary<T = any>(): { instance: NextArbitrary<T> } & MaybeMocked<NextArbitrary<T>> {
+  const { Class, ...mockedMethods } = fakeNextArbitraryClass<T>();
+  return { instance: new Class(), ...mockedMethods };
+}
+
+/**
+ * Fake instance with the following capabilities:
+ * - shrink to strictly smaller values
+ * - fully implemented canGenerate
+ * - take bias into account on generate
+ * - context less shrink like context full (for first iteration only)
+ */
+export class FakeIntegerArbitrary extends NextArbitrary<number> {
+  generate(mrng: Random, biasFactor: number | undefined): NextValue<number> {
+    // Worst case: 0-10
+    // No bias   : 0-100
+    const maxLimit = biasFactor !== undefined ? Math.max(10, 100 - biasFactor) : 100;
+    return new NextValue(mrng.nextInt(0, maxLimit), { step: 2 });
+  }
+  canGenerate(value: unknown): value is number {
+    return typeof value === 'number' && value >= 0 && value <= 100 && Number.isInteger(value);
+  }
+  shrink(value: number, context?: unknown): Stream<NextValue<number>> {
+    if (value <= 0) {
+      return Stream.nil();
+    }
+    const currentStep = context !== undefined ? (context as { step: number }).step : 2;
+    const nextStep = currentStep + 1;
+    return Stream.of(
+      ...(value - currentStep >= 0 ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+      ...(value - currentStep + 1 >= 0 ? [new NextValue(value - currentStep + 1, { step: nextStep })] : [])
+    );
+  }
+}