Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/AbService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,27 @@ class AbService

public function __construct(
/** array<int,array<string,array<string,float>>> $experiments */
protected array $experiments = []
protected array $experiments = [],
protected ?string $activeExperiment = null,
protected bool $verifyConfig = false,
) {
$this->setUid($this->generateUid());

if ($this->verifyConfig) {
$this->verifyConfig();
}
}

public function verifyConfig()
public function verifyConfig(): void
{
if (empty($this->experiments)) {
throw new InvalidArgumentException("No experiments defined");
}

if (!empty($this->activeExperiment) && !array_key_exists($this->activeExperiment, $this->experiments)) {
throw new InvalidArgumentException("Active experiment '{$this->activeExperiment}' does not exist");
}

foreach ($this->experiments as $experiment => $variants) {
$sum = 0;
foreach ($variants as $variant => $weight) {
Expand Down Expand Up @@ -58,7 +69,7 @@ public function getExperiments(): array

public function getExperiment(): string
{
return array_key_first($this->experiments);
return $this->activeExperiment ?? array_key_first($this->experiments);
}

public function getVariants(string $experiment): array
Expand All @@ -74,10 +85,6 @@ public function getVariants(string $experiment): array

public function getVariant(string $experiment): string
{
if (is_null($experiment)) {
$experiment = $this->getExperiment();
}

if (preg_match('/^variant-(?<variant>.*)$/', $this->uid, $m)) {
return $m['variant'];
}
Expand Down
66 changes: 41 additions & 25 deletions test/AbServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,33 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(\DMT\AbMiddleware\AbService::class)]
#[CoversClass(AbService::class)]
class AbServiceTest extends TestCase
{
protected string $testExperiment = 'ab-test-experiment';
protected string $testActiveExperiment = 'ab-test-experiment';

protected array $testExperiments = [
'ab-test-experiment' => [
'variant1' => 0.5,
'control' => 0.5,
]
],
'ab-old-experiment' => [
'variant1' => 0.5,
'control' => 0.5,
],
];

public function testGenerateUid(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$uid = $abService->generateUid();

$this->assertIsString($uid);
}

public function testGetUid(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$uid = $abService->getUid();

$this->assertIsString($uid);
Expand All @@ -41,53 +45,62 @@ public function testGetUid(): void

public function testSetUid(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$abService->setUid('test');

$this->assertEquals('test', $abService->getUid());
}

public function testGetExperiments(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$experiments = $abService->getExperiments();

$this->assertIsArray($experiments);
}

public function testGetExperiment(): void
public function testGetExperimentDefaultsToFirstExperiment(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, null);
$experiment = $abService->getExperiment();

$this->assertIsString($experiment);
$this->assertEquals($this->testExperiment, $experiment);
$this->assertEquals($this->testActiveExperiment, $experiment);
}

public function testGetExperimentConfigured(): void
{
$abService = new AbService($this->testExperiments, 'ab-old-experiment');
$experiment = $abService->getExperiment();

$this->assertIsString($experiment);
$this->assertEquals('ab-old-experiment', $experiment);
}

public function testGetVariant(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$abService->setUid('test');

$variant = $abService->getVariant($this->testExperiment);
$variant = $abService->getVariant($this->testActiveExperiment);

$this->assertIsString($variant);
$this->assertContains($variant, array_keys($this->testExperiments[$this->testExperiment]));
$this->assertEquals($variant, $abService->getVariant($this->testExperiment));
$this->assertContains($variant, array_keys($this->testExperiments[$this->testActiveExperiment]));
$this->assertEquals($variant, $abService->getVariant($this->testActiveExperiment));
}

public function testGetVariantDistributed(): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$abService->setUid('test');

$variantOriginal = $abService->getVariant($this->testExperiment);
$variantOriginal = $abService->getVariant($this->testActiveExperiment);

$this->assertIsString($variantOriginal);

for ($i = 0; $i < 100; $i++) {
$abService->setUid(uniqid());
$variant = $abService->getVariant($this->testExperiment);
$variant = $abService->getVariant($this->testActiveExperiment);

if ($variant !== $variantOriginal) {
break;
Expand All @@ -99,12 +112,15 @@ public function testGetVariantDistributed(): void

public function testMissingExperiments(): void
{
$abService = new AbService([
$this->testExperiment => [
]
]);
$this->expectException(InvalidArgumentException::class);
$abService->verifyConfig();

$abService = new AbService(
experiments: [
$this->testActiveExperiment => [],
],
activeExperiment: null,
verifyConfig: true
);
}

public static function provideHashVariants(): Generator
Expand All @@ -123,7 +139,7 @@ public static function provideHashVariants(): Generator
public function testChooseVariant(string $expected, float $hash, array $variants): void
{
$abService = new AbService([
$this->testExperiment => $variants
$this->testActiveExperiment => $variants,
]);

$variant = $abService->chooseVariant($hash, $variants);
Expand All @@ -141,7 +157,7 @@ public static function provideTestResultData(): Generator
'conversionB' => 200,
'uplift' => 100,
'zscore' => 6.3246,
];
];
yield 'test2' => [
'countA' => 1000,
'countB' => 1000,
Expand Down Expand Up @@ -171,7 +187,7 @@ public static function provideTestResultData(): Generator
#[DataProvider('provideTestResultData')]
public function testGetTestSignficance($countA, $countB, $conversionA, $conversionB, $uplift, $zscore): void
{
$abService = new AbService($this->testExperiments);
$abService = new AbService($this->testExperiments, $this->testActiveExperiment);
$significance = $abService->getTestSignificance($countA, $countB, $conversionA, $conversionB);
$this->assertEquals($uplift, $significance['uplift']);
$this->assertEquals($zscore, $significance['z-score']);
Expand Down