在Magento 2中使用PHPUnit 4.1对PHP7代码进行单元测试的正确方法是什么?


23

在编写模块时,我试图为它们提供应用程序最关键部分的单元测试。但是,目前(Magento 2.1.3)有几种编写单元测试的方法:

不同的测试方式

  • 与它集成bin/magento dev:tests:run unit并在与Magento捆绑在一起的默认phpunit设置之上运行它。
  • 分别编写它们,与它们一起运行vendor/bin/phpunit app/code/Vendor/Module/Test/Unit并模拟Magento的所有内容。
  • 分别编写它们,模拟所有内容,并使用系统全局版本的PHPUnit。
  • 单独编写它们,然后使用运行它们vendor/bin/phpunit,但仍然使用\Magento\Framework\TestFramework\Unit\Helper\ObjectManager

Magento 2和PHPUnit

除此之外,Magento 2捆绑了与PHP7不兼容的PHPUnit 4.1.0。提示类型的本机(如string和int)并在签名中声明返回类型将引发错误。例如,具有方法签名的接口/类如下:

public function foo(string $bar) : bool;

...将无法被PHPUnit 4.1.0模拟。:-(

我目前的情况

因此,我现在主要以第三种方式(通过调用系统全局PHPUnit版本)编写单元测试。

在我的设置中,我全局安装了PHPUnit 5.6,因此我可以解决编写正确的PHP7代码的问题,但是我必须做一些调整。例如:

phpunit.xml 必须看起来像这样,所以我可以使用composer autoloader:

<?xml version="1.0"?>
<phpunit bootstrap="../../../../../../vendor/autoload.php"
         colors="true">
    <testsuites>
        <testsuite name="Testsuite">
            <directory>.</directory>
        </testsuite>
    </testsuites>
</phpunit>

...以及我所有的setUp()方法中,都有以下检查,以便可以编写具有前向兼容性的测试:

// Only allow PHPUnit 5.x:
if (version_compare(\PHPUnit_Runner_Version::id(), '5', '<')) {
    $this->markTestSkipped();
}

这样,当我的测试由Magentos的内置PHPUnit运行时,它不会引发错误。

我的问题

所以这是我的问题:这是编写单元测试的“健康”方式吗?因为在我看来Magento捆绑了一大堆工具来辅助测试,这在我看来并不对,我无法使用它们,因为我正在使用PHP7。我知道GitHub上有解决此问题的票证,但我想知道社区当前是如何编写测试的。

有没有一种方法可以在Magento 2中编写单元测试,所以我不必“降级”我的代码,仍然可以使用Magentos的内置帮助器来模拟对象管理器接触的所有内容?还是即使在单元测试中也使用对象管理器甚至是不好的做法?

我缺少有关如何对自己的自定义模块进行单元测试的正确方法的大量指南/示例。


1
真是个好问题。
camdixon

Answers:


17

使用捆绑的PHPUnit版本,即使它是古老的,也可能是最好的方法,因为这将允许在CI期间一起运行所有模块的测试。

我认为以与捆绑的测试框架不兼容的方式编写测试会大大降低测试的价值。
当然,您可以设置CI以使用其他版本的PHPUnit运行测试,但这会给构建系统增加很多复杂性。

就是说,我确实同意您的看法,不值得支持PHP 5.6。我尽可能使用PHP7标量类型提示和返回类型提示(此外,我不在乎市场)。

为了解决PHPUnit 4.1模拟库的局限性,我过去至少使用了两种非常简单的解决方法:

  1. 例如,使用匿名或常规类构建测试双打

    $fooIsFalseStub = new class extends Foo implements BarInterface() {
        public function __construct(){};
        public function isSomethingTrue(string $something): bool
        {
            return false;
        }
    };
  2. 使用捆绑的PHPUnit,但可以使用composer包含第三方模拟库, require-dev例如https://github.com/padraic/mockery。我尝试过的所有模拟库都可以很容易地与任何测试框架一起使用,甚至是非常老的PHPUnit版本(如4.1)。

这些都不比其他任何技术优势。您可以使用任何一种实现任何必需的测试双重逻辑。

我个人更喜欢使用匿名类,因为这不会增加外部依赖项的数量,并且以这种方式编写它们会更有趣。

编辑
要回答您的问题:

Mockery是否可以“解决” PHPUnit 4.1.0无法正确处理PHP7类型提示的问题?

是的,请参见下面的示例。

匿名类比模拟有什么好处?

使用匿名类创建测试双打也是“嘲笑”,与使用诸如PHPUnits或Mockery或其他嘲笑库的模仿并没有什么不同。
模拟只是针对特定类型的测试double,而不管其创建方式如何。
使用匿名类或模拟库之间的一个小区别是,匿名类没有外部库依赖关系,因为它只是纯PHP。否则就没有好处或缺点。这只是一个偏好问题。我之所以喜欢它,是因为它说明了测试与任何测试框架或模拟库无关,它只是编写代码来执行被测系统并自动验证其正常工作。

以及如何将主composer.json文件中的PHPUnit版本更新为5.3.5(支持PHP7的最新版本,并具有公共模拟方法(Magento 2自己的测试要求))?

这可能是有问题的,因为其他模块和核心中的测试仅使用PHPUnit 4.1进行了测试,因此,CI中可能会遇到错误的故障。出于这个原因,我认为最好坚持使用捆绑版本的PHPUnit。@maksek表示他们将更新PHPUnit,但没有为此提供ETA。


使用Mockery库对需要PHP7和PHPUnit 4.1一起运行的类的测试双进行测试的示例:

<?php

declare(strict_types = 1);

namespace Example\Php7\Test\Unit;

// Foo is a class that will not work with the mocking library bundled with PHPUnit 4.1 
// The test below creates a mock of this class using mockery and uses it in a test run by PHPUnit 4.1
class Foo
{
    public function isSomethingTrue(string $baz): bool
    {
        return 'something' === $baz; 
    }
}

// This is another class that uses PHP7 scalar argument types and a return type.
// It is the system under test in the example test below.
class Bar
{
    private $foo;

    public function __construct(Foo $foo)
    {
        $this->foo = $foo;
    }

    public function useFooWith(string $s): bool
    {
        return $this->foo->isSomethingTrue($s);
    }
}

// This is an example test that runs with PHPUnit 4.1 and uses mockery to create a test double
// of a class that is only compatible with PHP7 and younger.
class MockWithReturnTypeTest extends \PHPUnit_Framework_TestCase
{
    protected function tearDown()
    {
        \Mockery::close();
    }

    public function testPHPUnitVersion()
    {
        // FYI to show this test runs with PHPUnit 4.1
        $this->assertSame('4.1.0', \PHPUnit_Runner_Version::id());
    }

    public function testPhpVersion()
    {
        // FYI this test runs with PHP7
        $this->assertSame('7.0.15', \PHP_VERSION);
    }

    // Some nonsensical example test using a mock that has methods with
    // scalar argument types and PHP7 return types.
    public function testBarUsesFoo()
    {
        $stubFoo = \Mockery::mock(Foo::class);
        $stubFoo->shouldReceive('isSomethingTrue')->with('faz')->andReturn(false);
        $this->assertFalse((new Bar($stubFoo))->useFooWith('faz'));
    }
}

Mockery是否可以“解决” PHPUnit 4.1.0无法正确处理PHP7类型提示的问题?匿名类比模拟有什么好处?以及如何将主composer.json文件中的PHPUnit版本更新为5.3.5(最新版本支持PHP7并具有公共模拟方法(Magento 2自己的测试要求))?现在还有更多问题……
Giel Berkers

更新了我的回答以回应您的问题@GielBerkers
Vinai

感谢您的出色回答。现在完全清楚了!我想我先去试一试Mockery。匿名类似乎让我不得不重新发明Mockery已经提供的很多东西。我首先想学习PHPUnit的基础知识,然后继续学习。我认为现在是时候了。
吉尔·伯克斯

大!享受探索大型图书馆Mockery的乐趣。当您使用它时,也许还可以查看hamcrest,一个断言库-它会自动与Mockery一起安装。
Vinai'2

3

现在,Magento 2支持下一个PHP版本:

"php": "~5.6.5|7.0.2|7.0.4|~7.0.6"

这意味着Magento Team编写的所有代码都可以在每个受支持的版本上使用。

因此,Magento团队不使用仅PHP 7功能。PHPUnit 5.6.1可以涵盖PHP 5.6功能。

编写自己的代码,您可以做所有想做的事情,并可以按自己喜欢的任何方式编写测试。但我认为由于违反要求,您将无法在Magento Marketplace上发布扩展程序。


实际上,PHP 5.6,PHP 7.0和PHP 7.1支持PHPUnit 5.7。PHP 5.3 – 5.6支持PHPUnit 4.8。因此,即使Magento 2支持PHP 5.6,它仍然可以升级到PHPUnit 5.7。
Vinai
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.