jsiefer / mage-mock
模拟 Magento 框架
Requires
- jsiefer/class-mocker: ~0.3
Requires (Dev)
- firegento/magento: v1.9.2.4
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 将按以下顺序检查定义的方法:
-
检查任何模拟方法。
$mock->expects($this->once())->method('save');
-
检查闭包函数。
$mock->save = function() { return $this; }
-
检查 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();
-
检查并调用魔法 trait 方法(例如,
__call())上述 traits 可以使用
___call()方法来监听任何调用。 -
默认方法调用(返回 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初始化过程,测试可以在几毫秒内运行。
注意
这仍然是一个早期版本和概念验证。还需要测试这种方法是否有用。如果你有任何想法、反馈或问题,请告诉我。