【问题标题】:How to handle a unit/integration test that contains too many lines of codes?如何处理包含太多代码行的单元/集成测试?
【发布时间】:2020-04-20 00:18:11
【问题描述】:

在我目前的公司,自 2014 年以来,我们有一个由 20 多个不同的开发人员使用 PHP 开发的 web 应用程序。最近,我听到我的团队成员抱怨说某些 单元测试太体积庞大(不可读且不易跟踪内部代码),他们不想向测试类添加新的场景/方法,而是希望创建新的测试类并将这些新的场景/方法添加到新的测试类中。假设我们有一个类如下:

<?php
    class Foo {   
        public function getOne(): string
        public function getAll(): array
        public function validate(): bool
    }
?>

我们有一个现有的单元测试类如下:

<?php
    class FooTest {   
        public function testGetOne() {
            // scenario 1
            // scenerio 2
            // scenario 3
            // scenario 4
            // scenario 5
            // scenario 6
            ...
        }
        public function testGetAll() {
            // scenario 1
            // scenario 2
            // scenario 3
            ...
        }
        public function testValidate() {
            // scenario 1
            // scenario 2
            ...
        }
    }
?>

正如您在上面看到的,我们遵循一些测试类指南:

  • 测试类名形成为原类名+测试后缀
  • 在测试类内部,为原始类的每个可测试方法定义了一个方法(例如 testValidate)。
  • 每个测试方法中都列出了测试场景

我们相信它有利于可追溯性。另一方面,有超过 1000 行的测试类,我们想以某种方式重构它们。对我来说,要解决的问题是:

  1. 可以为一个类创建多个单元测试类吗?如果是,那么我们应该根据什么标准将它们分开? (例如,我们是否应该为每个可测试的函数分别创建一个单元测试类FooValidateTest?)
  2. 我们应该如何处理这些测试类的命名约定?再次,基于 规则?
  3. 我们如何避免为每个单元测试类复制/过去多次帮助和模拟方法?

我想听听你的建议!

【问题讨论】:

    标签: php unit-testing phpunit refactoring


    【解决方案1】:
    1. 是的,我认为这个选项比不可读的测试类要好。您可以按功能拆分测试,在您的情况下按方法拆分。
    2. 命名测试的规则必须是反映正在测试的功能的名称。 “GetOneFooTest”可以是名称的示例。
    3. 功能测试类(“GetOneFootTest”...)从“FooTestCase”类扩展而来,所有 Foo 测试的设置都放置在该类中。

    【讨论】:

    • 感谢您的回答!第 3 点的唯一缺点是我们的测试框架 (PHPUnit) 不允许单独运行单元测试,如果它扩展了任何其他单元测试,所以基本上,它们应该分别执行但一起执行:)
    【解决方案2】:

    给你更多的想法 -

    1/ 你可以在一个新的类层中进行一些设置步骤,你可以从它扩展而来——例如,这是我用来创建一些通用设置和代码质量检查的更通用的测试父类之一对于测试代码本身:

    <?php
    
    namespace Tests;
    
    use PHPUnit\Framework\TestCase as PHPUnit_TestCase;
    use PHPUnitGoodPractices\Traits as PHPUnitGoodPractices;
    
    /**
     * @SuppressWarnings(PHPMD.NumberOfChildren)
     */
    abstract class TestCase extends PHPUnit_TestCase
    {
        use TestCaseTrait;      // setup Mockery
        use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
        use PHPUnitGoodPractices\ExpectationViaCodeOverAnnotationTrait;
        use PHPUnitGoodPractices\ExpectOverSetExceptionTrait;
        use PHPUnitGoodPractices\IdentityOverEqualityTrait;
        #use PHPUnitGoodPractices\ProphecyOverMockObjectTrait;
    }
    

    我自己的许多测试都将扩展 Tests\TestCase

    正如我在该类中使用的那样,您还可以使用特征将通用功能移出各个测试类。 Soem DocBlock 注释,例如@before,也将以与protected function setUp(): void {} 等方法相同的方式运行代码。

    2/ 更有用的可能是@dataProvider - 它们返回一个数据数组(或来自Generator 方法的yield)。如果您的测试针对多个不同数据的多个版本运行相同的测试代码,那么使用 DataProvider 方法允许使用测试方法的数据参数调用单个测试数十次甚至数百次。

    <?php
    use PHPUnit\Framework\TestCase;
    
    class DataTest extends TestCase
    {
        /**
         * @dataProvider additionProvider
         */
        public function testAdd($a, $b, $expected)
        {
            $this->assertSame($expected, $a + $b);
        }
    
        public function additionProvider()
        {
            return [
                [0, 0, 0],
                [0, 1, 1],
                [1, 0, 1],
                [1, 1, 3],    // this test would fail
            ];
        }
    }
    

    【讨论】:

      【解决方案3】:

      1) - 我会说,当你要重构你的测试类时,你会有一个更干净、更轻量级的类,例如,我有一个包含 4000 多行代码的 userTest 类。我构建测试文件夹的方式是设置项目文件夹的方式。例如,如果我有 App/Repositories/UserRepository,那么我将有 Tests/Repositories/UserTest。它更干净我宁愿不将我的测试分成不同的类。因为其他开发者永远不会明白其中的原因。

      2) - 我建议使用这个命名约定,MethodName_ExpectedBehavior_StateUnderTest,validate_should_throw_an_error_if_X_field_is_missing,或者你可以使用 PascalCase。 https://dzone.com/articles/7-popular-unit-test-naming

      3) - 您始终可以将模拟和辅助方法封装在单独的类中,您可以静态定义它们或使用测试类中的 setup 方法创建该类的新实例。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-11-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-02-25
        • 1970-01-01
        • 2010-09-09
        • 1970-01-01
        相关资源
        最近更新 更多