Repository: dubzzz/fast-check
Pull request #2031 (2021-06-23 23:21:03)

✨ Add built-in unmapper support onto `uuid` and `uuidV`
<!-- Context of the PR: short description and potentially linked issues -->

<!-- ...a few words to describe the content of this PR...               -->
<!-- ... -->

<!-- Type of PR: [ ] unchecked / [ ] checked -->
**_Category:_**

- [x] ✨ Introduce new features
- [ ] 📝 Add or update documentation
- [ ] ✅ Add or update tests
- [ ] 🐛 Fix a bug
- [ ] 🏷️ Add or update types
- [ ] ⚡️ Improve performance
- [ ] _Other(s):_ ...
<!-- Don't forget to add the gitmoji icon in the name of the PR -->
<!-- See: https://gitmoji.dev/                                  -->

<!-- Fixing bugs, adding features... may impact existing ones           -->
<!-- in order to track potential issues that could be related to your PR -->
<!-- please check the impacts and describe more precisely what to expect -->

**_Potential impacts:_**

<!-- Generated values: Can your change impact any of the existing generators in terms of generated values, if so which ones? when? -->
<!-- Shrink values:    Can your change impact any of the existing generators in terms of shrink values, if so which ones? when? -->
<!-- Performance:      Can it require some typings changes on user side? Please give more details -->
<!-- Typings:          Is there a potential performance impact? In which cases? -->
- [ ] Generated values
- [ ] Shrink values
- [ ] Performance
- [ ] Typings
- [ ] _Other(s):_ ...


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

Latest deployment of this branch, based on commit 4d6c23c043b9f2477b1b448898f46a336884f931:

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

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2031?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#2031](https://codecov.io/gh/dubzzz/fast-check/pull/2031?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (4d6c23c) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/8f18566a30dc4a66be64f6344a74fc867a7a260b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (8f18566) will **decrease** coverage by `0.07%`.
> The diff coverage is `91.11%`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/2031/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/2031?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    #2031      +/-   ##
==========================================
- Coverage   96.61%   96.54%   -0.08%     
==========================================
  Files         166      170       +4     
  Lines        4284     4312      +28     
  Branches      801      805       +4     
==========================================
+ Hits         4139     4163      +24     
- Misses        141      145       +4     
  Partials        4        4              
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `96.54% <91.11%> (-0.08%)` | :arrow_down: |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `96.54% <91.11%> (-0.08%)` | :arrow_down: |
| unit-tests-16.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/2031?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | Coverage Δ | |
|---|---|---|
| [...rbitrary/\_internals/mappers/NumberToPaddedEight.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL21hcHBlcnMvTnVtYmVyVG9QYWRkZWRFaWdodC50cw==) | `77.77% <77.77%> (ø)` | |
| [...arbitrary/\_internals/mappers/PaddedEightsToUuid.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL21hcHBlcnMvUGFkZGVkRWlnaHRzVG9VdWlkLnRz) | `80.00% <80.00%> (ø)` | |
| [...internals/builders/PaddedNumberArbitraryBuilder.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS9faW50ZXJuYWxzL2J1aWxkZXJzL1BhZGRlZE51bWJlckFyYml0cmFyeUJ1aWxkZXIudHM=) | `100.00% <100.00%> (ø)` | |
| [src/arbitrary/uuid.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS91dWlkLnRz) | `100.00% <100.00%> (ø)` | |
| [src/arbitrary/uuidV.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2FyYml0cmFyeS91dWlkVi50cw==) | `100.00% <100.00%> (ø)` | |
| [src/fast-check-default.ts](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN#diff-c3JjL2Zhc3QtY2hlY2stZGVmYXVsdC50cw==) | `100.00% <100.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/dubzzz/fast-check/pull/2031/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/2031?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/2031?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [8f18566...4d6c23c](https://codecov.io/gh/dubzzz/fast-check/pull/2031?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/builders/PaddedNumberArbitraryBuilder.ts b/src/arbitrary/_internals/builders/PaddedNumberArbitraryBuilder.ts
new file mode 100644
index 00000000..9c3cf1df
--- /dev/null
+++ b/src/arbitrary/_internals/builders/PaddedNumberArbitraryBuilder.ts
@@ -0,0 +1,11 @@
+import { Arbitrary } from '../../../check/arbitrary/definition/Arbitrary';
+import { convertFromNext, convertToNext } from '../../../check/arbitrary/definition/Converters';
+import { integer } from '../../integer';
+import { numberToPaddedEightMapper, numberToPaddedEightUnmapper } from '../mappers/NumberToPaddedEight';
+
+/** @internal */
+export function buildPaddedNumberArbitrary(min: number, max: number): Arbitrary<string> {
+  return convertFromNext(
+    convertToNext(integer({ min, max })).map(numberToPaddedEightMapper, numberToPaddedEightUnmapper)
+  );
+}
diff --git a/src/arbitrary/_internals/mappers/NumberToPaddedEight.ts b/src/arbitrary/_internals/mappers/NumberToPaddedEight.ts
new file mode 100644
index 00000000..bf2a672e
--- /dev/null
+++ b/src/arbitrary/_internals/mappers/NumberToPaddedEight.ts
@@ -0,0 +1,19 @@
+/** @internal */
+export function numberToPaddedEightMapper(n: number): string {
+  return n.toString(16).padStart(8, '0');
+}
+
+/** @internal */
+export function numberToPaddedEightUnmapper(value: unknown): number {
+  if (typeof value !== 'string') {
+    throw new Error('Unsupported type');
+  }
+  if (value.length !== 8) {
+    throw new Error('Unsupported value: invalid length');
+  }
+  const n = parseInt(value, 16);
+  if (value !== numberToPaddedEightMapper(n)) {
+    throw new Error('Unsupported value: invalid content');
+  }
+  return n;
+}
diff --git a/src/arbitrary/_internals/mappers/PaddedEightsToUuid.ts b/src/arbitrary/_internals/mappers/PaddedEightsToUuid.ts
new file mode 100644
index 00000000..4da89d40
--- /dev/null
+++ b/src/arbitrary/_internals/mappers/PaddedEightsToUuid.ts
@@ -0,0 +1,19 @@
+/** @internal */
+export function paddedEightsToUuidMapper(t: [string, string, string, string]): string {
+  return `${t[0]}-${t[1].substring(4)}-${t[1].substring(0, 4)}-${t[2].substring(0, 4)}-${t[2].substring(4)}${t[3]}`;
+}
+
+/** @internal */
+const UuidRegex = /^([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})$/;
+
+/** @internal */
+export function paddedEightsToUuidUnmapper(value: unknown): [string, string, string, string] {
+  if (typeof value !== 'string') {
+    throw new Error('Unsupported type');
+  }
+  const m = UuidRegex.exec(value);
+  if (m === null) {
+    throw new Error('Unsupported type');
+  }
+  return [m[1], m[3] + m[2], m[4] + m[5].substring(0, 4), m[5].substring(4)];
+}
diff --git a/src/arbitrary/uuid.ts b/src/arbitrary/uuid.ts
new file mode 100644
index 00000000..cc2a07e3
--- /dev/null
+++ b/src/arbitrary/uuid.ts
@@ -0,0 +1,31 @@
+import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
+import { tuple } from './tuple';
+import { buildPaddedNumberArbitrary } from './_internals/builders/PaddedNumberArbitraryBuilder';
+import { paddedEightsToUuidMapper, paddedEightsToUuidUnmapper } from './_internals/mappers/PaddedEightsToUuid';
+
+/**
+ * For UUID from v1 to v5
+ *
+ * According to {@link https://tools.ietf.org/html/rfc4122 | RFC 4122}
+ *
+ * No mixed case, only lower case digits (0-9a-f)
+ *
+ * @remarks Since 1.17.0
+ * @public
+ */
+export function uuid(): Arbitrary<string> {
+  // According to RFC 4122: Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively
+  // ie.: ????????-????-X???-Y???-????????????
+  //      with X in 1, 2, 3, 4, 5
+  //      with Y in 8, 9, A, B
+  const padded = buildPaddedNumberArbitrary(0, 0xffffffff);
+  const secondPadded = buildPaddedNumberArbitrary(0x10000000, 0x5fffffff);
+  const thirdPadded = buildPaddedNumberArbitrary(0x80000000, 0xbfffffff);
+  return convertFromNext(
+    convertToNext(tuple(padded, secondPadded, thirdPadded, padded)).map(
+      paddedEightsToUuidMapper,
+      paddedEightsToUuidUnmapper
+    )
+  );
+}
diff --git a/src/arbitrary/uuidV.ts b/src/arbitrary/uuidV.ts
new file mode 100644
index 00000000..849d1040
--- /dev/null
+++ b/src/arbitrary/uuidV.ts
@@ -0,0 +1,28 @@
+import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
+import { convertFromNext, convertToNext } from '../check/arbitrary/definition/Converters';
+import { tuple } from './tuple';
+import { buildPaddedNumberArbitrary } from './_internals/builders/PaddedNumberArbitraryBuilder';
+import { paddedEightsToUuidMapper, paddedEightsToUuidUnmapper } from './_internals/mappers/PaddedEightsToUuid';
+
+/**
+ * For UUID of a given version (in v1 to v5)
+ *
+ * According to {@link https://tools.ietf.org/html/rfc4122 | RFC 4122}
+ *
+ * No mixed case, only lower case digits (0-9a-f)
+ *
+ * @remarks Since 1.17.0
+ * @public
+ */
+export function uuidV(versionNumber: 1 | 2 | 3 | 4 | 5): Arbitrary<string> {
+  const padded = buildPaddedNumberArbitrary(0, 0xffffffff);
+  const offsetSecond = versionNumber * 0x10000000;
+  const secondPadded = buildPaddedNumberArbitrary(offsetSecond, offsetSecond + 0x0fffffff);
+  const thirdPadded = buildPaddedNumberArbitrary(0x80000000, 0xbfffffff);
+  return convertFromNext(
+    convertToNext(tuple(padded, secondPadded, thirdPadded, padded)).map(
+      paddedEightsToUuidMapper,
+      paddedEightsToUuidUnmapper
+    )
+  );
+}
diff --git a/src/check/arbitrary/UuidArbitrary.ts b/src/check/arbitrary/UuidArbitrary.ts
deleted file mode 100644
index 95a249fa..00000000
--- a/src/check/arbitrary/UuidArbitrary.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Arbitrary } from './definition/Arbitrary';
-import { integer } from '../../arbitrary/integer';
-import { nat } from '../../arbitrary/nat';
-import { tuple } from '../../arbitrary/tuple';
-
-/** @internal */
-const padEight = (arb: Arbitrary<number>) => arb.map((n) => n.toString(16).padStart(8, '0'));
-
-/**
- * For UUID from v1 to v5
- *
- * According to {@link https://tools.ietf.org/html/rfc4122 | RFC 4122}
- *
- * No mixed case, only lower case digits (0-9a-f)
- *
- * @remarks Since 1.17.0
- * @public
- */
-export function uuid(): Arbitrary<string> {
-  // According to RFC 4122: Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively
-  // ie.: ????????-????-X???-Y???-????????????
-  //      with X in 1, 2, 3, 4, 5
-  //      with Y in 8, 9, A, B
-  const padded = padEight(nat(0xffffffff));
-  const secondPadded = padEight(integer(0x10000000, 0x5fffffff));
-  const thirdPadded = padEight(integer(0x80000000, 0xbfffffff));
-  return tuple(padded, secondPadded, thirdPadded, padded).map((t) => {
-    return `${t[0]}-${t[1].substring(4)}-${t[1].substring(0, 4)}-${t[2].substring(0, 4)}-${t[2].substring(4)}${t[3]}`;
-  });
-}
-
-/**
- * For UUID of a given version (in v1 to v5)
- *
- * According to {@link https://tools.ietf.org/html/rfc4122 | RFC 4122}
- *
- * No mixed case, only lower case digits (0-9a-f)
- *
- * @remarks Since 1.17.0
- * @public
- */
-export function uuidV(versionNumber: 1 | 2 | 3 | 4 | 5): Arbitrary<string> {
-  const padded = padEight(nat(0xffffffff));
-  const secondPadded = padEight(nat(0x0fffffff));
-  const thirdPadded = padEight(integer(0x80000000, 0xbfffffff));
-  return tuple(padded, secondPadded, thirdPadded, padded).map((t) => {
-    return `${t[0]}-${t[1].substring(4)}-${versionNumber}${t[1].substring(1, 4)}-${t[2].substring(
-      0,
-      4
-    )}-${t[2].substring(4)}${t[3]}`;
-  });
-}
diff --git a/src/fast-check-default.ts b/src/fast-check-default.ts
index e1450a93..61dcf4fb 100644
--- a/src/fast-check-default.ts
+++ b/src/fast-check-default.ts
@@ -88,7 +88,8 @@ import { unicodeString } from './arbitrary/unicodeString';
 import { shuffledSubarray, subarray, SubarrayConstraints } from './check/arbitrary/SubarrayArbitrary';
 import { genericTuple } from './arbitrary/genericTuple';
 import { tuple } from './arbitrary/tuple';
-import { uuid, uuidV } from './check/arbitrary/UuidArbitrary';
+import { uuid } from './arbitrary/uuid';
+import { uuidV } from './arbitrary/uuidV';
 import {
   webAuthority,
   WebAuthorityConstraints,


Test patch:
diff --git a/test/unit/arbitrary/_internals/mappers/NumberToPaddedEight.spec.ts b/test/unit/arbitrary/_internals/mappers/NumberToPaddedEight.spec.ts
new file mode 100644
index 00000000..a8ce3c2e
--- /dev/null
+++ b/test/unit/arbitrary/_internals/mappers/NumberToPaddedEight.spec.ts
@@ -0,0 +1,51 @@
+import fc from '../../../../../lib/fast-check';
+import {
+  numberToPaddedEightMapper,
+  numberToPaddedEightUnmapper,
+} from '../../../../../src/arbitrary/_internals/mappers/NumberToPaddedEight';
+
+describe('numberToPaddedEightUnmapper', () => {
+  it('should be able to unmap any mapped value', () =>
+    fc.assert(
+      fc.property(fc.nat({ max: 0xffffffff }), (n) => {
+        // Arrange
+        const mapped = numberToPaddedEightMapper(n);
+
+        // Act
+        const out = numberToPaddedEightUnmapper(mapped);
+
+        // Assert
+        expect(out).toBe(n);
+      })
+    ));
+
+  it('should reject the value whenever it has an invalid length', () => {
+    // Arrange
+    const valueNk = '012345678';
+    const valueOk = '01234567';
+
+    // Act / Assert
+    expect(() => numberToPaddedEightUnmapper(valueNk)).toThrowError();
+    expect(() => numberToPaddedEightUnmapper(valueOk)).not.toThrowError();
+  });
+
+  it('should reject the value whenever it contains an invalid character', () => {
+    // Arrange
+    const valueNk = '0123456z';
+    const valueOk = '01234567';
+
+    // Act / Assert
+    expect(() => numberToPaddedEightUnmapper(valueNk)).toThrowError();
+    expect(() => numberToPaddedEightUnmapper(valueOk)).not.toThrowError();
+  });
+
+  it('should reject the value whenever it contains an uppercase hexa character', () => {
+    // Arrange
+    const valueNk = '89abcdeF';
+    const valueOk = '89abcdef';
+
+    // Act / Assert
+    expect(() => numberToPaddedEightUnmapper(valueNk)).toThrowError();
+    expect(() => numberToPaddedEightUnmapper(valueOk)).not.toThrowError();
+  });
+});
diff --git a/test/unit/arbitrary/_internals/mappers/PaddedEightsToUuid.spec.ts b/test/unit/arbitrary/_internals/mappers/PaddedEightsToUuid.spec.ts
new file mode 100644
index 00000000..0b06b5a5
--- /dev/null
+++ b/test/unit/arbitrary/_internals/mappers/PaddedEightsToUuid.spec.ts
@@ -0,0 +1,28 @@
+import fc from '../../../../../lib/fast-check';
+import {
+  paddedEightsToUuidMapper,
+  paddedEightsToUuidUnmapper,
+} from '../../../../../src/arbitrary/_internals/mappers/PaddedEightsToUuid';
+
+describe('paddedEightsToUuidUnmapper', () => {
+  it('should be able to unmap any mapped value', () =>
+    fc.assert(
+      fc.property(
+        fc.hexaString({ minLength: 8, maxLength: 8 }),
+        fc.hexaString({ minLength: 8, maxLength: 8 }),
+        fc.hexaString({ minLength: 8, maxLength: 8 }),
+        fc.hexaString({ minLength: 8, maxLength: 8 }),
+        (a, b, c, d) => {
+          // Arrange
+          const ins: [string, string, string, string] = [a, b, c, d];
+          const mapped = paddedEightsToUuidMapper(ins);
+
+          // Act
+          const out = paddedEightsToUuidUnmapper(mapped);
+
+          // Assert
+          expect(out).toEqual(ins);
+        }
+      )
+    ));
+});
diff --git a/test/unit/arbitrary/uuid.spec.ts b/test/unit/arbitrary/uuid.spec.ts
new file mode 100644
index 00000000..3ca29e52
--- /dev/null
+++ b/test/unit/arbitrary/uuid.spec.ts
@@ -0,0 +1,82 @@
+import fc from '../../../lib/fast-check';
+import { uuid } from '../../../src/arbitrary/uuid';
+import { fakeNextArbitraryStaticValue } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import { convertFromNextWithShrunkOnce, convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+
+import * as _IntegerMock from '../../../src/arbitrary/integer';
+import { ArbitraryWithShrink } from '../../../src/check/arbitrary/definition/ArbitraryWithShrink';
+import { fakeRandom } from '../check/arbitrary/generic/RandomHelpers';
+import {
+  assertProduceSameValueGivenSameSeed,
+  assertProduceCorrectValues,
+  assertProduceValuesShrinkableWithoutContext,
+  assertShrinkProducesSameValueWithoutInitialContext,
+} from '../check/arbitrary/generic/NextArbitraryAssertions';
+const IntegerMock: { integer: (ct: { min: number; max: number }) => ArbitraryWithShrink<number> } = _IntegerMock;
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('uuid', () => {
+  it('should produce the minimal uuid (v1-v5) given all minimal generated values', () => {
+    // Arrange
+    const { instance: mrng } = fakeRandom();
+    const integer = jest.spyOn(IntegerMock, 'integer');
+    integer.mockImplementation(({ min }) => {
+      const { instance } = fakeNextArbitraryStaticValue(() => min);
+      return convertFromNextWithShrunkOnce(instance, undefined);
+    });
+
+    // Act
+    const arb = convertToNext(uuid());
+    const out = arb.generate(mrng, undefined);
+
+    // Assert
+    expect(out.value).toBe('00000000-0000-1000-8000-000000000000');
+  });
+
+  it('should produce the maximal uuid (v1-v5) given all maximal generated values', () => {
+    // Arrange
+    const { instance: mrng } = fakeRandom();
+    const integer = jest.spyOn(IntegerMock, 'integer');
+    integer.mockImplementation(({ max }) => {
+      const { instance } = fakeNextArbitraryStaticValue(() => max);
+      return convertFromNextWithShrunkOnce(instance, undefined);
+    });
+
+    // Act
+    const arb = convertToNext(uuid());
+    const out = arb.generate(mrng, undefined);
+
+    // Assert
+    expect(out.value).toBe('ffffffff-ffff-5fff-bfff-ffffffffffff');
+  });
+});
+
+describe('uuid (integration)', () => {
+  const isCorrect = (u: string) => {
+    expect(u).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[12345][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
+  };
+
+  const uuidVBuilder = () => convertToNext(uuid());
+
+  it('should produce the same values given the same seed', () => {
+    assertProduceSameValueGivenSameSeed(uuidVBuilder);
+  });
+
+  it('should only produce correct values', () => {
+    assertProduceCorrectValues(uuidVBuilder, isCorrect);
+  });
+
+  it('should produce values seen as shrinkable without any context', () => {
+    assertProduceValuesShrinkableWithoutContext(uuidVBuilder);
+  });
+
+  it('should be able to shrink to the same values without initial context', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(uuidVBuilder);
+  });
+});
diff --git a/test/unit/arbitrary/uuidV.spec.ts b/test/unit/arbitrary/uuidV.spec.ts
new file mode 100644
index 00000000..f2287647
--- /dev/null
+++ b/test/unit/arbitrary/uuidV.spec.ts
@@ -0,0 +1,100 @@
+import fc from '../../../lib/fast-check';
+import { uuidV } from '../../../src/arbitrary/uuidV';
+import { fakeNextArbitraryStaticValue } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import { convertFromNextWithShrunkOnce, convertToNext } from '../../../src/check/arbitrary/definition/Converters';
+
+import * as _IntegerMock from '../../../src/arbitrary/integer';
+import { ArbitraryWithShrink } from '../../../src/check/arbitrary/definition/ArbitraryWithShrink';
+import { fakeRandom } from '../check/arbitrary/generic/RandomHelpers';
+import {
+  assertProduceCorrectValues,
+  assertProduceSameValueGivenSameSeed,
+  assertProduceValuesShrinkableWithoutContext,
+  assertShrinkProducesSameValueWithoutInitialContext,
+} from '../check/arbitrary/generic/NextArbitraryAssertions';
+const IntegerMock: { integer: (ct: { min: number; max: number }) => ArbitraryWithShrink<number> } = _IntegerMock;
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('uuidV', () => {
+  it.each`
+    version | expected
+    ${1}    | ${'00000000-0000-1000-8000-000000000000'}
+    ${2}    | ${'00000000-0000-2000-8000-000000000000'}
+    ${3}    | ${'00000000-0000-3000-8000-000000000000'}
+    ${4}    | ${'00000000-0000-4000-8000-000000000000'}
+    ${5}    | ${'00000000-0000-5000-8000-000000000000'}
+  `('should produce the minimal uuid (v$version) given all minimal generated values', ({ version, expected }) => {
+    // Arrange
+    const { instance: mrng } = fakeRandom();
+    const integer = jest.spyOn(IntegerMock, 'integer');
+    integer.mockImplementation(({ min }) => {
+      const { instance } = fakeNextArbitraryStaticValue(() => min);
+      return convertFromNextWithShrunkOnce(instance, undefined);
+    });
+
+    // Act
+    const arb = convertToNext(uuidV(version));
+    const out = arb.generate(mrng, undefined);
+
+    // Assert
+    expect(out.value).toBe(expected);
+  });
+
+  it.each`
+    version | expected
+    ${1}    | ${'ffffffff-ffff-1fff-bfff-ffffffffffff'}
+    ${2}    | ${'ffffffff-ffff-2fff-bfff-ffffffffffff'}
+    ${3}    | ${'ffffffff-ffff-3fff-bfff-ffffffffffff'}
+    ${4}    | ${'ffffffff-ffff-4fff-bfff-ffffffffffff'}
+    ${5}    | ${'ffffffff-ffff-5fff-bfff-ffffffffffff'}
+  `('should produce the maximal uuid (v$version) given all maximal generated values', ({ version, expected }) => {
+    // Arrange
+    const { instance: mrng } = fakeRandom();
+    const integer = jest.spyOn(IntegerMock, 'integer');
+    integer.mockImplementation(({ max }) => {
+      const { instance } = fakeNextArbitraryStaticValue(() => max);
+      return convertFromNextWithShrunkOnce(instance, undefined);
+    });
+
+    // Act
+    const arb = convertToNext(uuidV(version));
+    const out = arb.generate(mrng, undefined);
+
+    // Assert
+    expect(out.value).toBe(expected);
+  });
+});
+
+describe('uuidV (integration)', () => {
+  type Extra = 1 | 2 | 3 | 4 | 5;
+  const extraParameters: fc.Arbitrary<Extra> = fc.constantFrom(...([1, 2, 3, 4, 5] as const));
+
+  const isCorrect = (u: string, extra: Extra) => {
+    expect(u).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[12345][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
+    expect(u[14]).toBe(String(extra));
+  };
+
+  const uuidVBuilder = (extra: Extra) => convertToNext(uuidV(extra));
+
+  it('should produce the same values given the same seed', () => {
+    assertProduceSameValueGivenSameSeed(uuidVBuilder, { extraParameters });
+  });
+
+  it('should only produce correct values', () => {
+    assertProduceCorrectValues(uuidVBuilder, isCorrect, { extraParameters });
+  });
+
+  it('should produce values seen as shrinkable without any context', () => {
+    assertProduceValuesShrinkableWithoutContext(uuidVBuilder, { extraParameters });
+  });
+
+  it('should be able to shrink to the same values without initial context', () => {
+    assertShrinkProducesSameValueWithoutInitialContext(uuidVBuilder, { extraParameters });
+  });
+});
diff --git a/test/unit/check/arbitrary/UuidArbitrary.itest.spec.ts b/test/unit/check/arbitrary/UuidArbitrary.itest.spec.ts
deleted file mode 100644
index 64cfeeb3..00000000
--- a/test/unit/check/arbitrary/UuidArbitrary.itest.spec.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { uuid, uuidV } from '../../../../src/check/arbitrary/UuidArbitrary';
-import * as genericHelper from './generic/GenericArbitraryHelper';
-import fc from '../../../../lib/fast-check';
-
-describe('UuidArbitrary', () => {
-  describe('uuid', () => {
-    genericHelper.isValidArbitrary(() => uuid(), {
-      isValidValue: (g: string) => /[0-9a-f]{8}-[0-9a-f]{4}-[12345][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(g),
-      isStrictlySmallerValue: (a, b) => a !== b,
-    });
-  });
-  describe('uuidV', () => {
-    genericHelper.isValidArbitrary((constraint: 1 | 2 | 3 | 4 | 5) => uuidV(constraint), {
-      seedGenerator: fc.constantFrom(...([1, 2, 3, 4, 5] as const)),
-      isValidValue: (g: string, constraint: 1 | 2 | 3 | 4 | 5) =>
-        /[0-9a-f]{8}-[0-9a-f]{4}-[12345][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/.test(g) &&
-        g[14] === String(constraint),
-      isStrictlySmallerValue: (a, b) => a !== b,
-    });
-  });
-});
diff --git a/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts b/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts
deleted file mode 100644
index d0f2085b..00000000
--- a/test/unit/check/arbitrary/UuidArbitrary.utest.spec.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import { uuid, uuidV } from '../../../../src/check/arbitrary/UuidArbitrary';
-import * as stubRng from '../../stubs/generators';
-import { mocked } from 'ts-jest/utils';
-import { ArbitraryWithShrink } from '../../../../src/check/arbitrary/definition/ArbitraryWithShrink';
-
-jest.mock('../../../../src/arbitrary/integer');
-jest.mock('../../../../src/arbitrary/nat');
-jest.mock('../../../../src/arbitrary/tuple');
-import * as _IntegerMock from '../../../../src/arbitrary/integer';
-import * as _NatMock from '../../../../src/arbitrary/nat';
-import * as TupleMock from '../../../../src/arbitrary/tuple';
-import { arbitraryFor } from './generic/ArbitraryBuilder';
-
-const IntegerMock: { integer: (min: number, max: number) => ArbitraryWithShrink<number> } = _IntegerMock;
-const NatMock: { nat: (max: number) => ArbitraryWithShrink<number> } = _NatMock;
-
-const mrng = () => stubRng.mutable.nocall();
-
-describe('UuidArbitrary', () => {
-  describe('uuid', () => {
-    beforeEach(() => {
-      jest.clearAllMocks();
-    });
-    it('Should be able to build the smallest uuid', () => {
-      // Arrange
-      const { integer } = mocked(IntegerMock);
-      const { nat } = mocked(NatMock);
-      const { tuple } = mocked(TupleMock);
-      nat.mockImplementation(() => arbitraryFor([{ value: 0 }, { value: 0 }, { value: 0 }]));
-      integer.mockImplementation((a, _b) => arbitraryFor([{ value: a }]));
-      tuple.mockImplementation((...arbs) =>
-        arbitraryFor([{ value: arbs.map((a) => a.generate(mrng()).value_) as any }])
-      );
-
-      // Act
-      const arb = uuid();
-      const { value_: u } = arb.generate(mrng());
-
-      // Assert
-      expect(u).toBe(`00000000-0000-1000-8000-000000000000`);
-      expect(tuple).toHaveBeenCalledTimes(1);
-    });
-    it('Should be able to build the largest uuid', () => {
-      // Arrange
-      const { integer } = mocked(IntegerMock);
-      const { nat } = mocked(NatMock);
-      const { tuple } = mocked(TupleMock);
-      nat.mockImplementation((a) => arbitraryFor([{ value: a }, { value: a }, { value: a }]));
-      integer.mockImplementation((a, b) => arbitraryFor([{ value: b }]));
-      tuple.mockImplementation((...arbs) =>
-        arbitraryFor([{ value: arbs.map((a) => a.generate(mrng()).value_) as any }])
-      );
-
-      // Act
-      const arb = uuid();
-      const { value_: u } = arb.generate(mrng());
-
-      // Assert
-      expect(u).toBe(`ffffffff-ffff-5fff-bfff-ffffffffffff`);
-      expect(tuple).toHaveBeenCalledTimes(1);
-    });
-  });
-  describe('uuidV', () => {
-    beforeEach(() => {
-      jest.clearAllMocks();
-    });
-    it('Should be able to build the smallest versioned uuid', () => {
-      // Arrange
-      const { integer } = mocked(IntegerMock);
-      const { nat } = mocked(NatMock);
-      const { tuple } = mocked(TupleMock);
-      nat.mockImplementation(() => arbitraryFor([{ value: 0 }, { value: 0 }]));
-      integer.mockImplementation((a, _b) => arbitraryFor([{ value: a }]));
-      tuple.mockImplementation((...arbs) =>
-        arbitraryFor([{ value: arbs.map((a) => a.generate(mrng()).value_) as any }])
-      );
-
-      // Act
-      const arb = uuidV(3);
-      const { value_: u } = arb.generate(mrng());
-
-      // Assert
-      expect(u).toBe(`00000000-0000-3000-8000-000000000000`);
-      expect(tuple).toHaveBeenCalledTimes(1);
-    });
-    it('Should be able to build the largest versioned uuid', () => {
-      // Arrange
-      const { integer } = mocked(IntegerMock);
-      const { nat } = mocked(NatMock);
-      const { tuple } = mocked(TupleMock);
-      nat.mockImplementation((a) => arbitraryFor([{ value: a }, { value: a }]));
-      integer.mockImplementation((a, b) => arbitraryFor([{ value: b }]));
-      tuple.mockImplementation((...arbs) =>
-        arbitraryFor([{ value: arbs.map((a) => a.generate(mrng()).value_) as any }])
-      );
-
-      // Act
-      const arb = uuidV(3);
-      const { value_: u } = arb.generate(mrng());
-
-      // Assert
-      expect(u).toBe(`ffffffff-ffff-3fff-bfff-ffffffffffff`);
-      expect(tuple).toHaveBeenCalledTimes(1);
-    });
-  });
-});
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
index 5f6e4262..850f60db 100644
--- a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
+++ b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
@@ -42,6 +42,21 @@ export function fakeNextArbitrary<T = any>(): { instance: NextArbitrary<T> } & M
   return { instance: new Class(), ...mockedMethods };
 }
 
+/**
+ * Generate a fake instance inheriting from NextArbitrary but always producing the same value
+ */
+export function fakeNextArbitraryStaticValue<T>(
+  value: () => T,
+  context: () => unknown = () => undefined
+): { instance: NextArbitrary<T> } {
+  const { instance, generate, map } = fakeNextArbitrary<T>();
+  generate.mockImplementation(() => new NextValue(value(), context()));
+  map.mockImplementation((mapper) => {
+    return fakeNextArbitraryStaticValue(() => mapper(value())).instance;
+  });
+  return { instance };
+}
+
 /**
  * Fake instance with the following capabilities:
  * - shrink to strictly smaller values