将常见测试重构为基本测试用例


Refactoring out common tests into base test case

如果两个或多个测试类(测试同一接口/抽象类的不同实现(具有共同的测试但具有不同的夹具,那么重构测试用例是一个好主意吗?

假设代码和测试如下所示:

interface MathOperation
{
    public function doMath($a, $b);
}
class Sumator implements MathOperation
{
    public function doMath($a, $b)
    {
        return $a + $b;
    }
}

class Multiplicator implements MathOperation
{
    public function doMath($a, $b)
    {
        return $a * $b;
    }
}
// tests
class SumatorTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var Sumator
     */
    protected $sumator;
    public function setUp()
    {
        $this->sumator = new Sumator;
    }
    /**
     * @dataProvider fixtures
     */
    public function testDoMath($a, $b, $expected)
    {
        $result = $this->sumator->doMath($a, $b);
        $this->assertEqual($expected, $result);
    }
    public function fixtures()
    {
        return array(
            array(1, 1, 2);
            array(2, 1, 3);
            array(100, -1, 99);
        );
    }
}
class MultiplicatorTest extends PHPUnit_Framework_TestCase
{
    /**
     * @var Multiplicator
     */
    protected $multiplicator;
    public function setUp()
    {
        $this->multiplicator = new Multiplicator;
    }
    /**
     * @dataProvider fixtures
     */
    public function testDoMath($a, $b, $expected)
    {
        $result = $this->multiplicator->doMath($a, $b);
        $this->assertEqual($expected, $result);
    }
    public function fixtures()
    {
        return array(
            array(1, 1, 1);
            array(2, 1, 2);
            array(100, -1, -100);
        );
    }
}

我希望它们(测试(看起来像这样:

class MathOperationTestCase extends PHPUnit_Framework_TestCase
{
    /**
     * @var MathOperation
     */
    protected $operation;
    public function setUp()
    {
        $this->operation = $this->createImpl();
    }
    /**
     * @return MathOperation
     */
    abstract function createImpl();
    /**
     * @dataProvider fixtures
     */
    public function testDoMath($a, $b, $expected)
    {
        $result = $this->operation->doMath($a, $b);
        $this->assertEqual($expected, $result);
    }
    abstract public function fixtures();
}
class SumatorTest extends MathOperationTestCase
{
    public function createImpl()
    {
        return new Sumator;
    }
    public function fixtures()
    {
        return array(
            array(1, 1, 2);
            array(2, 1, 3);
            array(100, -1, 99);
        );
    }
}
class MultiplicatorTest extends MathOperationTestCase
{
    public function createImpl()
    {
        return new Multiplicator;
    }
    public function fixtures()
    {
        return array(
            array(1, 1, 1);
            array(2, 1, 2);
            array(100, -1, -100);
        );
    }
}

这似乎结构更好,但可能缺乏可读性。所以最后我不确定它是否可用。

你已经抽象出了 PHPUnitTest 的功能,足以让它适用于多个类!凉。我还看到,如果 Sumator 或 Multiplicator 将来添加了功能,这将变得有问题 - 无论您对任一类执行什么操作,您都将始终面临是否应该将其抽象为基类的问题测试框架。

在我看来,这会使可维护性变得复杂,不是因为您必须调整多个类(无论哪种方式在测试类中都会发生这种情况(,而是因为维护额外的代码结构的额外负担,每当您为任一类做出选择时,您都需要跟踪该代码结构。

出于这个原因,在我看来,单元测试适用于一对一的结构。您的方法减少了代码重复,因为只要一个类具有相同的结构和功能,它就适用于这个测试类。另一方面,在我看来,它打开了让课程符合测试的诱惑,而不是相反。

如果你的原始代码发生了变化,测试也必须改变。请记住这一点,然后您将看到哪种方式可以更轻松地处理更改。如果您决定将来分离接口或此类问题可能会帮助您做出决定怎么办。

经过一番考虑,我得出的结论是,这种方法的唯一好处是减少代码重复。

提取基本测试用例只能应用于被测试类的通用接口,但这些接口不能强制我们尝试测试的业务逻辑的相同工作流。让我们修改Multiplicator类来证明这一点。

class Multiplicator implements MathOperation
{
    private $factor; // added factor which influences result of doMath()
    public function __construct($factor)
    {
        $this->factor = $factor;
    }
    public function doMath($a, $b)
    {
        return ($a * $b) * $factor;
    }
}

现在,尽管SumatorMultiplicator共享相同的接口,但测试Multiplicator的方式完全不同,例如

class MultiplicatorTest extends MathOperationTestCase
{
    // rest of code
    public function testDoMath2($ab, $b, $factor, $expected)
    {
        $multiplicator = new Multiplicator($factor);
        $result = $multiplicator->doMath($a, $b);
        $this->assertEqual($expected, $result);
    }
}

此外,我还必须通过对测试类进行轻微修改来保持与基本测试用例的向后兼容性,这是巨大的禁忌......

class Multiplicator implements MathOperation
{
    // rest of code
    public function __construct($factor = 1) // default value set in class
    {
        $this->factor = $factor;
    }
}

。或通过修改测试本身。这使得从提取的测试用例派生的测试重复且无用。

class MultiplicatorTest extends MathOperationTestCase
{
    // rest of code
    public function createImpl()
    {
        return new Multiplicator(1); // added default value
    }
}

除了明显的陷阱外,上述所有内容在可读性和可维护性方面都增加了不必要的复杂性。

感谢大家的贡献。

我发现为测试设置基类主要仅在两种情况下有用:

  1. 基类仅包含您正在处理的应用程序的通用实用程序/帮助程序方法/类之类的内容,即常见的模拟类创建者。
  2. 待测试产品
  3. 与其他产品共享一些代码,但在某种程度上对其进行扩展;因此,您可以在测试基类及其子类中镜像它。