PHPUnit 测试隔离以及如何编写预期值取决于方法中经常更改的某些值的测试

PHPUnit tests isolation and how to write a test where expected value depends on some value in method that changes often

所以我在 PHP 中编码并使用 PHPUnit 编写测试。我有这个方法 convertCashAmountToMainCurrency 我想测试:

public static function convertCashAmountToMainCurrency(string $currency, float $amount): float
{
    $feeInMainCurrency = $amount / Constants::CURRENCIES[$currency][Constants::RATE];

    return self::roundUp($feeInMainCurrency, Constants::CURRENCIES[Constants::MAIN_CURRENCY][Constants::PRECISION]);
}

public static function roundUp(float $value, int $precision): float
{
    $pow = pow(10, $precision);

    return (ceil($pow * $value) + ceil($pow * $value - ceil($pow * $value))) / $pow;
}

我有常量 在这里使用:

public const RATE = 'RATE';
public const PRECISION = 'PRECISION';

public const CURRENCY_EUR = 'EUR';
public const CURRENCY_USD = 'USD';
public const CURRENCY_JPY = 'JPY';

public const MAIN_CURRENCY = self::CURRENCY_EUR;

public const CURRENCIES = [
    self::CURRENCY_EUR => [
        self::RATE => 1,
        self::PRECISION => 2,
    ],
    self::CURRENCY_USD => [
        self::RATE => 1.1497,
        self::PRECISION => 2,
    ],
    self::CURRENCY_JPY => [
        self::RATE => 129.53,
        self::PRECISION => 0,
    ],
];

我正在测试这样的方法:

/**
 * @param string $currency
 * @param float $amount
 * @param float $expectation
 *
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 * @dataProvider dataProviderForConvertCashAmountToMainCurrencyTesting
 */
public function testConvertCashAmountToMainCurrency(string $currency, float $amount, float $expectation)
{
    $this->assertEquals(
        $expectation,
        Math::convertCashAmountToMainCurrency($currency, $amount)
    );
}

public function dataProviderForConvertCashAmountToMainCurrencyTesting(): array
{
    return [
        'convert EUR to main currency' => [Constants::CURRENCY_EUR, 100.01, 100.01],
        'convert USD to main currency' => [Constants::CURRENCY_USD, 100.01, 86.99],
        'convert JPY to main currency' => [Constants::CURRENCY_JPY, 10001, 77.21],
    ];
}

当期望值和货币汇率以固定大小表示时,测试通过得很好。 但问题是无论货币兑换率值如何,我都需要它们每次都通过,所以每次汇率变化时我都不需要重写测试。 因为现在,如果我更改了 RATE(或者甚至是 PRECISION,例如)值,由于断言失败,测试将不会通过。

既然我被告知我需要隔离我的测试来解决这个问题,谁能证实这一点并让我走上正确的道路来解决这个问题? 在此先感谢您的帮助!

问题是 convertCashAmountToMainCurrency 依赖于全局状态。有一些方法可以在测试中处理这个问题,但对我来说这不是好的做法。我的建议是将转换提取到一个新的 class 中,以获取传入的所有必需信息。这样它就不会依赖于全局状态,并且可以轻松地单独进行测试。

这可能看起来像这样:

class CurrencyConverter
{
    private string $mainCurrency;

    /**
     * @var array<string, CurrencyConfig>
     */
    private array $currencies;

    /**
     * @param array<string, CurrencyConfig> $currencies
     */
    public function __construct(string $mainCurrency, array $currencies)
    {
        Assert::keyExists($currencies, $mainCurrency);

        $this->mainCurrency = $mainCurrency;
        $this->currencies   = $currencies;
    }

    public function toMainCurrency(string $currency, float $amount): float
    {
        Assert::keyExists($this->currencies, $currency);

        $feeInMainCurrency = $amount / $this->currencies[$currency]->rate();

        return self::roundUp($feeInMainCurrency, $this->currencies[$this->mainCurrency]->precision());
    }

    private static function roundUp(float $value, int $precision): float
    {
        $pow = pow(10, $precision);

        return (ceil($pow * $value) + ceil($pow * $value - ceil($pow * $value))) / $pow;
    }
}
class CurrencyConfig
{
    private float $rate;

    private int $precision;

    public function __construct(float $rate, int $precision)
    {
        Assert::greaterThan($rate, 0);
        Assert::greaterThanEq($precision, 0);

        $this->rate      = $rate;
        $this->precision = $precision;
    }

    public function rate(): float
    {
        return $this->rate;
    }

    public function precision(): int
    {
        return $this->precision;
    }
}
    /**
     * @param string $currency
     * @param float  $amount
     * @param float  $expectation
     *
     * @runInSeparateProcess
     * @preserveGlobalState disabled
     * @dataProvider        dataProviderForConvertCashAmountToMainCurrencyTesting
     */
    public function testConvertCashAmountToMainCurrency(string $currency, float $amount, float $expectation)
    {
        $converter = new CurrencyConverter(
            Constants::CURRENCY_EUR,
            [
                Constants::CURRENCY_EUR => new CurrencyConfig(1, 2),
                Constants::CURRENCY_USD => new CurrencyConfig(1.1497, 2),
                Constants::CURRENCY_JPY => new CurrencyConfig(129.53, 0),
            ]
        );

        self::assertEquals($expectation, $converter->toMainCurrency($currency, $amount));
    }

如果无法摆脱现有的静态方法,只需使用全局状态的配置创建一个转换器。

class Math
{
    public static function convertCashAmountToMainCurrency(string $currency, float $amount): float
    {
        $converter = new CurrencyConverter(
            Constants::MAIN_CURRENCY,
            array_map(
                fn(array $item) => new CurrencyConfig($item[Constants::RATE], $item[Constants::PRECISION]),
                Constants::CURRENCIES
            )
        );

        return $converter->toMainCurrency($currency, $amount);
    }
}