Repository: dubzzz/fast-check
Pull request #1734 (2021-04-13 06:33:57)

⬆️ Bump actions/cache from v2.1.4 to v2.1.5
Bumps [actions/cache](https://github.com/actions/cache) from v2.1.4 to v2.1.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/cache/releases">actions/cache's releases</a>.</em></p>
<blockquote>
<h2>v2.1.5</h2>
<ul>
<li>Fix permissions error seen when extracting caches with GNU tar that were previously created using BSD tar (<a href="https://github-redirect.dependabot.com/actions/cache/issues/527">actions/cache#527</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/cache/commit/1a9e2138d905efd099035b49d8b7a3888c653ca8"><code>1a9e213</code></a> Update cache module to v1.0.7 (<a href="https://github-redirect.dependabot.com/actions/cache/issues/562">#562</a>)</li>
<li><a href="https://github.com/actions/cache/commit/981fa981ed950efef5277587ee4ba0eead30cadf"><code>981fa98</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/actions/cache/issues/469">#469</a> from ericmj/patch-1</li>
<li><a href="https://github.com/actions/cache/commit/4498c5b4d8b0673dbc7d9731bc11d9cc5a2c6087"><code>4498c5b</code></a> Drop the example based on using pip's internals (<a href="https://github-redirect.dependabot.com/actions/cache/issues/519">#519</a>)</li>
<li><a href="https://github.com/actions/cache/commit/4134e6de47156381d31ff74f8aabf86718858f70"><code>4134e6d</code></a> It is not recommended to cache node_modules (<a href="https://github-redirect.dependabot.com/actions/cache/issues/537">#537</a>)</li>
<li><a href="https://github.com/actions/cache/commit/62a4d754427893bc6d043c6d4f5486490cab29b6"><code>62a4d75</code></a> Also cache _build for Elixir</li>
<li>See full diff in <a href="https://github.com/actions/cache/compare/v2.1.4...1a9e2138d905efd099035b49d8b7a3888c653ca8">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/1734/builds/119079) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 047c26f707877ed288ef040ba7ae00d8c33e60d4:

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

codecov[bot]: # [Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1734?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) Report
> Merging [#1734](https://codecov.io/gh/dubzzz/fast-check/pull/1734?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (047c26f) into [main](https://codecov.io/gh/dubzzz/fast-check/commit/cb4be4d32f7ed47dfdfed4925f8eb4bf4cfa4af3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN) (cb4be4d) will **not change** coverage.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/dubzzz/fast-check/pull/1734/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/1734?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    #1734   +/-   ##
=======================================
  Coverage   96.60%   96.60%           
=======================================
  Files         109      109           
  Lines        3593     3593           
  Branches      713      713           
=======================================
  Hits         3471     3471           
  Misses        118      118           
  Partials        4        4           
```

| Flag | Coverage Δ | |
|---|---|---|
| unit-tests | `96.60% <ø> (ø)` | |
| unit-tests-10.x-Linux | `?` | |
| unit-tests-12.x-Linux | `?` | |
| unit-tests-14.x-Linux | `96.60% <ø> (ø)` | |

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.


------

[Continue to review full report at Codecov](https://codecov.io/gh/dubzzz/fast-check/pull/1734?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/1734?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Nicolas+DUBIEN). Last update [cb4be4d...047c26f](https://codecov.io/gh/dubzzz/fast-check/pull/1734?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/.github/workflows/build-status.yml b/.github/workflows/build-status.yml
index da5b465e..6ee32f14 100644
--- a/.github/workflows/build-status.yml
+++ b/.github/workflows/build-status.yml
@@ -20,7 +20,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/cache@v2.1.4
+      - uses: actions/cache@v2.1.5
         with:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}
@@ -42,7 +42,7 @@ jobs:
         node-version: [10.x, 12.x, 14.x]
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/cache@v2.1.4
+      - uses: actions/cache@v2.1.5
         with:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-${{matrix.node-version}}
@@ -79,7 +79,7 @@ jobs:
         os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/cache@v2.1.4
+      - uses: actions/cache@v2.1.5
         with:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-${{matrix.node-version}}
@@ -116,7 +116,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/cache@v2.1.4
+      - uses: actions/cache@v2.1.5
         with:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-v14.x
@@ -144,7 +144,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v2
-      - uses: actions/cache@v2.1.4
+      - uses: actions/cache@v2.1.5
         with:
           path: '**/node_modules'
           key: ${{runner.os}}-modules-${{hashFiles('**/yarn.lock')}}-v14.x
diff --git a/src/arbitrary/_internals/FrequencyArbitrary.ts b/src/arbitrary/_internals/FrequencyArbitrary.ts
index 6d925604..19d36894 100644
--- a/src/arbitrary/_internals/FrequencyArbitrary.ts
+++ b/src/arbitrary/_internals/FrequencyArbitrary.ts
@@ -11,7 +11,17 @@ export class FrequencyArbitrary<T> extends NextArbitrary<T> {
   readonly summedWarbs: _WeightedNextArbitrary<T>[];
   readonly totalWeight: number;
 
-  static from<T>(warbs: _WeightedArbitrary<T>[], constraints: _FrequencyContraints, label: string): Arbitrary<T> {
+  static fromOld<T>(warbs: _WeightedArbitrary<T>[], constraints: _Constraints, label: string): Arbitrary<T> {
+    return convertFromNext(
+      FrequencyArbitrary.from(
+        warbs.map((w) => ({ ...w, arbitrary: convertToNext(w.arbitrary) })),
+        constraints,
+        label
+      )
+    );
+  }
+
+  static from<T>(warbs: _WeightedNextArbitrary<T>[], constraints: _Constraints, label: string): NextArbitrary<T> {
     if (warbs.length === 0) {
       throw new Error(`${label} expects at least one weigthed arbitrary`);
     }
@@ -30,19 +40,17 @@ export class FrequencyArbitrary<T> extends NextArbitrary<T> {
       if (currentWeight < 0) {
         throw new Error(`${label} expects weights to be superior or equal to 0`);
       }
-      warbsNext.push({ weight: currentWeight, arbitrary: convertToNext(currentArbitrary) });
+      warbsNext.push({ weight: currentWeight, arbitrary: currentArbitrary });
     }
     if (totalWeight <= 0) {
       throw new Error(`${label} expects the sum of weights to be strictly superior to 0`);
     }
-    return convertFromNext(
-      new FrequencyArbitrary(warbsNext, constraints, getDepthContextFor(constraints.depthIdentifier))
-    );
+    return new FrequencyArbitrary(warbsNext, constraints, getDepthContextFor(constraints.depthIdentifier));
   }
 
   private constructor(
     readonly warbs: _WeightedNextArbitrary<T>[],
-    readonly constraints: _FrequencyContraints,
+    readonly constraints: _Constraints,
     readonly context: DepthContext
   ) {
     super();
@@ -106,11 +114,11 @@ export class FrequencyArbitrary<T> extends NextArbitrary<T> {
 
   /** Extract the index of the generator that would have been able to gennrate the value */
   private canGenerateIndex(value: unknown): number {
-    ++this.context.depth; // increase depth
+    if (this.mustGenerateFirst()) {
+      return this.warbs[0].arbitrary.canGenerate(value) ? 0 : -1;
+    }
     try {
-      if (this.mustGenerateFirst()) {
-        return this.warbs[0].arbitrary.canGenerate(value) ? 0 : -1;
-      }
+      ++this.context.depth; // increase depth
       for (let idx = 0; idx !== this.warbs.length; ++idx) {
         const warb = this.warbs[idx];
         if (warb.weight !== 0 && warb.arbitrary.canGenerate(value)) {
@@ -175,6 +183,14 @@ export class FrequencyArbitrary<T> extends NextArbitrary<T> {
   }
 }
 
+/** @internal */
+export type _Constraints = {
+  withCrossShrink?: boolean;
+  depthFactor?: number;
+  maxDepth?: number;
+  depthIdentifier?: string;
+};
+
 /** @internal */
 interface _WeightedArbitrary<T> {
   weight: number;
@@ -187,14 +203,6 @@ interface _WeightedNextArbitrary<T> {
   arbitrary: NextArbitrary<T>;
 }
 
-/** @internal */
-type _FrequencyContraints = {
-  withCrossShrink?: boolean;
-  depthFactor?: number;
-  maxDepth?: number;
-  depthIdentifier?: string;
-};
-
 /** @internal */
 type _FrequencyArbitraryContext<T> = {
   selectedIndex: number;
diff --git a/src/arbitrary/frequency.ts b/src/arbitrary/frequency.ts
index 8feb0a86..5b0308ad 100644
--- a/src/arbitrary/frequency.ts
+++ b/src/arbitrary/frequency.ts
@@ -122,9 +122,9 @@ function frequency<Ts extends WeightedArbitrary<unknown>[]>(
   const label = 'fc.frequency';
   const constraints = args[0];
   if (isFrequencyContraints(constraints)) {
-    return FrequencyArbitrary.from(args.slice(1) as WeightedArbitrary<FrequencyValue<Ts>>[], constraints, label);
+    return FrequencyArbitrary.fromOld(args.slice(1) as WeightedArbitrary<FrequencyValue<Ts>>[], constraints, label);
   }
 
-  return FrequencyArbitrary.from(args as WeightedArbitrary<FrequencyValue<Ts>>[], {}, label);
+  return FrequencyArbitrary.fromOld(args as WeightedArbitrary<FrequencyValue<Ts>>[], {}, label);
 }
 export { frequency };
diff --git a/src/arbitrary/oneof.ts b/src/arbitrary/oneof.ts
index 5a411d2c..3d3b2369 100644
--- a/src/arbitrary/oneof.ts
+++ b/src/arbitrary/oneof.ts
@@ -93,9 +93,9 @@ function oneof<Ts extends Arbitrary<unknown>[]>(
   const constraints = args[0];
   if (isOneOfContraints(constraints)) {
     const weightedArbs = (args.slice(1) as Arbitrary<OneOfValue<Ts>>[]).map((arbitrary) => ({ arbitrary, weight: 1 }));
-    return FrequencyArbitrary.from(weightedArbs, constraints, 'fc.oneof');
+    return FrequencyArbitrary.fromOld(weightedArbs, constraints, 'fc.oneof');
   }
   const weightedArbs = (args as Arbitrary<OneOfValue<Ts>>[]).map((arbitrary) => ({ arbitrary, weight: 1 }));
-  return FrequencyArbitrary.from(weightedArbs, {}, 'fc.oneof');
+  return FrequencyArbitrary.fromOld(weightedArbs, {}, 'fc.oneof');
 }
 export { oneof };
diff --git a/src/arbitrary/option.ts b/src/arbitrary/option.ts
index 20bf08c9..680d9766 100644
--- a/src/arbitrary/option.ts
+++ b/src/arbitrary/option.ts
@@ -1,7 +1,6 @@
-import { FrequencyContraints } from '../fast-check-default';
 import { constant } from '../check/arbitrary/ConstantArbitrary';
 import { Arbitrary } from '../check/arbitrary/definition/Arbitrary';
-import { FrequencyArbitrary } from './_internals/FrequencyArbitrary';
+import { FrequencyArbitrary, _Constraints as FrequencyContraints } from './_internals/FrequencyArbitrary';
 
 /**
  * Constraints to be applied on {@link option}
@@ -47,8 +46,8 @@ export interface OptionConstraints<TNil = null> {
 
 /** @internal */
 function extractOptionConstraints<TNil>(constraints?: number | OptionConstraints<TNil>): OptionConstraints<TNil> {
-  if (!constraints) return {};
   if (typeof constraints === 'number') return { freq: constraints };
+  if (!constraints) return {};
   return constraints;
 }
 
@@ -99,6 +98,6 @@ function option<T, TNil>(arb: Arbitrary<T>, rawConstraints?: number | OptionCons
     maxDepth: constraints.maxDepth,
     depthIdentifier: constraints.depthIdentifier,
   };
-  return FrequencyArbitrary.from(weightedArbs, frequencyConstraints, 'fc.option');
+  return FrequencyArbitrary.fromOld(weightedArbs, frequencyConstraints, 'fc.option');
 }
 export { option };


Test patch:
diff --git a/test/unit/arbitrary/FrequencyArbitrary.itest.spec.ts b/test/unit/arbitrary/FrequencyArbitrary.itest.spec.ts
deleted file mode 100644
index d1cb00ea..00000000
--- a/test/unit/arbitrary/FrequencyArbitrary.itest.spec.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import * as fc from '../../../lib/fast-check';
-
-import { frequency } from '../../../src/arbitrary/frequency';
-import { integer } from '../../../src/arbitrary/integer';
-import { constant } from '../../../src/check/arbitrary/ConstantArbitrary';
-
-import * as genericHelper from '../check/arbitrary/generic/GenericArbitraryHelper';
-
-import { mocked } from 'ts-jest/utils';
-import * as DepthContextMock from '../../../src/arbitrary/_internals/helpers/DepthContext';
-jest.mock('../../../src/arbitrary/_internals/helpers/DepthContext');
-
-const depthContextData: Record<string, DepthContextMock.DepthContext> = {};
-
-beforeEach(() => {
-  // Cleaning between runs (WARNING: Doew not clean within properties themselves)
-  for (const k in depthContextData) delete depthContextData[k];
-
-  // Mocking
-  const { getDepthContextFor } = mocked(DepthContextMock);
-  getDepthContextFor.mockImplementation((key) => {
-    if (key === undefined) return { depth: 0 };
-    if (typeof key !== 'string') return key;
-    if (!(key in depthContextData)) depthContextData[key] = { depth: 0 };
-    return depthContextData[key];
-  });
-});
-
-describe('FrequencyArbitrary', () => {
-  describe('frequency', () => {
-    const seedGenerator = fc.record(
-      {
-        data: fc
-          .array(
-            fc.record({
-              type: fc.constantFrom('unique', 'range'),
-              value: fc.nat(),
-              weight: fc.nat(),
-            }),
-            { minLength: 1 }
-          )
-          .filter((config) => {
-            const totalWeight = config.reduce((acc, e) => acc + e.weight, 0);
-            return totalWeight > 0;
-          }),
-        constraints: fc.record(
-          {
-            withCrossShrink: fc.boolean(),
-            depthFactor: fc.double({ min: 0, max: Number.MAX_VALUE, noNaN: true }),
-            maxDepth: fc.nat(),
-            depthIdentifierMetas: fc.record(
-              {
-                identifier: fc.constantFrom('toto', 'titi', 'tata'), // no need to have thousands of them
-                initial: fc.nat(),
-              },
-              { requiredKeys: ['identifier', 'initial'] }
-            ),
-          },
-          { requiredKeys: [] }
-        ),
-      },
-      { requiredKeys: ['data'] }
-    );
-
-    type SeedGeneratorType = typeof seedGenerator extends fc.Arbitrary<infer T> ? T : never;
-
-    function canProduce(dataFromMetas: SeedGeneratorType['data'][number], value: number): boolean {
-      switch (dataFromMetas.type) {
-        case 'unique':
-          return dataFromMetas.value === value;
-        case 'range':
-          return dataFromMetas.value - 10 <= value && value <= dataFromMetas.value;
-      }
-      return false;
-    }
-
-    genericHelper.isValidArbitrary(
-      (metas: SeedGeneratorType) => {
-        const arbs = metas.data.map((m) =>
-          m.type === 'unique'
-            ? { arbitrary: constant(m.value), weight: m.weight }
-            : { arbitrary: integer(m.value - 10, m.value), weight: m.weight }
-        );
-        if (metas.constraints === undefined) {
-          return frequency(...arbs);
-        }
-
-        const sanitizedConstraints =
-          metas.constraints.depthIdentifierMetas !== undefined
-            ? { ...metas.constraints, depthIdentifier: metas.constraints.depthIdentifierMetas.identifier }
-            : metas.constraints;
-        if (metas.constraints.depthIdentifierMetas !== undefined) {
-          // WARNING - This side-effect is not supposed to alter other tests but who knows!?
-          const { identifier, initial } = metas.constraints.depthIdentifierMetas;
-          depthContextData[identifier] = { depth: initial };
-        }
-        return frequency(sanitizedConstraints, ...arbs);
-      },
-      {
-        seedGenerator,
-        isValidValue: (v: number, metas: SeedGeneratorType) => {
-          const constraints = metas.constraints || {};
-          const { identifier, initial: initialDepth = 0 } = constraints.depthIdentifierMetas || {};
-          // Check if depth has been reset after generate
-          if (identifier !== undefined) {
-            expect(depthContextData[identifier]).toEqual({ depth: initialDepth });
-          }
-          // If maxDepth is <= initialDepth, then only the first arbitrary can be called
-          if (constraints.maxDepth !== undefined && constraints.maxDepth <= initialDepth) {
-            expect(canProduce(metas.data[0], v)).toBe(true);
-          } else {
-            expect(metas.data.some((m) => m.weight !== 0 && canProduce(m, v))).toBe(true);
-          }
-          return true;
-        },
-        isStrictlySmallerValue: (a: number, b: number, metas: SeedGeneratorType) => {
-          // When withCrossShrink is toggled, the shrinker can jump from one arbitrary to the first one on shrink
-          // But only if the weight associated to the first arbitrary is strictly greater than 0
-          if (metas.constraints !== undefined && metas.constraints.withCrossShrink && metas.data[0].weight > 0) {
-            const canBeInFirstArbitrary =
-              metas.data[0].type === 'unique'
-                ? metas.data[0].value === a
-                : metas.data[0].value - 10 <= a && a <= metas.data[0].value;
-            if (canBeInFirstArbitrary) {
-              // `a` is possibly coming from our first arbitrary
-              return true;
-            }
-          }
-          // Otherwise, shrinks are always coming from the arbitrary itself
-          return Math.abs(b - a) <= 10 && b > 0 ? b - a > 0 : b - a < 0;
-        },
-      }
-    );
-  });
-});
diff --git a/test/unit/arbitrary/FrequencyArbitrary.utest.spec.ts b/test/unit/arbitrary/FrequencyArbitrary.utest.spec.ts
deleted file mode 100644
index ea30f71b..00000000
--- a/test/unit/arbitrary/FrequencyArbitrary.utest.spec.ts
+++ /dev/null
@@ -1,292 +0,0 @@
-import * as fc from '../../../lib/fast-check';
-
-import { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary';
-import { Shrinkable } from '../../../src/check/arbitrary/definition/Shrinkable';
-import { frequency } from '../../../src/arbitrary/frequency';
-import { Random } from '../../../src/random/generator/Random';
-
-import * as stubArb from '../stubs/arbitraries';
-
-const frequencyValidInputsArb = fc
-  .tuple(
-    fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() }),
-    fc.array(fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() })),
-    fc.array(fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() }))
-  )
-  .map(([positiveWeightMeta, headingWeightsMeta, traillingWeightsMeta]) => {
-    return [...headingWeightsMeta, positiveWeightMeta, ...traillingWeightsMeta].map((meta) => ({
-      weight: meta.weight,
-      arbitrary: stubArb.single(meta.arbitraryValue, true),
-      expectedValue: meta.arbitraryValue,
-    }));
-  });
-
-class LazyArb extends Arbitrary<any> {
-  constructor(readonly arbBuilder: () => Arbitrary<any>) {
-    super();
-  }
-  generate(mrng: Random): Shrinkable<any, any> {
-    return this.arbBuilder().generate(mrng);
-  }
-  withBias(freq: number) {
-    return this.arbBuilder().withBias(freq);
-  }
-}
-
-describe('FrequencyArbitrary', () => {
-  describe('frequency', () => {
-    it('Should call Random generator to generate values between 0 and total weight (not included)', () =>
-      fc.assert(
-        fc.property(frequencyValidInputsArb, fc.nat(), (validInputs, generateSeed) => {
-          // Arrange
-          const arb = frequency(...validInputs);
-          const totalWeight = validInputs.reduce((acc, cur) => acc + cur.weight, 0);
-          const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn();
-          nextInt.mockImplementation((a = 0, b = 0) => a + (generateSeed % (b - a + 1)));
-          const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-          // Act
-          arb.generate(fakeRandom);
-
-          // Assert
-          expect(nextInt).toHaveBeenCalledTimes(1);
-          expect(nextInt).toHaveBeenCalledWith(0, totalWeight - 1);
-        })
-      ));
-    it('Should call the right arbitrary to generate the value', () =>
-      fc.assert(
-        fc.property(
-          frequencyValidInputsArb,
-          fc.nat(),
-          fc.nat(),
-          (validInputs, arbitrarySelectionSeed, generateSeed) => {
-            // Arrange
-            const selectedArbitraryIndex = arbitrarySelectionSeed % validInputs.length;
-            const selectedArbitrary = validInputs[selectedArbitraryIndex];
-            fc.pre(selectedArbitrary.weight > 0);
-
-            const totalWeightBefore = validInputs
-              .slice(0, selectedArbitraryIndex)
-              .reduce((acc, cur) => acc + cur.weight, 0);
-            const arb = frequency(...validInputs);
-            const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn();
-            nextInt.mockImplementation(() => totalWeightBefore + (generateSeed % selectedArbitrary.weight));
-            const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-            // Act
-            const g = arb.generate(fakeRandom).value_;
-
-            // Assert
-            expect(g).toBe(selectedArbitrary.expectedValue);
-          }
-        )
-      ));
-    it('Should call the first arbitrary to generate the value when maxDepth of 0', () =>
-      fc.assert(
-        fc.property(frequencyValidInputsArb, (validInputs) => {
-          // Arrange
-          const arb = frequency({ maxDepth: 0 }, ...validInputs);
-          const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn();
-          const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-          // Act
-          const g = arb.generate(fakeRandom).value_;
-
-          // Assert
-          expect(nextInt).not.toHaveBeenCalled();
-          expect(g).toBe(validInputs[0].expectedValue);
-        })
-      ));
-    it('Should call the first arbitrary to generate the value as soon as maxDepth has been reached', () => {
-      // Arrange
-      const arb: Arbitrary<any> = frequency(
-        { maxDepth: 5 },
-        { weight: 0, arbitrary: stubArb.single(0) },
-        { weight: 1, arbitrary: new LazyArb(() => arb).map((d) => [d]) }
-      );
-      const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn().mockReturnValue(0);
-      const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-      // Act
-      const g = arb.generate(fakeRandom).value_;
-
-      // Assert
-      expect(nextInt).toHaveBeenCalledTimes(5); // maxDepth
-      expect(g).toEqual([[[[[0]]]]]); // const mapper = (d) => [d]; mapper(mapper(mapper(mapper(mapper(0))))) is [[[[[0]]]]]
-    });
-    it('Should not share depth accross distinct instances of frequency (if not requested)', () => {
-      // Arrange
-      const arb: Arbitrary<any> = frequency(
-        { maxDepth: 1 },
-        { weight: 0, arbitrary: stubArb.single(0) },
-        {
-          weight: 1,
-          arbitrary: frequency(
-            { maxDepth: 1 },
-            { weight: 0, arbitrary: stubArb.single(0) },
-            {
-              weight: 1,
-              arbitrary: frequency(
-                { maxDepth: 1 },
-                { weight: 0, arbitrary: stubArb.single(0) },
-                { weight: 1, arbitrary: stubArb.single(1) }
-              ).map((d) => [d]),
-            }
-          ).map((d) => [d]),
-        }
-      );
-      const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn().mockReturnValue(0);
-      const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-      // Act
-      const g = arb.generate(fakeRandom).value_;
-
-      // Assert
-      expect(nextInt).toHaveBeenCalledTimes(3); // once per instance of frequency
-      expect(g).toEqual([[1]]);
-    });
-    it('Should share depth accross distinct instances of frequency if requested', () => {
-      // Arrange
-      const arb: Arbitrary<any> = frequency(
-        { maxDepth: 1, depthIdentifier: 'toto' },
-        { weight: 0, arbitrary: stubArb.single(0) },
-        {
-          weight: 1,
-          arbitrary: frequency(
-            { maxDepth: 1, depthIdentifier: 'titi' },
-            { weight: 0, arbitrary: stubArb.single(0) },
-            {
-              weight: 1,
-              arbitrary: frequency(
-                { maxDepth: 1, depthIdentifier: 'toto' },
-                { weight: 0, arbitrary: stubArb.single(0) },
-                { weight: 1, arbitrary: stubArb.single(1) }
-              ).map((d) => [d]),
-            }
-          ).map((d) => [d]),
-        }
-      );
-      const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn().mockReturnValue(0);
-      const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-      // Act
-      const g = arb.generate(fakeRandom).value_;
-
-      // Assert
-      expect(nextInt).toHaveBeenCalledTimes(2); // one for toto, one for titi
-      expect(g).toEqual([[0]]);
-    });
-    it('Should ask ranges containing negative values as we go deeper in the structure if depthFactor', () => {
-      // Arrange
-      const arb: Arbitrary<any> = frequency(
-        { depthFactor: 0.1 },
-        { weight: 1, arbitrary: stubArb.single(0) },
-        { weight: 1000, arbitrary: new LazyArb(() => arb).map((d) => [d]) }
-      );
-      const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn().mockImplementation((a, b) => {
-        // we stop on the first negative value of a
-        if (a < 0) return a;
-        // otherwise we return b to go deeper in the tree
-        else return b;
-      });
-      const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-      // Act
-      arb.generate(fakeRandom).value_;
-
-      // Assert
-      expect(nextInt).toHaveBeenCalledWith(0, 1000); // first calls: 0-1000 contains 1001 values
-      expect(nextInt).toHaveBeenCalledWith(-1, 1000); // as we go deeper (too deep)
-    });
-    it('Should never ask ranges containing negative values as we go deeper in the structure if depthFactor with weight of 0 on first arbitrary', () => {
-      // Arrange
-      const arb: Arbitrary<any> = frequency(
-        { depthFactor: 0.1 },
-        { weight: 0, arbitrary: stubArb.single(0) },
-        { weight: 1000, arbitrary: new LazyArb(() => arb).map((d) => [d]) }
-      );
-      const nextInt: jest.Mock<number, [] | [number] | [number, number]> = jest.fn().mockImplementation((a, b) => {
-        // we stop on the first negative value of a
-        if (a < 0) return a;
-        // otherwise we return b to go deeper in the tree
-        else return b;
-      });
-      const fakeRandom = { nextInt: nextInt as Random['nextInt'] } as Random;
-
-      // Act
-      try {
-        arb.generate(fakeRandom).value_;
-        fail('Stack overflow expected as the structure is going deeper and deeper without any end case');
-      } catch (err) {
-        // noop
-      }
-
-      // Assert
-      expect(nextInt).toHaveBeenCalledWith(0, 999); // first calls: 0-999 contains 1000 values
-      expect(nextInt).not.toHaveBeenCalledWith(-1, 999); // never called as first arbitrary has a weight of zero
-    });
-
-    it('Should reject calls without any weighted arbitraries', () => {
-      expect(() => frequency()).toThrowError();
-    });
-    it('Should reject calls without weight', () => {
-      expect(() => frequency({ arbitrary: stubArb.single(0), weight: undefined! })).toThrowError(
-        /expects weights to be integer values/
-      );
-    });
-    it('Should reject calls without arbitrary', () => {
-      expect(() => frequency({ arbitrary: undefined!, weight: 1 })).toThrowError(/expects arbitraries to be specified/);
-    });
-    it('Should reject calls including at least one strictly negative weight', () =>
-      fc.assert(
-        fc.property(
-          fc.integer({ max: -1 }),
-          fc.array(fc.nat()),
-          fc.array(fc.nat()),
-          (negativeWeight, headingWeights, traillingWeights) => {
-            expect(() =>
-              frequency(
-                ...[...headingWeights, negativeWeight, ...traillingWeights].map((weight) => ({
-                  weight,
-                  arbitrary: stubArb.single(0),
-                }))
-              )
-            ).toThrowError();
-          }
-        )
-      ));
-    it('Should reject calls having a total weight of zero', () =>
-      fc.assert(
-        fc.property(fc.nat({ max: 1000 }), (numEntries) => {
-          // Combined with: 'Should reject calls including at one strictly negative weight'
-          // it means that we have: 'Should reject calls having a total weight inferior or equal to zero'
-          expect(() =>
-            frequency(
-              ...[...Array(numEntries)].map(() => ({
-                weight: 0,
-                arbitrary: stubArb.single(0),
-              }))
-            )
-          ).toThrowError();
-        })
-      ));
-    it('Should not reject calls defining a strictly positive total weight without any negative weights', () =>
-      fc.assert(
-        fc.property(
-          fc.integer({ min: 1 }),
-          fc.array(fc.nat()),
-          fc.array(fc.nat()),
-          (positiveWeight, headingWeights, traillingWeights) => {
-            expect(() =>
-              frequency(
-                ...[...headingWeights, positiveWeight, ...traillingWeights].map((weight) => ({
-                  weight,
-                  arbitrary: stubArb.single(0),
-                }))
-              )
-            ).not.toThrowError();
-          }
-        )
-      ));
-  });
-});
diff --git a/test/unit/arbitrary/OneOfArbitrary.itest.spec.ts b/test/unit/arbitrary/OneOfArbitrary.itest.spec.ts
deleted file mode 100644
index 4729f509..00000000
--- a/test/unit/arbitrary/OneOfArbitrary.itest.spec.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as fc from '../../../lib/fast-check';
-
-import { Arbitrary } from '../../../src/check/arbitrary/definition/Arbitrary';
-import { Shrinkable } from '../../../src/check/arbitrary/definition/Shrinkable';
-import { constant } from '../../../src/check/arbitrary/ConstantArbitrary';
-import { integer } from '../../../src/arbitrary/integer';
-import { oneof } from '../../../src/arbitrary/oneof';
-import { Random } from '../../../src/random/generator/Random';
-import { stream } from '../../../src/stream/Stream';
-
-import * as genericHelper from '../check//arbitrary/generic/GenericArbitraryHelper';
-
-import * as stubRng from '../stubs/generators';
-
-class CustomArbitrary extends Arbitrary<number> {
-  constructor(readonly value: number) {
-    super();
-  }
-  generate(_mrng: Random): Shrinkable<number> {
-    function* g(v: number) {
-      yield new Shrinkable(v - 42);
-    }
-    return new Shrinkable(this.value, () => stream(g(this.value)));
-  }
-}
-
-describe('OneOfArbitrary', () => {
-  describe('oneof', () => {
-    it('Should generate based on one of the given arbitraries', () =>
-      fc.assert(
-        fc.property(fc.integer(), fc.array(fc.integer(), { minLength: 1 }), (seed, choices) => {
-          const mrng = stubRng.mutable.fastincrease(seed);
-          const g = oneof(...choices.map(constant)).generate(mrng).value;
-          return choices.indexOf(g) !== -1;
-        })
-      ));
-    it('Should call the right shrink on shrink', () =>
-      fc.assert(
-        fc.property(fc.integer(), fc.array(fc.integer(), { minLength: 1 }), (seed, choices) => {
-          const mrng = stubRng.mutable.fastincrease(seed);
-          const shrinkable = oneof(...choices.map((c) => new CustomArbitrary(c))).generate(mrng);
-          const shrinks = [...shrinkable.shrink()];
-          return shrinks.length === 1 && shrinks[0].value === shrinkable.value - 42;
-        })
-      ));
-    it('Should reject calls without any arbitraries', () => {
-      expect(() => oneof()).toThrowError();
-    });
-
-    genericHelper.isValidArbitrary(
-      (metas: { type: string; value: number }[]) => {
-        const arbs = metas.map((m) => (m.type === 'unique' ? constant(m.value) : integer(m.value - 10, m.value)));
-        return oneof(...arbs);
-      },
-      {
-        seedGenerator: fc.array(fc.record({ type: fc.constantFrom('unique', 'range'), value: fc.nat() }), {
-          minLength: 1,
-        }),
-        isValidValue: (v: number, metas: { type: string; value: number }[]) =>
-          metas.findIndex((m) => (m.type === 'unique' ? m.value === v : m.value - 10 <= v && v <= m.value)) !== -1,
-        isStrictlySmallerValue: (a: number, b: number) => (Math.abs(b - a) <= 10 && b > 0 ? b - a > 0 : b - a < 0),
-      }
-    );
-  });
-});
diff --git a/test/unit/arbitrary/OneOfArbitrary.utest.spec.ts b/test/unit/arbitrary/OneOfArbitrary.utest.spec.ts
deleted file mode 100644
index 27eee63b..00000000
--- a/test/unit/arbitrary/OneOfArbitrary.utest.spec.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { mocked } from 'ts-jest/utils';
-
-import { constant } from '../../../src/check/arbitrary/ConstantArbitrary';
-import { oneof, OneOfConstraints } from '../../../src/arbitrary/oneof';
-
-import * as FrequencyArbitraryMock from '../../../src/arbitrary/_internals/FrequencyArbitrary';
-jest.mock('../../../src/arbitrary/_internals/FrequencyArbitrary');
-
-describe('OneOfArbitrary', () => {
-  describe('oneof', () => {
-    it('Should call frequency with empty constraints if no constraints have been passed', () => {
-      // Arrange
-      const from = mocked(FrequencyArbitraryMock.FrequencyArbitrary.from);
-      const arb1 = constant(1);
-      const arb2 = constant(2);
-
-      // Act
-      oneof(arb1, arb2);
-
-      // Assert
-      expect(from).toHaveBeenCalledWith(
-        [
-          { arbitrary: arb1, weight: 1 },
-          { arbitrary: arb2, weight: 1 },
-        ],
-        {}, // empty constraints
-        'fc.oneof'
-      );
-    });
-
-    it('Should pass received constraints to frequency', () => {
-      // Arrange
-      const from = mocked(FrequencyArbitraryMock.FrequencyArbitrary.from);
-      const constraints: OneOfConstraints = { maxDepth: 10, depthIdentifier: 'hello' };
-      const arb1 = constant(1);
-      const arb2 = constant(2);
-
-      // Act
-      oneof(constraints, arb1, arb2);
-
-      // Assert
-      expect(from).toHaveBeenCalledWith(
-        [
-          { arbitrary: arb1, weight: 1 },
-          { arbitrary: arb2, weight: 1 },
-        ],
-        constraints,
-        'fc.oneof'
-      );
-    });
-
-    it('Should pass received constraints to frequency even if empty', () => {
-      // Arrange
-      const from = mocked(FrequencyArbitraryMock.FrequencyArbitrary.from);
-      const constraints: OneOfConstraints = {};
-      const arb1 = constant(1);
-      const arb2 = constant(2);
-
-      // Act
-      oneof(constraints, arb1, arb2);
-
-      // Assert
-      expect(from).toHaveBeenCalledWith(
-        [
-          { arbitrary: arb1, weight: 1 },
-          { arbitrary: arb2, weight: 1 },
-        ],
-        constraints,
-        'fc.oneof'
-      );
-    });
-  });
-});
diff --git a/test/unit/arbitrary/OptionArbitrary.spec.ts b/test/unit/arbitrary/OptionArbitrary.spec.ts
deleted file mode 100644
index e49d501a..00000000
--- a/test/unit/arbitrary/OptionArbitrary.spec.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import * as prand from 'pure-rand';
-import * as fc from '../../../lib/fast-check';
-
-import { option } from '../../../src/arbitrary/option';
-import { Random } from '../../../src/random/generator/Random';
-
-import * as stubArb from '../stubs/arbitraries';
-import * as stubRng from '../stubs/generators';
-
-describe('OptionArbitrary', () => {
-  describe('option', () => {
-    it('Should produce null option on default freq value', () =>
-      fc.assert(
-        fc.property(fc.integer(), (seed) => {
-          const MAX_GUESSES = 1000;
-          const mrng = new Random(prand.xorshift128plus(seed));
-          const arb = option(stubArb.forward());
-          for (let idx = 0; idx != MAX_GUESSES; ++idx) {
-            if (arb.generate(mrng).value == null) {
-              return true;
-            }
-          }
-          return false;
-        })
-      ));
-    it('Should shrink towards null', () =>
-      fc.assert(
-        fc.property(fc.integer(), fc.integer(), (seed, start) => {
-          const mrng = stubRng.mutable.fastincrease(seed);
-          let shrinkable = option(stubArb.withShrink(start)).generate(mrng);
-          while (shrinkable.shrink().has((_) => true)[0]) {
-            shrinkable = shrinkable.shrink().next().value;
-          } // only check one shrink path
-          return shrinkable.value === null;
-        })
-      ));
-    it('Should shrink towards the custom nil value if any', () =>
-      fc.assert(
-        fc.property(fc.integer(), fc.integer(), (seed, start) => {
-          const nil = Symbol();
-          const mrng = stubRng.mutable.fastincrease(seed);
-          let shrinkable = option(stubArb.withShrink(start), { nil }).generate(mrng);
-          while (shrinkable.shrink().has((_) => true)[0]) {
-            shrinkable = shrinkable.shrink().next().value;
-          } // only check one shrink path
-          return shrinkable.value === nil;
-        })
-      ));
-  });
-});
diff --git a/test/unit/arbitrary/_internals/FrequencyArbitrary.spec.ts b/test/unit/arbitrary/_internals/FrequencyArbitrary.spec.ts
new file mode 100644
index 00000000..9952daf4
--- /dev/null
+++ b/test/unit/arbitrary/_internals/FrequencyArbitrary.spec.ts
@@ -0,0 +1,731 @@
+import * as fc from '../../../../lib/fast-check';
+import { FrequencyArbitrary, _Constraints } from '../../../../src/arbitrary/_internals/FrequencyArbitrary';
+import { NextValue } from '../../../../src/check/arbitrary/definition/NextValue';
+import { fakeRandom } from '../../check/arbitrary/generic/RandomHelpers';
+import { FakeIntegerArbitrary, fakeNextArbitrary } from '../../check/arbitrary/generic/NextArbitraryHelpers';
+import {
+  assertGenerateProducesCorrectValues,
+  assertGenerateProducesSameValueGivenSameSeed,
+  assertGenerateProducesValuesFlaggedAsCanGenerate,
+  assertShrinkProducesCorrectValues,
+  assertShrinkProducesStrictlySmallerValue,
+  assertShrinkProducesValuesFlaggedAsCanGenerate,
+} from '../../check/arbitrary/generic/NextArbitraryAssertions';
+import * as DepthContextMock from '../../../../src/arbitrary/_internals/helpers/DepthContext';
+import { Stream } from '../../../../src/stream/Stream';
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+const frequencyValidInputsArb = fc
+  .tuple(
+    fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() }),
+    fc.array(fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() })),
+    fc.array(fc.record({ weight: fc.integer({ min: 1 }), arbitraryValue: fc.integer() }))
+  )
+  .map(([positiveWeightMeta, headingWeightsMeta, traillingWeightsMeta]) => [
+    ...headingWeightsMeta,
+    positiveWeightMeta,
+    ...traillingWeightsMeta,
+  ]);
+
+const fromValidInputs = (metas: { weight: number; arbitraryValue: number }[]) =>
+  metas.map((meta) => {
+    const expectedContext = Symbol();
+    const arbitraryMeta = fakeNextArbitrary<number>();
+    arbitraryMeta.generate.mockReturnValue(new NextValue(meta.arbitraryValue, expectedContext));
+    return {
+      weight: meta.weight,
+      arbitraryMeta,
+      arbitrary: arbitraryMeta.instance,
+      expectedValue: meta.arbitraryValue,
+      expectedContext,
+    };
+  });
+
+const frequencyConstraintsArbFor = (keys: {
+  forbidden?: (keyof _Constraints)[];
+  required?: (keyof _Constraints)[];
+}): fc.Arbitrary<_Constraints> => {
+  const { forbidden = [], required = [] } = keys;
+  return fc.record(
+    {
+      ...(!forbidden.includes('depthIdentifier') ? { depthIdentifier: fc.string() } : {}),
+      ...(!forbidden.includes('depthFactor')
+        ? { depthFactor: fc.double({ min: 0.01, noDefaultInfinity: true, noNaN: true }) }
+        : {}),
+      ...(!forbidden.includes('maxDepth') ? { maxDepth: fc.nat() } : {}),
+      ...(!forbidden.includes('withCrossShrink') ? { withCrossShrink: fc.boolean() } : {}),
+    },
+    { requiredKeys: required }
+  );
+};
+
+describe('FrequencyArbitrary', () => {
+  describe('from', () => {
+    it('should build instances of FrequencyArbitrary', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.nat(),
+          (validInputs, constraints, depth) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            const depthContext = { depth };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+
+            // Assert
+            expect(arb).toBeInstanceOf(FrequencyArbitrary);
+          }
+        )
+      ));
+
+    it('should always use the context coming from getDepthContextFor', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.nat(),
+          (validInputs, constraints, depth) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            const depthContext = { depth };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            const typedArb = arb as FrequencyArbitrary<number>;
+
+            // Assert
+            expect(getDepthContextFor).toHaveBeenCalledTimes(1);
+            expect(getDepthContextFor).toHaveBeenCalledWith(constraints.depthIdentifier);
+            expect(typedArb.context).toBe(depthContext);
+          }
+        )
+      ));
+
+    it('should reject calls without any weighted arbitraries', () => {
+      // Arrange / Act / Assert
+      expect(() => FrequencyArbitrary.from([], {}, 'test')).toThrowError();
+    });
+
+    it('should reject calls without weight', () => {
+      // Arrange / Act / Assert
+      expect(() =>
+        FrequencyArbitrary.from([{ arbitrary: fakeNextArbitrary(), weight: undefined! }], {}, 'test')
+      ).toThrowError(/expects weights to be integer values/);
+    });
+
+    it('should reject calls without arbitrary', () => {
+      // Arrange / Act / Assert
+      expect(() => FrequencyArbitrary.from([{ arbitrary: undefined!, weight: 1 }], {}, 'test')).toThrowError(
+        /expects arbitraries to be specified/
+      );
+    });
+
+    it('should reject calls including at least one strictly negative weight', () =>
+      fc.assert(
+        fc.property(
+          fc.integer({ max: -1 }),
+          fc.array(fc.nat()),
+          fc.array(fc.nat()),
+          (negativeWeight, headingWeights, traillingWeights) => {
+            // Arrange
+            const weightedArbs = [...headingWeights, negativeWeight, ...traillingWeights].map((weight) => ({
+              weight,
+              arbitrary: fakeNextArbitrary(),
+            }));
+
+            // Act / Assert
+            expect(() => FrequencyArbitrary.from(weightedArbs, {}, 'test')).toThrowError();
+          }
+        )
+      ));
+
+    it('should reject calls having a total weight of zero', () =>
+      fc.assert(
+        fc.property(fc.nat({ max: 1000 }), (numEntries) => {
+          // Arrange
+          const weightedArbs = [...Array(numEntries)].map(() => ({
+            weight: 0,
+            arbitrary: fakeNextArbitrary(),
+          }));
+
+          // Act / Assert
+          // Combined with: 'Should reject calls including at one strictly negative weight'
+          // it means that we have: 'Should reject calls having a total weight inferior or equal to zero'
+          expect(() => FrequencyArbitrary.from(weightedArbs, {}, 'test')).toThrowError();
+        })
+      ));
+
+    it('should not reject calls defining a strictly positive total weight without any negative weights', () =>
+      fc.assert(
+        fc.property(
+          fc.integer({ min: 1 }),
+          fc.array(fc.nat()),
+          fc.array(fc.nat()),
+          (positiveWeight, headingWeights, traillingWeights) => {
+            // Arrange
+            const weightedArbs = [...headingWeights, positiveWeight, ...traillingWeights].map((weight) => ({
+              weight,
+              arbitrary: fakeNextArbitrary(),
+            }));
+
+            // Act / Assert
+            expect(() => FrequencyArbitrary.from(weightedArbs, {}, 'test')).not.toThrowError();
+          }
+        )
+      ));
+  });
+
+  describe('generate', () => {
+    it('should call Random generator to generate values between 0 and total weight (not included)', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, generateSeed) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const totalWeight = warbs.reduce((acc, cur) => acc + cur.weight, 0);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockImplementation((a = 0, b = 0) => a + (generateSeed % (b - a + 1)));
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            arb.generate(mrng, biasFactor);
+
+            // Assert
+            expect(nextInt).toHaveBeenCalledTimes(1);
+            expect(nextInt).toHaveBeenCalledWith(0, totalWeight - 1);
+          }
+        )
+      ));
+
+    it('should call the right arbitrary to generate the value', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, arbitrarySelectionSeed, generateSeed) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const selectedArbitraryIndex = arbitrarySelectionSeed % warbs.length;
+            const selectedArbitrary = warbs[selectedArbitraryIndex];
+            fc.pre(selectedArbitrary.weight > 0);
+
+            const totalWeightBefore = warbs.slice(0, selectedArbitraryIndex).reduce((acc, cur) => acc + cur.weight, 0);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockImplementation(() => totalWeightBefore + (generateSeed % selectedArbitrary.weight));
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            const g = arb.generate(mrng, biasFactor).value;
+
+            // Assert
+            expect(g).toBe(selectedArbitrary.expectedValue);
+            expect(selectedArbitrary.arbitraryMeta.generate).toHaveBeenCalledTimes(1);
+            expect(selectedArbitrary.arbitraryMeta.generate).toHaveBeenCalledWith(mrng, biasFactor);
+          }
+        )
+      ));
+
+    it('should always call the first arbitrary to generate the value when maxDepth has been reached', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ required: ['maxDepth'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, overflowFromMaxDepth) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            const requestedMaxDepth = constraints.maxDepth!;
+            const { instance: mrng, nextInt } = fakeRandom();
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue({ depth: requestedMaxDepth + overflowFromMaxDepth });
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            const g = arb.generate(mrng, biasFactor).value;
+
+            // Assert
+            expect(nextInt).not.toHaveBeenCalled();
+            expect(g).toBe(warbs[0].expectedValue);
+          }
+        )
+      ));
+
+    it('should increment received depth context when going deeper in the generate-tree then reset it', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, initialDepth, generateSeed) => {
+            // Arrange
+            let calledOnce = false;
+            const warbs = fromValidInputs(validInputs);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockImplementation((a = 0, b = 0) => a + (generateSeed % (b - a + 1)));
+            const depthContext = { depth: initialDepth };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+            for (const { arbitraryMeta, expectedValue } of warbs) {
+              arbitraryMeta.generate.mockReset();
+              arbitraryMeta.generate.mockImplementation(() => {
+                calledOnce = true;
+                expect(depthContext).toEqual({ depth: initialDepth + 1 });
+                return new NextValue(expectedValue);
+              });
+            }
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            arb.generate(mrng, biasFactor);
+
+            // Assert
+            expect(calledOnce).toBe(true);
+            expect(depthContext).toEqual({ depth: initialDepth });
+          }
+        )
+      ));
+
+    it('should ask ranges containing negative values as we go deeper in the structure if depthFactor and first arbitrary has weight >0', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ forbidden: ['maxDepth'], required: ['depthFactor'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          (validInputs, constraints, biasFactor) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            fc.pre(warbs[0].weight > 0);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockReturnValue(0);
+            const depthContext = { depth: 0 };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValueOnce(depthContext);
+
+            // Act / Assert
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            let currentDepth = 0;
+            // eslint-disable-next-line no-constant-condition
+            while (true) {
+              depthContext.depth = currentDepth;
+              arb.generate(mrng, biasFactor);
+              expect(depthContext.depth).toBe(currentDepth); // ensures the depth was properly reset
+              if (nextInt.mock.calls[nextInt.mock.calls.length - 1][0] < 0) {
+                break; // we have been called with a negative value once
+              }
+              ++currentDepth;
+            }
+            // first run (depth=0) will always call us with 0->?
+            // subsequent calls (depth>0) may call us with negative boundaries
+            expect(nextInt).toHaveBeenCalledWith(0, expect.any(Number));
+            // but all the runs will call us with the same max
+            const distinctMax = new Set(nextInt.mock.calls.map(([_min, max]) => max));
+            expect([...distinctMax]).toHaveLength(1);
+            const distinctMin = new Set(nextInt.mock.calls.map(([min, _max]) => min));
+            expect([...distinctMin]).toHaveLength(2);
+          }
+        )
+      ));
+
+    it('should never ask ranges containing negative values as we go deeper in the structure if first arbitrary has weight of zero', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ forbidden: ['maxDepth'], required: ['depthFactor'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          (validInputs, constraints, biasFactor) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockReturnValue(0);
+            const depthContext = { depth: 0 };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValueOnce(depthContext);
+
+            // Act / Assert
+            const arb = FrequencyArbitrary.from([{ ...warbs[0], weight: 0 }, ...warbs], constraints, 'test');
+            for (let currentDepth = 0; currentDepth !== 100; ++currentDepth) {
+              depthContext.depth = currentDepth;
+              arb.generate(mrng, biasFactor);
+              expect(depthContext.depth).toBe(currentDepth); // ensures the depth was properly reset
+            }
+            const distinctMin = new Set(nextInt.mock.calls.map(([min, _max]) => min));
+            expect([...distinctMin]).toHaveLength(1);
+            const distinctMax = new Set(nextInt.mock.calls.map(([_min, max]) => max));
+            expect([...distinctMax]).toHaveLength(1);
+          }
+        )
+      ));
+  });
+
+  describe('canGenerate', () => {
+    it('should tell it cannot generate the value if no sub-arbitrary can generate the value', () =>
+      fc.assert(
+        fc.property(frequencyValidInputsArb, frequencyConstraintsArbFor({}), (validInputs, constraints) => {
+          // Arrange
+          const warbs = fromValidInputs(validInputs);
+          const depthContext = { depth: 0 };
+          const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+          getDepthContextFor.mockReturnValue(depthContext);
+          const value = Symbol();
+          for (const input of warbs) {
+            input.arbitraryMeta.canGenerate.mockReturnValue(false);
+          }
+
+          // Act
+          const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+
+          // Assert
+          expect(arb.canGenerate(value)).toBe(false);
+        })
+      ));
+
+    it('should ignore arbitraries with weight of zero when maxDepth not reached', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.nat(),
+          (validInputs, constraints, position) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs([
+              ...validInputs.slice(0, position % validInputs.length),
+              { arbitraryValue: -1, weight: 0 },
+              ...validInputs.slice(position % validInputs.length),
+            ]);
+            const depthContext = { depth: 0 };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+            const value = Symbol();
+            for (const input of warbs) {
+              input.arbitraryMeta.canGenerate.mockReturnValue(false);
+            }
+            warbs[position % validInputs.length].arbitraryMeta.canGenerate.mockReturnValue(true);
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+
+            // Assert
+            expect(arb.canGenerate(value)).toBe(false);
+          }
+        )
+      ));
+
+    it('should tell it can generate the value if one of the sub-arbitraries can generate the value (maxDepth not reached)', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.nat(),
+          (validInputs, constraints, mod) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const depthContext = { depth: 0 };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+            const value = Symbol();
+            const selectedIndex = mod % warbs.length;
+            for (let index = 0; index !== warbs.length; ++index) {
+              const input = warbs[index];
+              const can = index === selectedIndex;
+              input.arbitraryMeta.canGenerate.mockReturnValue(can);
+            }
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+
+            // Assert
+            expect(arb.canGenerate(value)).toBe(true);
+            expect(warbs[selectedIndex].arbitraryMeta.canGenerate).toHaveBeenCalledWith(value);
+          }
+        )
+      ));
+
+    it('should only consider the first arbitrary when maxDepth has been reached', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ required: ['maxDepth'] }),
+          fc.nat(),
+          fc.boolean(),
+          fc.boolean(),
+          (validInputs, constraints, overflowFromMaxDepth, firstCanOrNot, allOthersCanOrNot) => {
+            // Arrange
+            const warbs = fromValidInputs(validInputs);
+            const maxDepth = constraints.maxDepth!;
+            const depthContext = { depth: maxDepth + overflowFromMaxDepth };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+            const value = Symbol();
+            for (let index = 0; index !== warbs.length; ++index) {
+              warbs[index].arbitraryMeta.canGenerate.mockReturnValue(index === 0 ? firstCanOrNot : allOthersCanOrNot);
+            }
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+
+            // Assert
+            expect(arb.canGenerate(value)).toBe(firstCanOrNot);
+            expect(warbs[0].arbitraryMeta.canGenerate).toHaveBeenCalledWith(value);
+            for (let index = 1; index < warbs.length; ++index) {
+              expect(warbs[index].arbitraryMeta.canGenerate).not.toHaveBeenCalled();
+            }
+          }
+        )
+      ));
+  });
+
+  describe('shrink', () => {
+    it('should call the right arbitrary to shrink a value generated by itself', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ forbidden: ['withCrossShrink'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, arbitrarySelectionSeed, generateSeed) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const selectedArbitraryIndex = arbitrarySelectionSeed % warbs.length;
+            const selectedArbitrary = warbs[selectedArbitraryIndex];
+            fc.pre(selectedArbitrary.weight > 0);
+
+            const totalWeightBefore = warbs.slice(0, selectedArbitraryIndex).reduce((acc, cur) => acc + cur.weight, 0);
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockImplementation(() => totalWeightBefore + (generateSeed % selectedArbitrary.weight));
+            selectedArbitrary.arbitraryMeta.shrink.mockReturnValue(Stream.of(new NextValue(1), new NextValue(42)));
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            const value = arb.generate(mrng, biasFactor);
+            const shrinks = [...arb.shrink(value.value, value.context)];
+
+            // Assert
+            expect(shrinks.map((v) => v.value)).toEqual([1, 42]);
+            expect(selectedArbitrary.arbitraryMeta.shrink).toHaveBeenCalledTimes(1);
+            expect(selectedArbitrary.arbitraryMeta.shrink).toHaveBeenCalledWith(
+              value.value,
+              selectedArbitrary.expectedContext
+            );
+          }
+        )
+      ));
+
+    it('should generate a new value using first arbitrary when cross-shrink enabled', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ forbidden: ['withCrossShrink'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          fc.nat(),
+          fc.nat(),
+          (validInputs, constraints, biasFactor, arbitrarySelectionSeed, generateSeed) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const selectedArbitraryIndex = arbitrarySelectionSeed % warbs.length;
+            const selectedArbitrary = warbs[selectedArbitraryIndex];
+            fc.pre(selectedArbitrary.weight > 0);
+            fc.pre(warbs[0].weight > 0);
+            fc.pre(selectedArbitraryIndex !== 0);
+
+            const totalWeightBefore = warbs.slice(0, selectedArbitraryIndex).reduce((acc, cur) => acc + cur.weight, 0);
+            const { instance: mrng, nextInt, clone } = fakeRandom();
+            const { instance: anotherMrng } = fakeRandom();
+            clone.mockReturnValue(anotherMrng);
+            nextInt.mockImplementation(() => totalWeightBefore + (generateSeed % selectedArbitrary.weight));
+            selectedArbitrary.arbitraryMeta.shrink.mockReturnValue(Stream.of(new NextValue(1), new NextValue(42)));
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, { ...constraints, withCrossShrink: true }, 'test');
+            const value = arb.generate(mrng, biasFactor);
+            const shrinks = [...arb.shrink(value.value, value.context)];
+
+            // Assert
+            expect(shrinks.map((v) => v.value)).toEqual([warbs[0].expectedValue, 1, 42]);
+            expect(warbs[0].arbitraryMeta.generate).toHaveBeenCalledWith(anotherMrng, biasFactor);
+            expect(selectedArbitrary.arbitraryMeta.shrink).toHaveBeenCalledTimes(1);
+            expect(selectedArbitrary.arbitraryMeta.shrink).toHaveBeenCalledWith(
+              value.value,
+              selectedArbitrary.expectedContext
+            );
+          }
+        )
+      ));
+
+    it('should not call generate on first arbitrary when cross-shrink enabled and first generate already used it', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({ forbidden: ['withCrossShrink'] }),
+          fc.option(fc.integer({ min: 2 }), { nil: undefined }),
+          (validInputs, constraints, biasFactor) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            fc.pre(warbs[0].weight > 0);
+
+            const { instance: mrng, nextInt } = fakeRandom();
+            nextInt.mockReturnValue(0);
+            warbs[0].arbitraryMeta.shrink.mockReturnValue(Stream.of(new NextValue(1), new NextValue(42)));
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, { ...constraints, withCrossShrink: true }, 'test');
+            const value = arb.generate(mrng, biasFactor);
+            const shrinks = [...arb.shrink(value.value, value.context)];
+
+            // Assert
+            expect(shrinks.map((v) => v.value)).toEqual([1, 42]);
+            expect(warbs[0].arbitraryMeta.shrink).toHaveBeenCalledTimes(1);
+            expect(warbs[0].arbitraryMeta.shrink).toHaveBeenCalledWith(value.value, warbs[0].expectedContext);
+          }
+        )
+      ));
+
+    it('should be able to shrink without context if one of the sub-arbitrary can generate the value', () =>
+      fc.assert(
+        fc.property(
+          frequencyValidInputsArb,
+          frequencyConstraintsArbFor({}),
+          fc.nat(),
+          (validInputs, constraints, mod) => {
+            // Arrange
+            fc.pre(constraints.maxDepth !== 0);
+            const warbs = fromValidInputs(validInputs);
+            const depthContext = { depth: 0 };
+            const getDepthContextFor = jest.spyOn(DepthContextMock, 'getDepthContextFor');
+            getDepthContextFor.mockReturnValue(depthContext);
+            const value = Symbol();
+            const selectedIndex = mod % warbs.length;
+            fc.pre(warbs[selectedIndex].weight !== 0);
+            for (let index = 0; index !== warbs.length; ++index) {
+              const input = warbs[index];
+              const can = index === selectedIndex;
+              input.arbitraryMeta.canGenerate.mockReturnValue(can);
+              input.arbitraryMeta.shrink.mockReturnValue(Stream.of(new NextValue(42), new NextValue(index)));
+            }
+
+            // Act
+            const arb = FrequencyArbitrary.from(warbs, constraints, 'test');
+            const shrinks = [...arb.shrink(value as any)];
+
+            // Assert
+            expect(shrinks.map((v) => v.value)).toEqual([42, selectedIndex]);
+            expect(warbs[selectedIndex].arbitraryMeta.canGenerate).toHaveBeenCalledWith(value);
+            expect(warbs[selectedIndex].arbitraryMeta.shrink).toHaveBeenCalledWith(value);
+          }
+        )
+      ));
+  });
+});
+
+describe('FrequencyArbitrary (integration)', () => {
+  type Extra = { data: { offset: number; weight: number }[]; constraints: Partial<_Constraints> };
+  const maxRangeLength = 10;
+  const extraParameters: fc.Arbitrary<Extra> = fc.record(
+    {
+      data: fc
+        .array(
+          fc.record(
+            {
+              offset: fc.nat(),
+              weight: fc.nat(),
+            },
+            { requiredKeys: ['offset', 'weight'] }
+          ),
+          { minLength: 1 }
+        )
+        .filter((inputs) => inputs.reduce((summedWeight, current) => summedWeight + current.weight, 0) > 0),
+      constraints: fc.record(
+        {
+          withCrossShrink: fc.boolean(),
+          depthFactor: fc.double({ min: 0, max: Number.MAX_VALUE, noNaN: true }),
+          maxDepth: fc.nat(),
+        },
+        { requiredKeys: [] }
+      ),
+    },
+    { requiredKeys: ['data', 'constraints'] }
+  );
+
+  const isCorrect = (value: number, extra: Extra) =>
+    typeof value === 'number' && extra.data.some((m) => m.offset <= value && value <= m.offset + maxRangeLength);
+
+  const isStrictlySmaller = (v1: number, v2: number, extra: Extra) => {
+    // When withCrossShrink is toggled, the shrinker can jump from one arbitrary to the first one on shrink
+    // But only if the weight associated to the first arbitrary is strictly greater than 0
+    if (extra.constraints.withCrossShrink && extra.data[0].weight > 0) {
+      const canBeInFirstArbitrary = extra.data[0].offset <= v1 && v1 <= extra.data[0].offset + maxRangeLength;
+      if (canBeInFirstArbitrary) {
+        // `v1` is possibly coming from our first arbitrary
+        return true;
+      }
+    }
+    return Math.abs(v1) < Math.abs(v2);
+  };
+
+  const frequencyBuilder = (extra: Extra) =>
+    FrequencyArbitrary.from(
+      extra.data.map((m) => ({ weight: m.weight, arbitrary: new FakeIntegerArbitrary(m.offset, maxRangeLength) })),
+      extra.constraints,
+      'test'
+    );
+
+  it('should generate the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(frequencyBuilder, { extraParameters });
+  });
+
+  it('should only generate correct values', () => {
+    assertGenerateProducesCorrectValues(frequencyBuilder, isCorrect, { extraParameters });
+  });
+
+  it('should recognize values that would have been generated using it during generate', () => {
+    assertGenerateProducesValuesFlaggedAsCanGenerate(frequencyBuilder, { extraParameters });
+  });
+
+  it('should shrink towards the same values given the same seed', () => {
+    assertGenerateProducesSameValueGivenSameSeed(frequencyBuilder, { extraParameters });
+  });
+
+  it('should only shrink towards correct values', () => {
+    assertShrinkProducesCorrectValues(frequencyBuilder, isCorrect, { extraParameters });
+  });
+
+  it('should recognize values that would have been generated using it during shrink', () => {
+    assertShrinkProducesValuesFlaggedAsCanGenerate(frequencyBuilder, { extraParameters });
+  });
+
+  it('should shrink towards strictly smaller values (underlyings do)', () => {
+    assertShrinkProducesStrictlySmallerValue(frequencyBuilder, isStrictlySmaller, { extraParameters });
+  });
+});
diff --git a/test/unit/arbitrary/frequency.spec.ts b/test/unit/arbitrary/frequency.spec.ts
new file mode 100644
index 00000000..1a43b800
--- /dev/null
+++ b/test/unit/arbitrary/frequency.spec.ts
@@ -0,0 +1,86 @@
+import * as fc from '../../../lib/fast-check';
+import { frequency, FrequencyContraints } from '../../../src/arbitrary/frequency';
+import { fakeNextArbitrary } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import { convertFromNext } from '../../../src/check/arbitrary/definition/Converters';
+import * as FrequencyArbitraryMock from '../../../src/arbitrary/_internals/FrequencyArbitrary';
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('frequency', () => {
+  it('should call FrequencyArbitrary.from with the right parameters when called with constraints', () =>
+    fc.assert(
+      fc.property(
+        fc.record(
+          {
+            withCrossShrink: fc.boolean(),
+            depthIdentifier: fc.string(),
+            depthFactor: fc.double({ min: 0.01, noDefaultInfinity: true, noNaN: true }),
+            maxDepth: fc.nat(),
+          },
+          { requiredKeys: [] }
+        ),
+        (constraints: Partial<FrequencyContraints>) => {
+          // Arrange
+          const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+          const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+          fromOld.mockReturnValue(expectedArb);
+          const { instance: nextArb1 } = fakeNextArbitrary();
+          const { instance: nextArb2 } = fakeNextArbitrary();
+          const arb1 = convertFromNext(nextArb1);
+          const arb2 = convertFromNext(nextArb2);
+          const weight1 = 10;
+          const weight2 = 3;
+
+          // Act
+          const out = frequency(
+            constraints,
+            { arbitrary: arb1, weight: weight1 },
+            { arbitrary: arb2, weight: weight2 }
+          );
+
+          // Assert
+          expect(fromOld).toHaveBeenCalledWith(
+            [
+              { arbitrary: arb1, weight: weight1 },
+              { arbitrary: arb2, weight: weight2 },
+            ],
+            constraints,
+            'fc.frequency'
+          );
+          expect(out).toBe(expectedArb);
+        }
+      )
+    ));
+
+  it('should call FrequencyArbitrary.from with the right parameters when called without constraints', () => {
+    // Arrange
+    const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+    const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+    fromOld.mockReturnValue(expectedArb);
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+    const weight1 = 10;
+    const weight2 = 3;
+
+    // Act
+    const out = frequency({ arbitrary: arb1, weight: weight1 }, { arbitrary: arb2, weight: weight2 });
+
+    // Assert
+    expect(fromOld).toHaveBeenCalledWith(
+      [
+        { arbitrary: arb1, weight: weight1 },
+        { arbitrary: arb2, weight: weight2 },
+      ],
+      {},
+      'fc.frequency'
+    );
+    expect(out).toBe(expectedArb);
+  });
+});
diff --git a/test/unit/arbitrary/oneof.spec.ts b/test/unit/arbitrary/oneof.spec.ts
new file mode 100644
index 00000000..e46173eb
--- /dev/null
+++ b/test/unit/arbitrary/oneof.spec.ts
@@ -0,0 +1,83 @@
+import { oneof, OneOfConstraints } from '../../../src/arbitrary/oneof';
+import { fakeNextArbitrary } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import { convertFromNext } from '../../../src/check/arbitrary/definition/Converters';
+import * as FrequencyArbitraryMock from '../../../src/arbitrary/_internals/FrequencyArbitrary';
+
+describe('oneof', () => {
+  it('should call FrequencyArbitrary.from with empty constraints if no constraints have been passed', () => {
+    // Arrange
+    const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+    const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+    fromOld.mockReturnValue(expectedArb);
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+
+    // Act
+    const out = oneof(arb1, arb2);
+
+    // Assert
+    expect(fromOld).toHaveBeenCalledWith(
+      [
+        { arbitrary: arb1, weight: 1 },
+        { arbitrary: arb2, weight: 1 },
+      ],
+      {}, // empty constraints
+      'fc.oneof'
+    );
+    expect(out).toBe(expectedArb);
+  });
+
+  it('should pass received constraints to FrequencyArbitrary.from', () => {
+    // Arrange
+    const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+    const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+    fromOld.mockReturnValue(expectedArb);
+    const constraints: OneOfConstraints = { maxDepth: 10, depthIdentifier: 'hello' };
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+
+    // Act
+    const out = oneof(constraints, arb1, arb2);
+
+    // Assert
+    expect(fromOld).toHaveBeenCalledWith(
+      [
+        { arbitrary: arb1, weight: 1 },
+        { arbitrary: arb2, weight: 1 },
+      ],
+      constraints,
+      'fc.oneof'
+    );
+    expect(out).toBe(expectedArb);
+  });
+
+  it('should pass received constraints to FrequencyArbitrary.from even if empty', () => {
+    // Arrange
+    const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+    const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+    fromOld.mockReturnValue(expectedArb);
+    const constraints: OneOfConstraints = {};
+    const { instance: nextArb1 } = fakeNextArbitrary();
+    const { instance: nextArb2 } = fakeNextArbitrary();
+    const arb1 = convertFromNext(nextArb1);
+    const arb2 = convertFromNext(nextArb2);
+
+    // Act
+    const out = oneof(constraints, arb1, arb2);
+
+    // Assert
+    expect(fromOld).toHaveBeenCalledWith(
+      [
+        { arbitrary: arb1, weight: 1 },
+        { arbitrary: arb2, weight: 1 },
+      ],
+      constraints,
+      'fc.oneof'
+    );
+    expect(out).toBe(expectedArb);
+  });
+});
diff --git a/test/unit/arbitrary/option.spec.ts b/test/unit/arbitrary/option.spec.ts
new file mode 100644
index 00000000..38d8c4ce
--- /dev/null
+++ b/test/unit/arbitrary/option.spec.ts
@@ -0,0 +1,128 @@
+import * as fc from '../../../lib/fast-check';
+import { option, OptionConstraints } from '../../../src/arbitrary/option';
+import { fakeNextArbitrary } from '../check/arbitrary/generic/NextArbitraryHelpers';
+import { convertFromNext } from '../../../src/check/arbitrary/definition/Converters';
+import * as FrequencyArbitraryMock from '../../../src/arbitrary/_internals/FrequencyArbitrary';
+import * as ConstantArbitraryMock from '../../../src/check/arbitrary/ConstantArbitrary';
+
+function beforeEachHook() {
+  jest.resetModules();
+  jest.restoreAllMocks();
+  fc.configureGlobal({ beforeEach: beforeEachHook });
+}
+beforeEach(beforeEachHook);
+
+describe('option', () => {
+  it('should call FrequencyArbitrary.from with the right parameters when called with constraints', () =>
+    fc.assert(
+      fc.property(
+        fc.record(
+          {
+            depthIdentifier: fc.string(),
+            depthFactor: fc.double({ min: 0.01, noDefaultInfinity: true, noNaN: true }),
+            maxDepth: fc.nat(),
+            freq: fc.nat(),
+            nil: fc.anything(),
+          },
+          { requiredKeys: [] }
+        ),
+        (constraints: Partial<OptionConstraints<unknown>>) => {
+          // Arrange
+          const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+          const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+          fromOld.mockReturnValue(expectedArb);
+          const expectedConstantArb = convertFromNext(fakeNextArbitrary().instance);
+          const constant = jest.spyOn(ConstantArbitraryMock, 'constant');
+          constant.mockReturnValue(expectedConstantArb);
+          const { instance: nextArb } = fakeNextArbitrary();
+          const arb = convertFromNext(nextArb);
+
+          // Act
+          const out = option(arb, constraints);
+
+          // Assert
+          expect(constant).toHaveBeenCalledWith('nil' in constraints ? constraints.nil : null);
+          expect(fromOld).toHaveBeenCalledWith(
+            [
+              { arbitrary: expectedConstantArb, weight: 1 },
+              { arbitrary: arb, weight: 'freq' in constraints ? constraints.freq! : 5 },
+            ],
+            {
+              withCrossShrink: true,
+              depthFactor: constraints.depthFactor,
+              maxDepth: constraints.maxDepth,
+              depthIdentifier: constraints.depthIdentifier,
+            },
+            'fc.option'
+          );
+          expect(out).toBe(expectedArb);
+        }
+      )
+    ));
+
+  it('should call FrequencyArbitrary.from with the right parameters when called without constraints', () => {
+    // Arrange
+    const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+    const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+    fromOld.mockReturnValue(expectedArb);
+    const expectedConstantArb = convertFromNext(fakeNextArbitrary().instance);
+    const constant = jest.spyOn(ConstantArbitraryMock, 'constant');
+    constant.mockReturnValue(expectedConstantArb);
+    const { instance: nextArb } = fakeNextArbitrary();
+    const arb = convertFromNext(nextArb);
+
+    // Act
+    const out = option(arb);
+
+    // Assert
+    expect(constant).toHaveBeenCalledWith(null);
+    expect(fromOld).toHaveBeenCalledWith(
+      [
+        { arbitrary: expectedConstantArb, weight: 1 },
+        { arbitrary: arb, weight: 5 },
+      ],
+      {
+        withCrossShrink: true,
+        depthFactor: undefined,
+        maxDepth: undefined,
+        depthIdentifier: undefined,
+      },
+      'fc.option'
+    );
+    expect(out).toBe(expectedArb);
+  });
+  it('[legacy] should call FrequencyArbitrary.from with the right parameters when called with only freq', () =>
+    fc.assert(
+      fc.property(fc.nat(), (freq) => {
+        // Arrange
+        const expectedArb = convertFromNext(fakeNextArbitrary().instance);
+        const fromOld = jest.spyOn(FrequencyArbitraryMock.FrequencyArbitrary, 'fromOld');
+        fromOld.mockReturnValue(expectedArb);
+        const expectedConstantArb = convertFromNext(fakeNextArbitrary().instance);
+        const constant = jest.spyOn(ConstantArbitraryMock, 'constant');
+        constant.mockReturnValue(expectedConstantArb);
+        const { instance: nextArb } = fakeNextArbitrary();
+        const arb = convertFromNext(nextArb);
+
+        // Act
+        const out = option(arb, freq);
+
+        // Assert
+        expect(constant).toHaveBeenCalledWith(null);
+        expect(fromOld).toHaveBeenCalledWith(
+          [
+            { arbitrary: expectedConstantArb, weight: 1 },
+            { arbitrary: arb, weight: freq },
+          ],
+          {
+            withCrossShrink: true,
+            depthFactor: undefined,
+            maxDepth: undefined,
+            depthIdentifier: undefined,
+          },
+          'fc.option'
+        );
+        expect(out).toBe(expectedArb);
+      })
+    ));
+});
diff --git a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
index a3842461..660f15e3 100644
--- a/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
+++ b/test/unit/check/arbitrary/generic/NextArbitraryHelpers.ts
@@ -50,24 +50,31 @@ export function fakeNextArbitrary<T = any>(): { instance: NextArbitrary<T> } & M
  * - context less shrink like context full (for first iteration only)
  */
 export class FakeIntegerArbitrary extends NextArbitrary<number> {
+  constructor(readonly offset: number = 0, readonly rangeLength: number = 100) {
+    super();
+  }
   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 });
+    const minRange = Math.floor(this.rangeLength / 10);
+    const maxRange = this.rangeLength;
+    // Worst case: 0-minRange
+    // No bias   : 0-maxRange
+    const maxLimit = biasFactor !== undefined ? Math.max(minRange, maxRange - biasFactor) : maxRange;
+    return new NextValue(mrng.nextInt(0, maxLimit) + this.offset, { step: 2 });
   }
   canGenerate(value: unknown): value is number {
-    return typeof value === 'number' && value >= 0 && value <= 100 && Number.isInteger(value);
+    return (
+      typeof value === 'number' &&
+      value >= this.offset &&
+      value <= this.rangeLength + this.offset &&
+      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 })] : [])
+      ...(value - currentStep >= this.offset ? [new NextValue(value - currentStep, { step: nextStep })] : []),
+      ...(value - currentStep + 1 >= this.offset ? [new NextValue(value - currentStep + 1, { step: nextStep })] : [])
     );
   }
 }