Repository: dubzzz/fast-check
Pull request #1741 (2021-04-14 06:32:47)

✅ Rewrite tests dealing with tuple
<!-- Why is this PR for? -->
<!-- Describe the reason why you opened this PR or give a link towards the associated issue. -->

## In a nutshell

❌ New feature
❌ Fix an issue
❌ Documentation improvement
✔️ Other: *tests*

(✔️: yes, ❌: no)

## Potential impacts

None


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/1741/builds/119546) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 789cb087d46826ecad78bf57e0bdb57601c909f7:

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

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#1741](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (789cb08) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/a3af13b20d6bbb950172453166312f45c9f50790?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (a3af13b) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1741/graphs/tree.svg?width=650&height=150&src=pr&token=fRi2fJN730&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)

```diff
@@           Coverage Diff           @@
##             main    #1741   +/-   ##
=======================================
  Coverage   97.08%   97.08%           
=======================================
  Files         111      111           
  Lines        3599     3600    +1     
  Branches      713      713           
=======================================
+ Hits         3494     3495    +1     
  Misses        101      101           
  Partials        4        4           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `97.08% <100.00%> (+<0.01%)` | :arrow_up: |
| unit-tests-10.x-Linux | `97.08% <100.00%> (+<0.01%)` | :arrow_up: |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `?` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [src/arbitrary/\_internals/TupleArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1741/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL1R1cGxlQXJiaXRyYXJ5LnRz) | `98.07% <100.00%> (ø)` | |
| [src/arbitrary/genericTuple.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1741/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9nZW5lcmljVHVwbGUudHM=) | `100.00% <100.00%> (ø)` | |
| [src/arbitrary/tuple.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1741/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS90dXBsZS50cw==) | `100.00% <100.00%> (ø)` | |
| [src/check/arbitrary/ObjectArbitrary.ts](https://codecov.io/gh/dubzzz/fast-check/pull/1741/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2NoZWNrL2FyYml0cmFyeS9PYmplY3RBcmJpdHJhcnkudHM=) | `97.36% <0.00%> (+0.02%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [a3af13b...789cb08](https://codecov.io/gh/dubzzz/fast-check/pull/1741?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN).


Patch:
diff --git a/src/arbitrary/_internals/TupleArbitrary.ts b/src/arbitrary/_internals/TupleArbitrary.ts
index e360c75f..7a0beda4 100644
--- a/src/arbitrary/_internals/TupleArbitrary.ts
+++ b/src/arbitrary/_internals/TupleArbitrary.ts
@@ -10,7 +10,7 @@ type ArbsArray<Ts extends unknown[]> = { [K in keyof Ts]: NextArbitrary<Ts[K]> }
 type ValuesArray<Ts extends unknown[]> = { [K in keyof Ts]: NextValue<Ts[K]> };
 
 /** @internal */
-export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<Ts> {
+export class TupleArbitrary<Ts extends unknown[]> extends NextArbitrary<Ts> {
   constructor(readonly arbs: ArbsArray<Ts>) {
     super();
     for (let idx = 0; idx !== arbs.length; ++idx) {
@@ -26,7 +26,7 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<T
         for (let idx = 0; idx !== values.length; ++idx) {
           cloned.push(values[idx].value); // push potentially cloned values
         }
-        GenericTupleArbitrary.makeItCloneable(cloned, values);
+        TupleArbitrary.makeItCloneable(cloned, values);
         return cloned;
       },
     });
@@ -42,12 +42,12 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<T
       ctxs.push(v.context);
     }
     if (cloneable) {
-      GenericTupleArbitrary.makeItCloneable(vs, values);
+      TupleArbitrary.makeItCloneable(vs, values);
     }
     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>);
+    return TupleArbitrary.wrapper<Ts>(this.arbs.map((a) => a.generate(mrng, biasFactor)) as ValuesArray<Ts>);
   }
   canGenerate(value: unknown): value is Ts {
     if (!Array.isArray(value) || value.length !== this.arbs.length) {
@@ -77,7 +77,7 @@ export class GenericTupleArbitrary<Ts extends unknown[]> extends NextArbitrary<T
             .concat([v])
             .concat(nextValues.slice(idx + 1));
         })
-        .map((values) => GenericTupleArbitrary.wrapper(values) as NextValue<Ts>);
+        .map((values) => TupleArbitrary.wrapper(values) as NextValue<Ts>);
       s = s.join(shrinksForIndex);
     }
     return s;
diff --git a/src/arbitrary/genericTuple.ts b/src/arbitrary/genericTuple.ts
index 95ad8603..5ba530d2 100644
--- a/src/arbitrary/genericTuple.ts
+++ b/src/arbitrary/genericTuple.ts
@@ -1,7 +1,7 @@
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
 import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
 import { NextArbitrary } from '../check/arbitrary/definition/NextArbitrary';
-import { GenericTupleArbitrary } from './_internals/TupleArbitrary';
+import { TupleArbitrary } from './_internals/TupleArbitrary';
 
 /**
  * For tuples produced by the provided `arbs`
@@ -14,5 +14,5 @@ import { GenericTupleArbitrary } from './_internals/TupleArbitrary';
  */
 export function genericTuple<Ts extends unknown[]>(arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
   const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
-  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
+  return convertFromNext(new TupleArbitrary<Ts>(nextArbs));
 }
diff --git a/src/arbitrary/tuple.ts b/src/arbitrary/tuple.ts
index 0845eceb..1b9c1aba 100644
--- a/src/arbitrary/tuple.ts
+++ b/src/arbitrary/tuple.ts
@@ -1,7 +1,7 @@
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
 import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
 import { NextArbitrary } from '../check/arbitrary/definition/NextArbitrary';
-import { GenericTupleArbitrary } from './_internals/TupleArbitrary';
+import { TupleArbitrary } from './_internals/TupleArbitrary';
 
 /**
  * For tuples produced using the provided `arbs`
@@ -13,5 +13,5 @@ import { GenericTupleArbitrary } from './_internals/TupleArbitrary';
  */
 export function tuple<Ts extends unknown[]>(...arbs: { [K in keyof Ts]: Arbitrary<Ts[K]> }): Arbitrary<Ts> {
   const nextArbs = arbs.map((arb) => convertToNext(arb)) as { [K in keyof Ts]: NextArbitrary<Ts[K]> };
-  return convertFromNext(new GenericTupleArbitrary<Ts>(nextArbs));
+  return convertFromNext(new TupleArbitrary<Ts>(nextArbs));
 }


Test patch:
diff --git a/test/unit/arbitrary/_internals/TupleArbitrary.itest.spec.ts b/test/unit/arbitrary/_internals/TupleArbitrary.itest.spec.ts
deleted file mode 100644
index ab0e7d93..00000000
--- a/test/unit/arbitrary/_internals/TupleArbitrary.itest.spec.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-import { tuple as tupleOld } from '../../../../src/arbitrary/tuple';
-
-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 '../../check/arbitrary/generic/ShrinkTree';
-import {
-  assertGenerateProducesCorrectValues,
-  assertGenerateProducesSameValueGivenSameSeed,
-  assertGenerateProducesValuesFlaggedAsCanGenerate,
-  assertShrinkProducesCorrectValues,
-  assertShrinkProducesSameValueWithoutInitialContext,
-  assertShrinkProducesStrictlySmallerValue,
-  assertShrinkProducesValuesFlaggedAsCanGenerate,
-} from '../../check/arbitrary/generic/NextArbitraryAssertions';
-import { FakeIntegerArbitrary } from '../../check/arbitrary/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/arbitrary/_internals/TupleArbitrary.spec.ts b/test/unit/arbitrary/_internals/TupleArbitrary.spec.ts
new file mode 100644
index 00000000..7e438686
--- /dev/null
+++ b/test/unit/arbitrary/_internals/TupleArbitrary.spec.ts
@@ -0,0 +1,453 @@
+import { TupleArbitrary } from '../../../../src/arbitrary/_internals/TupleArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { FakeIntegerArbitrary, fakeNextArbitrary } from '../../check/arbitrary/generic/NextArbitraryHelpers';
+import { fakeRandom } from '../../check/arbitrary/generic/RandomHelpers';
+import { cloneMethod, hasCloneMethod } from '../../../../src/check/symbols';
+import { Stream } from '../../../../src/stream/Stream';
+import {
+  assertGenerateProducesCorrectValues,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesCorrectValues,
+  assertShrinkProducesSameValueWithoutInitialContext,
+  assertShrinkProducesStrictlySmallerValue,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from '../../check/arbitrary/generic/NextArbitraryAssertions';
+import { buildNextShrinkTree, renderTree, walkTree } from '../../check/arbitrary/generic/ShrinkTree';
+import { NextArbitrary } from '../../../../src/check/arbitrary/definition/NextArbitrary';
+import { Random } from '../../../../src/random/generator/Random';
+
+describe('TupleArbitrary', () => {
+  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));
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([instanceA, instanceB, instanceC]);
+      const g = arb.generate(mrng, expectedBiasFactor);
+
+      // Assert
+      expect(g.value).toEqual([vA, vB, vC]);
+      expect(generateA).toHaveBeenCalledWith(mrng, expectedBiasFactor);
+      expect(generateB).toHaveBeenCalledWith(mrng, expectedBiasFactor);
+      expect(generateC).toHaveBeenCalledWith(mrng, 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() })));
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([fakeArbitraryNotCloneableA, fakeArbitraryCloneableB]);
+      const g = arb.generate(mrng, 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([]));
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([fakeArbitraryNotCloneableA, fakeArbitraryNotCloneableB]);
+      const g = arb.generate(mrng, 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 })));
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([fakeArbitraryNotCloneableA, fakeArbitraryCloneableB]);
+      arb.generate(mrng, 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 = new TupleArbitrary([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 = new TupleArbitrary([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))
+      );
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([instanceA, instanceB, instanceC]);
+      const g = arb.generate(mrng, 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([])));
+      const { instance: mrng } = fakeRandom();
+
+      // Act
+      const arb = new TupleArbitrary([fakeArbitraryNotCloneableA, fakeArbitraryCloneableB, fakeArbitraryNotCloneableC]);
+      const g = arb.generate(mrng, 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);
+    });
+  });
+});
+
+describe('TupleArbitrary (integration)', () => {
+  const isCorrect = (value: number[]) => Array.isArray(value) && value.length === 3;
+
+  const isStrictlySmaller = (t1: number[], t2: number[]) => t1.findIndex((v, idx) => v < t2[idx]) !== -1;
+
+  const tupleBuilder = () =>
+    new TupleArbitrary([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, isCorrect);
+  });
+
+  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, isCorrect);
+  });
+
+  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, isStrictlySmaller);
+  });
+
+  it('should produce the right shrinking tree', () => {
+    // Arrange
+    const arb = new TupleArbitrary([new FirstArbitrary(), new SecondArbitrary()]);
+    const { instance: mrng } = fakeRandom();
+
+    // Act
+    const g = arb.generate(mrng, 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 = new TupleArbitrary([new FirstArbitrary(), new CloneableArbitrary(), new SecondArbitrary()]);
+    const { instance: mrng } = fakeRandom();
+
+    // Act
+    const g = arb.generate(mrng, 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/arbitrary/_internals/TupleArbitrary.utest.spec.ts b/test/unit/arbitrary/_internals/TupleArbitrary.utest.spec.ts
deleted file mode 100644
index 5d5c9351..00000000
--- a/test/unit/arbitrary/_internals/TupleArbitrary.utest.spec.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-import { tuple as tupleOld } from '../../../../src/arbitrary/tuple';
-
-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 '../../check/arbitrary/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/arbitrary/genericTuple.spec.ts b/test/unit/arbitrary/genericTuple.spec.ts
new file mode 100644
index 00000000..c34cbcdf
--- /dev/null
+++ b/test/unit/arbitrary/genericTuple.spec.ts
@@ -0,0 +1,24 @@
+import { genericTuple } from '../../../src/arbitrary/genericTuple';
+import { convertFromNext, convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+import { fakeNextArbitrary } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import * as TupleArbitraryMock from '../../../src/arbitrary/_internals/TupleArbitrary';
+
+describe('genericTuple', () => {
+  it('should instantiate a TupleArbitrary based on mapped-to-next arbitraries', () => {
+    // Arrange
+    const { instance } = fakeNextArbitrary<unknown[]>();
+    const TupleArbitrary = jest.spyOn(TupleArbitraryMock, 'TupleArbitrary');
+    TupleArbitrary.mockImplementation(() => instance as any);
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+
+    // Act
+    const out = genericTuple([arb1, arb2]);
+
+    // Assert
+    expect(convertToNext(out)).toBe(instance);
+    expect(TupleArbitrary).toHaveBeenCalledWith([nextArb1, nextArb2]);
+  });
+});
diff --git a/test/unit/arbitrary/tuple.spec.ts b/test/unit/arbitrary/tuple.spec.ts
new file mode 100644
index 00000000..eda31dea
--- /dev/null
+++ b/test/unit/arbitrary/tuple.spec.ts
@@ -0,0 +1,24 @@
+import { tuple } from '../../../src/arbitrary/tuple';
+import { convertFromNext, convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+import { fakeNextArbitrary } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import * as TupleArbitraryMock from '../../../src/arbitrary/_internals/TupleArbitrary';
+
+describe('tuple', () => {
+  it('should instantiate a TupleArbitrary based on mapped-to-next arbitraries', () => {
+    // Arrange
+    const { instance } = fakeNextArbitrary<unknown[]>();
+    const TupleArbitrary = jest.spyOn(TupleArbitraryMock, 'TupleArbitrary');
+    TupleArbitrary.mockImplementation(() => instance as any);
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+
+    // Act
+    const out = tuple(arb1, arb2);
+
+    // Assert
+    expect(convertToNext(out)).toBe(instance);
+    expect(TupleArbitrary).toHaveBeenCalledWith([nextArb1, nextArb2]);
+  });
+});