happyr / service-mocking
简化在构建容器中模拟服务的操作
0.3.1
2022-07-07 05:29 UTC
Requires
- php: >=7.2
- friendsofphp/proxy-manager-lts: ^1.0
- symfony/config: ^5.4 || ^6.0
- symfony/dependency-injection: ^5.4 || ^6.0
- symfony/http-kernel: ^5.4 || ^6.0
Requires (Dev)
- nyholm/symfony-bundle-test: ^2.0
- symfony/phpunit-bridge: ^6.0
README
您希望测试尽可能快地运行,因此您只需构建一次容器,然后让所有测试在该构建的容器上运行。这真是太好了!
然而,当您的服务容器构建时,它是不可变的。当您希望在功能测试期间模拟服务时,这会引发问题。您无法更改服务容器中的对象。
使用此包,您可以标记一些服务为“可模拟”,这将允许您为该服务中的方法定义新的自定义行为。如果没有定义自定义行为,服务将按正常工作。
安装
composer require --dev happyr/service-mocking
请确保只为您的测试环境启用此包
// config/bundles.php <?php return [ // ... Happyr\ServiceMocking\HappyrServiceMockingBundle::class => ['test' => true], ];
配置服务
您需要告诉包您想要模拟哪些服务。这可以通过“happyr_service_mock
”服务标签或通过定义服务ID列表来实现
PHP配置(Symfony 5.3)
<?php // config/packages/test/happyr_service_mocking.php use Symfony\Config\HappyrServiceMockingConfig; return static function (HappyrServiceMockingConfig $config) { $config->services([ \App\AcmeApiClient::class \App\Some\OtherService::class ]); };
Yaml配置
# config/packages/test/happyr_service_mocking.yaml happyr_service_mocking: services: - 'App\AcmeApiClient' - 'App\Some\OtherService'
使用方法
use App\AcmeApiClient; use App\Some\OtherService; use Happyr\ServiceMocking\ServiceMock; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class MyTest extends WebTestCase { public function testFoo() { // ... $apiClient = self::getContainer()->get(AcmeApiClient::class); // On Symfony < 5.3 // $apiClient = self::$container->get(AcmeApiClient::class); // For all calls to $apiClient->show() ServiceMock::all($apiClient, 'show', function ($id) { // $id here is the same that is passed to $apiClient->show('123') return ['id'=>$id, 'name'=>'Foobar']; }); // For only the next call to $apiClient->delete() ServiceMock::next($apiClient, 'delete', function () { return true; }); // This will queue a new callable for $apiClient->delete() ServiceMock::next($apiClient, 'delete', function () { throw new \InvalidArgument('Item cannot be deleted again'); }); $mock = // create a PHPUnit mock or any other mock you want. ServiceMock::swap(self::getContainer()->get(OtherService::class), $mock); // ... self::$client->request(...); } protected function tearDown(): void { // To make sure we don't affect other tests ServiceMock::resetAll(); // You can include the RestoreServiceContainer trait to automatically reset services } }
内部
那么这个魔法是如何工作的呢?
当容器构建时,会根据您的服务定义生成一个新的代理类。代理类的行为和原始类一样。但在每次方法调用时,它会检查ProxyDefinition
是否已添加自定义行为。
通过静态属性的帮助,即使Kernel重启,ProxyDefinition
也会被记住。
限制
如果您有两个不同的PHP进程,即您使用Panther、Selenium等运行测试,则此技巧将不起作用。
如果您的服务是final的,我们也不能创建代理。
我们只能模拟对服务的直接访问。间接方法调用不会被模拟。例如
class MyService { public function foo() { return $this->bar(); } public function bar() { return 'original'; } }
如果我们将MyService::bar()
模拟为返回“mocked
”。当您调用MyService::foo()
时,您仍然会得到“original
”。解决方法是模拟MyService::foo()
。