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

3

如果两个或更多测试类正在测试相同接口/抽象类的不同实现,并且具有相同的测试用例但使用不同的固定装置,是否将测试用例重构出来是一个好主意呢?

假设代码和测试看起来像这样:

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);
        );
    }
}

这个结构看起来更好,但可能缺乏可读性。所以最终我不确定它是否可行。

4个回答

1

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

提取基本测试用例只适用于被测试类的公共接口,但是这些接口不能强制执行我们试图测试的相同业务逻辑工作流程。让我们修改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

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


1

你已经将PHPUnitTest的功能抽象出来,使其适用于多个类!很棒。我也看到,如果Sumator或Multiplicator在未来添加了功能,这将成为问题-无论您对任何一个类做什么,您总是会面临一个问题,即是否应该将其抽象到测试框架的基类中。

在我的看法中,这会使可维护性变得复杂,不是因为您必须调整多个类(这在测试类中以任何方式都会发生),而是因为需要维护额外的代码结构,每当您为任何一个类做出选择时,都需要跟踪它。

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


0

我发现为测试编写一个基类只有两种情况下才有用:

  1. 当基类仅包含应用程序的常见实用程序/辅助方法/类,例如常见的模拟类创建器时。
  2. 被测试产品与其他产品共享一些代码,但在某种程度上进行了扩展;因此您可以在测试基类及其子类中反映这一点。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接