jsiefer/mage-mock

模拟 Magento 框架

0.3.0 2016-11-26 15:40 UTC

This package is not auto-updated.

Last update: 2024-09-26 00:29:49 UTC


README

鉴于您需要扩展许多类并在最初进行大量设置以初始化,为 Magento 类创建单元测试可能会很困难。

框架模拟器将帮助您模拟 Magento 环境。

这里单元测试的想法是...

  • ...应快速运行(<100ms)
  • ...不需要 Magento 安装或源

所有必要的 Magento 类都由 class-mocker 库即时生成。

设置

只需使用 composer 加载此项目

composer require jsiefer/mage-mock

创建一个 PHPUnit bootstrap.php 文件,并将 MagentMock 注册到 ClassMocker 并启用 ClassMocker。

<?php

use JSiefer\ClassMocker\ClassMocker;
use JSiefer\MageMock\MagentoMock;

$magentoFramework = new MagentoMock();

$classMocker = new ClassMocker();
// optional cache dir for generated classes
$classMocker->setGenerationDir('./var/generation');
$classMocker->mockFramework($magentoFramework);
$classMocker->enable();

还建议设置 ClassMocker 测试监听器,以便验证 ClassMock 对象断言。例如(例如,$test->expects($this->once())->method('test')

对于 MageMock 监听器也是如此,它将在每个测试后自动重置 Mage 类。

只需将监听器添加到您的 phpunit.xml 中

    <listeners>
        <listener class="JSiefer\ClassMocker\TestListener" />
        <listener class="JSiefer\MageMock\PHPUnit\TestListener" />
    </listeners>

工作原理

模拟的类

由 class-mocker 生成的所有类都实现了 PHPUnit MockObject 接口,这允许您在运行时更改或断言某些行为,就像使用标准 PHPUnit 模拟时那样熟悉(例如,$moock->expects($this->once())...)。

可以使用 traits 定义额外的固定行为,如简单的存根逻辑,适用于整个测试运行。

MageMock 已经提供了一些基本的存根逻辑。例如,对 Model 上的 save() 方法进行调用将触发 _beforeSave()_afterSave() 方法。

当在生成的模拟对象上调用方法时,会使用 traits 和注册到 Mocks,而不是直接调用 trait 方法,而是由所有生成的 Mock 的基类 BaseMock 处理。然后 BaseMock 将按以下顺序检查定义的方法:

  1. 检查任何模拟方法。

     $mock->expects($this->once())->method('save');
  2. 检查闭包函数。

     $mock->save = function() {
         return $this;
     }
  3. 检查 trait 方法。

    /**
     * Class Mage_Core_Model_Abstract
     *
     * @pattern Mage_Core_Model_Abstract
     * @sort 100
     */
    trait My_Mage_Core_Model_Abstract_Stub
    {
        /**
         * Save object data
         *
         * @return $this
         */
        public function save()
        {
         $this->_beforeSave();
         $this->_afterSave();
         $this->_afterSaveCommit();
        
         return $this;
        }
    }
    
    // Then in your bootstrap.php
    $magentoFramework = new MagentoMock();
    
    $classMocker = new ClassMocker();
    $classMocker->registerTrait(My_Mage_Core_Model_Abstract_Stub::class)
    $classMocker->setGenerationDir('./var/generation');
    $classMocker->mockFramework($magentoFramework);
    $classMocker->enable();
  4. 检查并调用魔法 trait 方法(例如,__call()

    上述 traits 可以使用 ___call() 方法来监听任何调用。

  5. 默认方法调用(返回 self)

    可以更改为返回 NULL 或抛出异常。

Mage 类问题

在 Magento 的单元测试期间,Mage 类是一个主要的挑战之一。对测试中的静态 Mage 类进行模拟行为并不直接。

为了解决这个问题,创建了两个 Mage 类。一个称为 MageFacade 类,它用于将 Mage 类的所有静态调用获取并委派给 MageClass 类,但 MageClass 不是静态的,也是一个为每个测试重新初始化的自动生成的模拟。

这样,我们不仅可以绑定 trait 存根到 Mage 类,还可以使用上述相同的方法在运行时更改 Mage 类的行为。

(见下面的 testMockingMage()

另一个问题是,没有可用的配置 XML,因此需要以半自动化的方式处理模型名称的解析,如 customer/session

可以将名称解析器设置到 MageFacade 类,它会尽力解析任何给定的名称。您可以在那里注册自己的命名空间,或创建自己的名称解析器。

(见下面的 testSingleton()

use Mage_Sales_Model_Quote as Quote;
use Mage_Catalog_Model_Product as Product;
use Mage_Customer_Model_Customer as Customer;

class MyTest extends JSiefer\MageMock\PHPUnit\TestCase
{
    /**
     * Test mocking mage
     *
     * @return void
     * @test
     */
    public function testMockingMage()
    {    
        // This is the Mage mock
        $mage = $this->getMage();
        $mage->expects($this->once())->method('getBaseUrl')->willReturn('foobar');
        
        $baseUrl = Mage::getBaseUrl(); // foobar
    }
    
    /**
     * Example for mocking a singleton
     *
     * @return void
     * @test
     */
    public function testSingleton()
    {
        $customer = new Customer();
        $customer->setFirstname('Joe');
    
        // Create customer session mock
        $session = $this->getSingleton('catalog/session');
        $session->expects($this->once())->method('getCustomer')->willReturn($customer);
        
        // Now you can use this session, only valid in this test
        $session = Mage::getSingleton('catalog/session');
    }
    
    /**
     * Creating model mocks on the fly using model factories
     *
     * Supose multiple Product models get initiated and you need them to return
     * a unique id by default.
     *
     * @return void
     * @test
     */
    public function testModelFactory()
    {
        $idCounter = 1;

        // Register a factory closure for creating class instances
        // Factories are registred by the full class name 
        $this->registerModelFactory(Product::class, function() use (&$idCounter) {
            $instance = new Product();
            $instance->expects($this->once())->method('getId')->will($this->returnValue($idCounter++));

            return $instance;
        });

        $productA = Mage::getModel('catalog/product');
        $productB = Mage::getModel('catalog/product');

        $this->assertNotSame($productA, $productB, "Both product should be two different instances");

        $this->assertEquals(1, $productA->getId());
        $this->assertEquals(2, $productB->getId());
    }

}

更多示例

假设您有以下模型

/**
 * Class Magemock_Sample_Model_Vehicle
 *
 * @method string getId()
 * @method string getName()
 * @method string getNumberOfDoors()
 * @method string getNumberOfSeats()
 * @method string getNumberOfWheels()
 * @method string getTopSpeed()
 */
class Magemock_Sample_Model_Vehicle extends Mage_Core_Model_Abstract
{
    /**
     * Initialize resources
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('sample/vehicle');
    }

    /**
     * @return $this
     */
    protected function _beforeSave()
    {
        if (!$this->getName()) {
            throw new DomainException("Vehicle must have a name");
        }

        return parent::_beforeSave();
    }

    /**
     * Is vehicle a bile
     *
     * @return bool
     */
    public function isBike()
    {
        return $this->getNumberOfWheels() == 2 &&
               $this->getNumberOfDoors() == 0;
    }

    /**
     * Load bike by name
     *
     * @param string $name
     *
     * @return $this
     */
    public function loadByName($name)
    {
        $this->load($name, 'name');
        return $this;
    }
}

现在我们来为这个类编写单元测试,记住你只想测试你的逻辑。父类的magento逻辑在此点并不相关。

然而,mage-mocker通过实现一些最重要的类和方法,如Varien_Object实现,来帮助你。

在运行测试时,所有Mage_*Varien_*类都会即时创建和扩展。它们将没有方法实现,但是它们都实现了正确的类层次和类常量。

例如:

  • Magemock_Sample_Model_Vehicle
  • 继承自:Mage_Core_Model_Abstract
  • 继承自:Varien_Object
  • 实现了:ArrayAccess

让我们看看上面示例的一个简单测试。

/**
 * Simple unit test for Vehicle model
 */
class VehicleTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Simple test to see that all classes
     * are initialized during testing without
     * the presents of the magento framework
     *
     * @test
     */
    public function testVehicleClassInstance()
    {
        $vehicle = new \Magemock_Sample_Model_Vehicle();

        $this->assertInstanceOf('Mage_Core_Model_Abstract', $vehicle);
        $this->assertInstanceOf('Varien_Object', $vehicle);
        $this->assertInstanceOf('ArrayAccess', $vehicle);

        // class const example
        $this->assertEquals(
            'COLLECTION_DATA',
            \Mage_Core_Model_Resource_Db_Collection_Abstract::CACHE_TAG
        );
    }

    /**
     * Test _construct() method
     *
     * You can call protected methods by using the helper 
     * function __callProtectedMethod($name, array $args = [])
     *
     * @return void
     * @test
     */
    public function validateConstructMethod()
    {
        /** @var \Magemock_Sample_Model_Vehicle|\JSiefer\ClassMocker\Mock\BaseMock $product */
        $product = new \Magemock_Sample_Model_Vehicle();
        $product->expects($this->once())->method('_init')->with('sample/vehicle');

        // call protected method _construct
        $product->__callProtectedMethod('_construct');
    }

    /**
     * Test before vehicle save validation
     *
     * @return void
     * @test
     */
    public function testSaveValidation()
    {
        $vehicle = new \Magemock_Sample_Model_Vehicle();
        $vehicle->setName("foobar");
        $vehicle->save();

        $this->assertEquals('foobar', $vehicle->getName());

        try {
            $vehicle = new \Magemock_Sample_Model_Vehicle();
            $vehicle->save();

            $this->fail("Expected validation error for missing name");
        }
        catch(\Exception $e) {
            $this->assertEquals(
                'Vehicle must have a name',
                $e->getMessage(),
                'Caught wrong exception'
            );
        }
    }

    /**
     * Test isBike() method
     *
     * @return void
     * @test
     */
    public function testIsBikeMethod()
    {
        $vehicle = new \Magemock_Sample_Model_Vehicle();

        $this->assertFalse(
            $vehicle->isBike(),
            'Vehicle should not be a bike by default'
        );

        $vehicle->setNumberOfWheels(2);
        $vehicle->setNumberOfDoors(0);

        $this->assertTrue(
            $vehicle->isBike(),
            'Vehicle should be a bike if it has two wheels and no doors'
        );
    }

    /**
     * Test load by name method
     *
     * Since the Magemock_Sample_Model_Vehicle class extends a class-mocker generated
     * class all PHPUnitObjectMock methods are available
     *
     * @return void
     * @test
     */
    public function testLoadByName()
    {
        $vehicle = new \Magemock_Sample_Model_Vehicle();
        $vehicle->expects($this->once())
                ->method('load')
                ->with($this->equalTo('foobar'), $this->equalTo('name'));

        $result = $vehicle->loadByName('foobar');

        $this->assertSame($vehicle, $result, 'loadByName should return $this');
    }
}

由于这是一个仅关注你类的单元测试,不需要进行magento初始化过程,测试可以在几毫秒内运行。

注意

这仍然是一个早期版本和概念验证。还需要测试这种方法是否有用。如果你有任何想法、反馈或问题,请告诉我。