【问题标题】:Refactoring out common tests into base test case将常见测试重构为基本测试用例
【发布时间】:2012-05-03 12:10:07
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: php unit-testing testing phpunit xunit


    【解决方案1】:

    您已经抽象出足够多的 PHPUnitTest 功能,使其适用于多个类!凉爽的。我还看到,如果 Sumator 或 Multiplicator 在未来添加了功能,这将成为问题——无论您对任一类做什么,您都将始终面临是否应该将其抽象到基础的问题测试框架中的类也是如此。

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

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

    【讨论】:

      【解决方案2】:

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

      【讨论】:

        【解决方案3】:

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

        提取基础测试用例只能适用于被测类的通用接口,但那些接口不能强制业务逻辑的工作流我们尝试测试。让我们修改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
            }
        }
        

        除了明显的缺陷外,以上所有因素都在可读性和可维护性方面增加了不必要的复杂性。

        感谢大家的贡献。

        【讨论】:

          【解决方案4】:

          我发现有一个用于测试的基类主要只在两种情况下有用:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2016-10-21
            • 1970-01-01
            • 2012-11-29
            • 1970-01-01
            • 1970-01-01
            • 2017-08-26
            • 2014-04-15
            相关资源
            最近更新 更多