improve tests

This commit is contained in:
Dennis Eichhorn 2020-09-17 18:50:56 +02:00
parent 793e7a5f15
commit 74c3c456b9
16 changed files with 249 additions and 95 deletions

View File

@ -27,6 +27,8 @@ final class CustomerValue
/**
* Simple customer lifetime value
*
* Hazard Model, same as $margin * (1 + $discountRate) / (1 + $discountRate - $retentionRate)
*
* @param float $margin Margin per period
* @param float $retentionRate Rate of remaining customers per period (= average lifetime / (1 + average lifetime))
* @param float $discountRate Cost of capital to discount future revenue

View File

@ -100,23 +100,21 @@ final class Metrics
*
* @since 1.0.0
*/
public static function calculateMailingSuccess(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix
public static function calculateMailingSuccessEstimation(float $discountRate, array $purchaseProbability, array $payoffs) : Matrix
{
$count = \count($purchaseProbability);
$profit = new Matrix($count, $count);
$profit = new Vector($count, 1);
$G = Vector::fromArray($payoffs);
$P = [
$G,
self::createCustomerPurchaseProbabilityMatrix($purchaseProbability),
];
$P = self::createCustomerPurchaseProbabilityMatrix($purchaseProbability);
$newP = new IdentityMatrix($count);
for ($i = 0; $i < $count; ++$i) {
if (!isset($P[$i])) {
$P[$i] = $P[$i - 1]->mult($P[$i - 1]);
}
// $i = 0;
$profit = $profit->add($G);
$profit->add($P[$i]->mult($G)->mult(1 / \pow(1 + $discountRate, $i)));
for ($i = 1; $i < $count + 1; ++$i) {
$newP = $newP->mult($P);
$profit = $profit->add($newP->mult($G)->mult(1 / \pow(1 + $discountRate, $i)));
}
return $profit;
@ -125,6 +123,8 @@ final class Metrics
/**
* Calculate V of the migration model
*
* Pfeifer and Carraway 2000
*
* @param float $discountRate Discount rate
* @param array $purchaseProbability Purchase probabilities for different periods
* @param array $payoffs Payoff vector (first element = payoff - cost, other elements = -cost, last element = 0)
@ -147,10 +147,12 @@ final class Metrics
/**
* Calculate the purchase probability of the different purchase states.
*
* Pfeifer and Carraway 2000
*
* A customer can either buy in a certain period or not.
* Depending on the result he either moves on to the next state (not buying) or returns to the first state (buying).
*
* @param int $period Period to evaluate
* @param int $period Period to evaluate (t)
* @param array $purchaseProbability Purchase probabilities
*
* @return Matrix [
@ -160,12 +162,14 @@ final class Metrics
*/
public static function migrationModelPurchaseProbability(int $period, array $purchaseProbability) : Matrix
{
$matrix = self::createCustomerPurchaseProbabilityMatrix($purchaseProbability);
for ($i = 0; $i < $period; ++$i) {
$matrix = $matrix->mult($matrix);
$matrix = self::createCustomerPurchaseProbabilityMatrix($purchaseProbability);
$newMatrix = clone $matrix;
for ($i = 0; $i < $period - 1 ; ++$i) {
$newMatrix = $newMatrix->mult($matrix);
}
return $matrix;
return $newMatrix;
}
/**
@ -183,7 +187,7 @@ final class Metrics
* p1, 1-p1, 0,
* p2, 0, 1-p2,
* p3, 0, 1-p3,
* ] where pi = Probability that customer buys in period i
* ] where pi = Probability that customer buys in period i / moves from one state to the next state
*
* @since 1.0.0
*/
@ -193,9 +197,12 @@ final class Metrics
$count = \count($purchaseProbability);
for ($i = 0; $i < $count; ++$i) {
$matrix[$i] = [];
$matrix[$i] = \array_fill(0, $count, 0);
$matrix[$i][0] = $purchaseProbability[$i];
$matrix[$i][$i + 1] = 1 - $purchaseProbability[$i];
$matrix[$i][
$i === $count - 1 ? $i : $i + 1
] = 1 - $purchaseProbability[$i];
}
return Matrix::fromArray($matrix);

View File

@ -144,35 +144,52 @@ final class EventManager implements \Countable
}
/**
* Trigger event
* Trigger event based on regex for group and/or id
*
* @param string $group Name of the event
* @param string $id Sub-requirement for event
* @param string $group Name of the event (can be regex)
* @param string $id Sub-requirement for event (can be regex)
* @param mixed $data Data to pass to the callback
*
* @return bool returns true on successfully triggering the event, false if the event couldn't be triggered which also includes sub-requirements missing
*
* @todo Orange-Management/phpOMS#241
* [EventManager] Create an event with a regex id/name and trigger it
* @return bool returns true on successfully triggering ANY event, false if NO event could be triggered which also includes sub-requirements missing
*
* @since 1.0.0
*/
public function trigger(string $group, string $id = '', $data = null) : bool
public function triggerSimilar(string $group, string $id = '', $data = null) : bool
{
if (isset($this->callbacks[$group])) {
return $this->triggerSingleEvent($group, $id, $data);
$groupIsRegex = \stripos($group, '/') === 0;
$idIsRegex = \stripos($id, '/') === 0;
$groups = [];
if ($groupIsRegex) {
foreach ($this->groups as $groupName => $value) {
if (\preg_match($group, $groupName) === 1) {
$groups[$groupName] = [];
}
}
} else {
$groups[$group] = [];
}
$allGroups = \array_keys($this->callbacks);
$result = false;
foreach ($allGroups as $match) {
if (\preg_match('~^' . $match . '$~', $group) === 1) {
$result = $result || $this->triggerSingleEvent($match, $id, $data);
foreach ($groups as $groupName => $groupValues) {
if ($idIsRegex) {
foreach ($this->groups[$groupName] as $idName => $value) {
if (\preg_match($id, $idName) === 1) {
$groups[$groupName][] = $idName;
}
}
} else {
$groups[$groupName][] = $id;
}
}
return $result;
$triggerValue = false;
foreach ($groups as $groupName => $ids) {
foreach ($ids as $id) {
$triggerValue = $triggerValue || $this->trigger($groupName, $id, $data);
}
}
return $triggerValue;
}
/**
@ -186,8 +203,12 @@ final class EventManager implements \Countable
*
* @since 1.0.0
*/
private function triggerSingleEvent(string $group, string $id = '', $data = null) : bool
public function trigger(string $group, string $id = '', $data = null) : bool
{
if (!isset($this->callbacks[$group])) {
return false;
}
if (isset($this->groups[$group])) {
$this->groups[$group][$id] = true;
}

View File

@ -103,13 +103,9 @@ final class FileLogger implements LoggerInterface
$path = \realpath(empty($lpath) ? __DIR__ . '/../../' : $lpath);
$this->verbose = $verbose;
if (\is_dir($lpath) || \strpos($lpath, '.') === false) {
$path = \rtrim($path !== false ? $path : $lpath, '/') . '/' . \date('Y-m-d') . '.log';
} else {
$path = $lpath;
}
$this->path = $path;
$this->path = \is_dir($lpath) || \strpos($lpath, '.') === false
? \rtrim($path !== false ? $path : $lpath, '/') . '/' . \date('Y-m-d') . '.log'
: $lpath;
}
/**
@ -135,11 +131,6 @@ final class FileLogger implements LoggerInterface
*
* @return FileLogger
*
* @todo Orange-Management/phpOMS#248
* [FileLogger] Create test for getInstance
* Maybe unset the instance static variable first because it might be defined already.
* In order to do this use the `TestUtils` functionality.
*
* @since 1.0.0
*/
public static function getInstance(string $path = '', bool $verbose = false) : self

View File

@ -35,7 +35,7 @@ final class Vector extends Matrix
*/
public static function fromArray(array $vector) : self
{
$v = new self();
$v = new self(\count($vector), 1);
$v->setMatrixV($vector);
return $v;

View File

@ -196,7 +196,7 @@ final class HttpRequest extends RequestAbstract
}
$line = \rtrim($lineRaw);
// @codeCoverageIgnoreEnd
if ($line === '') {
if (!empty($partInfo['Content-Disposition']['filename'])) { /* Is file */
$tempdir = \sys_get_temp_dir();
@ -247,6 +247,7 @@ final class HttpRequest extends RequestAbstract
$this->files[$name]['error'] = \UPLOAD_ERR_OK;
$this->files[$name]['size'] = \filesize($tempname);
$this->files[$name]['tmp_name'] = $tempname;
// @codeCoverageIgnoreEnd
} elseif ($partInfo !== null) { /* Is variable */
// @codeCoverageIgnoreStart
// Tested but coverage doesn't show up
@ -362,7 +363,7 @@ final class HttpRequest extends RequestAbstract
$firstLocalComponents = \explode('-', $locals[0]);
// @codeCoverageIgnoreEnd
return \strtolower($firstLocalComponents[0]);
return \strtolower($firstLocalComponents[0]); // @codeCoverageIgnore
}
/**
@ -383,7 +384,7 @@ final class HttpRequest extends RequestAbstract
$locals = \stripos($components[0], ',') !== false ? $locals = \explode(',', $components[0]) : $components;
// @codeCoverageIgnoreEnd
return \str_replace('-', '_', $locals[0]);
return \str_replace('-', '_', $locals[0]); // @codeCoverageIgnore
}
/**

View File

@ -207,7 +207,7 @@ final class UriFactory
}
if (isset($urlStructure['query'])) {
\parse_str($urlStructure['query'] ?? [], $urlStructure['query']);
\parse_str(\str_replace('?', '&', $urlStructure['query']), $urlStructure['query']);
}
$escaped =

View File

@ -30,10 +30,6 @@ class Tar implements ArchiveInterface
{
/**
* {@inheritdoc}
*
* @todo Orange-Management/phpOMS#250
* [Tar] Create tar test without destination path/name
* Simply call `Tar::pack([src1, src2, ...], 'output.tar')`
*/
public static function pack($sources, string $destination, bool $overwrite = false) : bool
{

View File

@ -30,10 +30,6 @@ class Zip implements ArchiveInterface
{
/**
* {@inheritdoc}
*
* @todo Orange-Management/phpOMS#249
* [Zip] Create zip test without destination path/name
* Simply call `Zip::pack([src1, src2, ...], 'output.zip')`
*/
public static function pack($sources, string $destination, bool $overwrite = false) : bool
{

View File

@ -219,14 +219,16 @@ abstract class ViewAbstract implements RenderableInterface
public function render(...$data) : string
{
$ob = '';
$path = __DIR__ . '/../..' . $this->template . '.tpl.php';
if (!\file_exists($path)) {
throw new PathException($path);
}
try {
\ob_start();
$path = __DIR__ . '/../..' . $this->template . '.tpl.php';
if (!\file_exists($path)) {
throw new PathException($path);
}
/** @noinspection PhpIncludeInspection */
$includeData = include $path;
$ob = (string) \ob_get_clean();
@ -235,7 +237,8 @@ abstract class ViewAbstract implements RenderableInterface
$ob = (string) \json_encode($includeData);
}
} catch (\Throwable $e) {
$ob = ''; // @codeCoverageIgnore
\ob_end_clean();
$ob = '';
} finally {
return $ob;
}

View File

@ -78,6 +78,19 @@ class ArticleCorrelationAffinityTest extends \PHPUnit\Framework\TestCase
$orders = [
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 0, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 0, 'C' => 0, 'D' => 1],
['A' => 0, 'B' => 0, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 0, 'C' => 1, 'D' => 0],
['A' => 0, 'B' => 0, 'C' => 1, 'D' => 0],
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 0, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 0, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 1, 'B' => 1, 'C' => 0, 'D' => 0],
['A' => 0, 'B' => 0, 'C' => 0, 'D' => 0],
['A' => 0, 'B' => 0, 'C' => 0, 'D' => 0],
];
$aff = new ArticleCorrelationAffinity($orders);

View File

@ -35,6 +35,7 @@ class CustomerValueTest extends \PHPUnit\Framework\TestCase
self::assertEqualsWithDelta(30000, CustomerValue::getSimpleCLV($margin, $retention, 0.0), 0.1);
}
/**
* @testdox The monthly recurring revenue (MRR) is correctly calculated
* @group framework

View File

@ -33,42 +33,87 @@ class MetricsTest extends \PHPUnit\Framework\TestCase
}
/**
* @testdox The profit according to Berry can be correctly calculated
* @group framework
*/
public function testBerrysCustomerProfits() : void
{
self::markTestIncomplete();
$acquisitionCost = 30.0;
$customers = 100000;
$ongoingMarketingCustomer = 10;
$revenueCustomer = 100;
$cogsCustomer = 50;
$discountRate = 0.15;
self::assertEqualsWithDelta(
4076923.08,
Metrics::getBerrysCustomerProfits($customers, $acquisitionCost, $revenueCustomer, $cogsCustomer, $ongoingMarketingCustomer, $discountRate, 0.5)
, 0.01
);
}
/**
* @testdox The purchase probability of customers can be calculated based on historic information using the migration model
* @group framework
*/
public function testLifeTimeValue() : void
public function testMigrationModelPurchaseMatrix() : void
{
self::markTestIncomplete();
}
/**
* @group framework
*/
public function testSimpleRetentionLifeTimeValue() : void
{
self::markTestIncomplete();
}
/**
* @group framework
*/
public function testMailingSuccess() : void
{
self::markTestIncomplete();
// Basis:
// Someone who just bought will buy again = 30%
// Someone who bought two years ago will buy again = 20%
// Someone who bought three years ago will buy again = 5%
// Result:
// Someone who just bought will have bought in 4 years = 8.2%
// Someone who bought 2 years ago wil have bought in 4 years = 3.7%
// ...
self::assertEqualsWithDelta(
[
[0.0823, 0.0973, 0.1288, 0.6916],
[0.037, 0.0406, 0.056, 0.8664],
[0.0070, 0.0080, 0.0084, 0.9766],
[0, 0, 0, 1],
],
Metrics::migrationModelPurchaseProbability(
4,
[0.3, 0.2, 0.05, 0]
)->toArray(),
0.01
);
}
/**
* @testdox The CLTV can be calculated using the migration model
* @group framework
*/
public function testMigrationModel() : void
{
self::markTestIncomplete();
// The first element in the migration model result is the CLTV
self::assertEqualsWithDelta(
[[231.08], [57.29], [21.01], [0.0]],
Metrics::migrationModel(
0.1,
[0.5, 0.2, 0.1, 0],
[100, 0, 0, 0]
)->toArray(),
0.1
);
}
/**
* @testdox The migration model can be used in order to determin which buying/none-buying customer group should receive a mailing
* @group framework
*/
public function testMailingSuccessEstimation() : void
{
self::assertEqualsWithDelta(
[[49.4], [2.69], [-1.98], [0.0]],
Metrics::calculateMailingSuccessEstimation(
0.2,
[0.3, 0.2, 0.05, 0],
[36, -4, -4, 0]
)->toArray(),
0.1
);
}
}

View File

@ -87,7 +87,7 @@ class EventManagerTest extends \PHPUnit\Framework\TestCase
$this->event->addGroup('group', 'id1');
$this->event->addGroup('group', 'id2');
$this->event->trigger('group', 'id1');
self::assertFalse($this->event->trigger('group', 'id1'));
self::assertTrue($this->event->trigger('group', 'id2'));
}
@ -115,6 +115,63 @@ class EventManagerTest extends \PHPUnit\Framework\TestCase
self::assertFalse($this->event->trigger('invalid'));
}
/**
* @testdox An event can be triggered with group and id regex matches
* @covers phpOMS\Event\EventManager
* @group framework
*/
public function testDispatchSimilarGroupAndId() : void
{
$this->event->attach('group', 'path_to_execute', false, true);
$this->event->addGroup('group', 'id1');
$this->event->addGroup('group', 'id2');
self::assertTrue($this->event->triggerSimilar('/[a-z]+/', '/id\d/'));
}
/**
* @testdox An event can be triggered with a fixed group definition and id regex matches
* @covers phpOMS\Event\EventManager
* @group framework
*/
public function testDispatchSimilarId() : void
{
$this->event->attach('group', 'path_to_execute', false, true);
$this->event->addGroup('group', 'id1');
$this->event->addGroup('group', 'id2');
self::assertTrue($this->event->triggerSimilar('group', '/id\d/'));
}
/**
* @testdox An event can be triggered with regex group matches and fixed id definition
* @covers phpOMS\Event\EventManager
* @group framework
*/
public function testDispatchSimilarGroup() : void
{
$this->event->attach('group', 'path_to_execute', false, true);
$this->event->addGroup('group', 'id1');
$this->event->addGroup('group', 'id2');
self::assertFalse($this->event->triggerSimilar('group', 'id1'));
self::assertTrue($this->event->triggerSimilar('group', 'id2'));
}
/**
* @testdox A invalid regex match will not triggered an event
* @covers phpOMS\Event\EventManager
* @group framework
*/
public function testDispatchSimilarInvalid() : void
{
$this->event->attach('group', 'path_to_execute', false, true);
$this->event->addGroup('group', 'id1');
$this->event->addGroup('group', 'id2');
self::assertFalse($this->event->triggerSimilar('group', '/id\d0/'));
}
/**
* @testdox An event can be defined to reset after all conditions and subconditions are met. Then all conditions and sub conditions must be met again before it gets triggered again.
* @covers phpOMS\Event\EventManager

View File

@ -16,6 +16,7 @@ namespace phpOMS\tests\Log;
use phpOMS\Log\FileLogger;
use phpOMS\Log\LogLevel;
use phpOMS\Utils\TestUtils;
require_once __DIR__ . '/../Autoloader.php';
@ -76,6 +77,30 @@ class FileLoggerTest extends \PHPUnit\Framework\TestCase
self::assertEquals([], $this->log->getByLine());
}
/**
* @testdox The file logger can automatically create a new instance if none exists
* @covers phpOMS\Log\FileLogger
* @group framework
*/
public function testFileLoggerInstance() : void
{
if (\file_exists(__DIR__ . '/named.log')) {
\unlink(__DIR__ . '/named.log');
}
$instance = FileLogger::getInstance(__DIR__ . '/named.log', false);
TestUtils::getMember($instance, 'instance', null);
$log = FileLogger::getInstance(__DIR__ . '/named.log', false);
self::assertInstanceOf(FileLogger::class, $log);
TestUtils::setMember($instance, 'instance', $instance);
if (\file_exists(__DIR__ . '/named.log')) {
\unlink(__DIR__ . '/named.log');
}
}
/**
* @testdox A log file for the output can be specified for the file logger
* @covers phpOMS\Log\FileLogger

View File

@ -431,12 +431,10 @@ class ViewTest extends \PHPUnit\Framework\TestCase
*/
public function testRenderException() : void
{
$this->expectException(\phpOMS\System\File\PathException::class);
$view = new View($this->app->l11nManager);
$view->setTemplate('something.txt');
$view->render();
self::assertEquals('', $view->render());
}
/**
@ -446,12 +444,10 @@ class ViewTest extends \PHPUnit\Framework\TestCase
*/
public function testSerializeException() : void
{
$this->expectException(\phpOMS\System\File\PathException::class);
$view = new View($this->app->l11nManager);
$view->setTemplate('something.txt');
$view->serialize();
self::assertEquals('', $view->serialize());
}
/**