Add automated tests for the 25% pricing formula and $25/$15 shipping thresholds
Launch gate (design doc §5/§8) requires automated tests for both; the repo had zero test infra until now. PHPUnit + Brain Monkey, run via `make test` against the existing wordpress dev container. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MqhgnCRjjVLFnZ3Jpn93Vg
This commit is contained in:
1 parent
a733ba4bfd
commit
5d8533f290
9 files changed
+2143
-1
No files matched your search
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Bookstore\Core\Tests;
|
||||
|
||||
use Brain\Monkey;
|
||||
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
|
||||
|
||||
abstract class TestCase extends PHPUnitTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Monkey\setUp();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Monkey\tearDown();
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Bookstore\Core\Tests\Unit\Pricing;
|
||||
|
||||
use Bookstore\Core\Pricing\PricingEngine;
|
||||
use Bookstore\Core\Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Launch gate: "25% pricing logic ... have automated tests" (design doc
|
||||
* §5/§8). calculate_retail_price()/round_to_99()/estimate_processing_cost()
|
||||
* are pure math with no WordPress/WooCommerce dependency, so no mocking
|
||||
* is needed here.
|
||||
*/
|
||||
class PricingEngineTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider retailPriceCases
|
||||
*/
|
||||
public function test_calculate_retail_price_applies_25_percent_margin_formula(
|
||||
float $supplier_base,
|
||||
float $supplier_shipping,
|
||||
float $expected
|
||||
): void {
|
||||
$actual = PricingEngine::calculate_retail_price($supplier_base, $supplier_shipping);
|
||||
$this->assertEqualsWithDelta($expected, $actual, 0.001);
|
||||
}
|
||||
|
||||
public static function retailPriceCases(): array
|
||||
{
|
||||
return [
|
||||
'small order' => [10.00, 3.00, 18.99],
|
||||
'no supplier shipping' => [5.00, 0.00, 7.99],
|
||||
'near a .99 boundary' => [14.20, 0.05, 20.99],
|
||||
'zero supplier cost still covers fixed processing' => [0.00, 0.00, 0.99],
|
||||
'larger order' => [100.00, 10.00, 152.99],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider roundTo99Cases
|
||||
*/
|
||||
public function test_round_to_99_never_rounds_below_the_input(float $price, float $expected): void
|
||||
{
|
||||
$this->assertEqualsWithDelta($expected, PricingEngine::round_to_99($price), 0.001);
|
||||
}
|
||||
|
||||
public static function roundTo99Cases(): array
|
||||
{
|
||||
return [
|
||||
// The case the class's own docblock calls out: the old
|
||||
// ceil($price) - 0.01 formula gave 20.99 here, which is
|
||||
// below the $20.995 input and would erode the target margin.
|
||||
'fractional price above the X.99 ending' => [20.995, 21.99],
|
||||
'exact integer input' => [18.00, 18.99],
|
||||
'already exactly X.99' => [7.99, 7.99],
|
||||
'just above X.99' => [8.00, 8.99],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_estimate_processing_cost(): void
|
||||
{
|
||||
$this->assertEqualsWithDelta(0.83, PricingEngine::estimate_processing_cost(20.99), 0.001);
|
||||
$this->assertEqualsWithDelta(3.00, PricingEngine::estimate_processing_cost(100.00), 0.001);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Bookstore\Core\Tests\Unit\Shipping;
|
||||
|
||||
use Bookstore\Core\Shipping\BSC_Free_Shipping;
|
||||
use Bookstore\Core\Tests\TestCase;
|
||||
use Brain\Monkey\Functions;
|
||||
use Mockery;
|
||||
|
||||
/**
|
||||
* Launch gate: "$25/$15 shipping thresholds have automated tests" (design
|
||||
* doc §5/§8). threshold_for_current_customer() reads membership via a raw
|
||||
* $wpdb query and thresholds via get_option() (with the $25/$15 launch
|
||||
* numbers as the fallback defaults) — both are stubbed here rather than
|
||||
* hitting WordPress/a real database. The class's own docblock says
|
||||
* thresholds are option-backed "so the launch gate's automated test can
|
||||
* assert against the same source this reads from" — the last test below
|
||||
* proves exactly that.
|
||||
*/
|
||||
class BSCFreeShippingTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
global $wpdb;
|
||||
$wpdb = null;
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_guest_gets_default_25_dollar_threshold(): void
|
||||
{
|
||||
Functions\when('get_current_user_id')->justReturn(0);
|
||||
Functions\when('get_option')->alias(fn($name, $default) => $default);
|
||||
|
||||
$this->assertSame(25.00, BSC_Free_Shipping::threshold_for_current_customer());
|
||||
}
|
||||
|
||||
public function test_logged_in_non_member_gets_default_25_dollar_threshold(): void
|
||||
{
|
||||
Functions\when('get_current_user_id')->justReturn(42);
|
||||
Functions\when('get_option')->alias(fn($name, $default) => $default);
|
||||
$this->mock_wpdb_is_free_member(false);
|
||||
|
||||
$this->assertSame(25.00, BSC_Free_Shipping::threshold_for_current_customer());
|
||||
}
|
||||
|
||||
public function test_free_member_gets_15_dollar_threshold(): void
|
||||
{
|
||||
Functions\when('get_current_user_id')->justReturn(7);
|
||||
Functions\when('get_option')->alias(fn($name, $default) => $default);
|
||||
$this->mock_wpdb_is_free_member(true);
|
||||
|
||||
$this->assertSame(15.00, BSC_Free_Shipping::threshold_for_current_customer());
|
||||
}
|
||||
|
||||
public function test_custom_option_override_is_respected_not_the_hardcoded_default(): void
|
||||
{
|
||||
Functions\when('get_current_user_id')->justReturn(0);
|
||||
Functions\expect('get_option')
|
||||
->once()
|
||||
->with('bsc_shipping_threshold_guest', 25.00)
|
||||
->andReturn(29.99);
|
||||
|
||||
$this->assertSame(29.99, BSC_Free_Shipping::threshold_for_current_customer());
|
||||
}
|
||||
|
||||
private function mock_wpdb_is_free_member(bool $is_member): void
|
||||
{
|
||||
global $wpdb;
|
||||
$wpdb = Mockery::mock('wpdb');
|
||||
$wpdb->prefix = 'wp_';
|
||||
$wpdb->shouldReceive('prepare')->once()->andReturn('SELECT is_free_member ...');
|
||||
$wpdb->shouldReceive('get_var')->once()->andReturn($is_member ? '1' : '0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Unit-test bootstrap: pure PHPUnit + Brain Monkey, not a full WordPress
|
||||
* bootstrap. includes/*.php all guard on `defined('ABSPATH') || exit;`
|
||||
* at file-load time, and BSC_Free_Shipping extends \WC_Shipping_Method
|
||||
* (a WooCommerce core class) — both need to exist before Composer's
|
||||
* autoloader ever pulls those files in, hence defining/stubbing them
|
||||
* here rather than per test case.
|
||||
*/
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
}
|
||||
|
||||
if (!class_exists(\WC_Shipping_Method::class)) {
|
||||
class WC_Shipping_Method
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user