Initial Commit
This commit is contained in:
125
packages/testkit/testing/ab-testing/tests/property.test.ts
Normal file
125
packages/testkit/testing/ab-testing/tests/property.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { describe, it } from "@suaveplan/testing/runner";
|
||||
import * as fc from "fast-check";
|
||||
import {
|
||||
bucketUser,
|
||||
normaliseWeights,
|
||||
selectVariant,
|
||||
} from "../src/bucketing/bucketing.js";
|
||||
import { evaluateTargeting } from "../src/targeting/targeting.js";
|
||||
|
||||
describe("property: bucketUser", () => {
|
||||
it("always returns [0,1) for arbitrary inputs", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string(),
|
||||
fc.string(),
|
||||
fc.integer({ min: -1_000_000, max: 1_000_000 }),
|
||||
(userId, experimentId, seed) => {
|
||||
const b = bucketUser(userId, experimentId, seed);
|
||||
return b >= 0 && b < 1;
|
||||
}
|
||||
),
|
||||
{ numRuns: 1000 }
|
||||
);
|
||||
});
|
||||
|
||||
it("is deterministic for the same inputs", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(fc.string(), fc.string(), (userId, experimentId) => {
|
||||
const a = bucketUser(userId, experimentId);
|
||||
const b = bucketUser(userId, experimentId);
|
||||
return a === b;
|
||||
}),
|
||||
{ numRuns: 1000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("property: normaliseWeights + selectVariant", () => {
|
||||
const positiveVariantArb = fc.record({
|
||||
id: fc.string({ minLength: 1 }),
|
||||
name: fc.string({ minLength: 1 }),
|
||||
weight: fc.float({
|
||||
min: Math.fround(0.001),
|
||||
max: Math.fround(1000),
|
||||
noNaN: true,
|
||||
}),
|
||||
});
|
||||
|
||||
it("selected variant always belongs to input", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(positiveVariantArb, { minLength: 1, maxLength: 10 }),
|
||||
fc.float({
|
||||
min: Math.fround(0),
|
||||
max: Math.fround(0.9999),
|
||||
noNaN: true,
|
||||
}),
|
||||
(rawVariants, bucket) => {
|
||||
const normalised = normaliseWeights(rawVariants);
|
||||
if (normalised.length === 0) return true; // skip degenerate
|
||||
const selected = selectVariant(normalised, bucket);
|
||||
return rawVariants.some((v) => v.id === selected.id);
|
||||
}
|
||||
),
|
||||
{ numRuns: 500 }
|
||||
);
|
||||
});
|
||||
|
||||
it("last normalised weight is always exactly 1.0", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(positiveVariantArb, { minLength: 1, maxLength: 20 }),
|
||||
(rawVariants) => {
|
||||
const normalised = normaliseWeights(rawVariants);
|
||||
if (normalised.length === 0) return true;
|
||||
return (
|
||||
normalised[normalised.length - 1]?.normalizedWeight ===
|
||||
1.0
|
||||
);
|
||||
}
|
||||
),
|
||||
{ numRuns: 500 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("property: evaluateTargeting", () => {
|
||||
it("empty rules always returns true", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.dictionary(
|
||||
fc.string(),
|
||||
fc.oneof(fc.string(), fc.integer(), fc.boolean())
|
||||
),
|
||||
(attrs) => {
|
||||
return evaluateTargeting([], attrs) === true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 500 }
|
||||
);
|
||||
});
|
||||
|
||||
it("eq rule is reflexive for present attributes", {
|
||||
timeout: 30000,
|
||||
}, () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string({ minLength: 1 }),
|
||||
fc.oneof(fc.string(), fc.integer()),
|
||||
(key, value) => {
|
||||
const attrs = { [key]: value };
|
||||
const rule = {
|
||||
attribute: key,
|
||||
operator: "eq" as const,
|
||||
value,
|
||||
};
|
||||
return evaluateTargeting([rule], attrs) === true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 500 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user