rwslinkman/metronome

此包已被废弃且不再维护。作者建议使用 phpunit/phpunit 包。

Symfony 3(PHP)应用测试工具

3.1.3 2022-07-03 16:49 UTC

This package is auto-updated.

Last update: 2022-07-17 13:27:05 UTC


README

该项目已过时,不应再使用。
请阅读 DEPRECATION NOTICE 获取更多信息。

Metronome 是为 Symfony 3 和 Symfony 4(PHP)应用提供的轻量级测试工具。
它提供了一个稳定的基座,便于进行模拟和注入 Symfony 容器。
让 Metronome 帮助您编排手头的 Symfony!

Metronome 致力于使 Symfony 项目的功能测试更加容易。
它创建一个自定义的 KernelContainer,并注入设置完整 Symfony 环境所需的一切。
您的应用将自动使用 Symfony 的 WebTestCaseKernelBrowser 客户端加载。

Metronome 提供了几个构建器以帮助您进行测试

  • MetronomeBuilder
  • MetronomeDoctrineMockBuilder
  • MetronomeFileSystemBuilder

使用 Metronome,您可以

  • 构建一个 MetronomeEnvironment,向测试 Controller 类发送 GET 和 POST 请求
  • 构建一个模拟的 EntityManager 以测试具有数据库交互的类
  • 构建一个模拟的 ReferenceRepository 以测试您的 Fixture
  • 构建一个模拟的 ManagerRegistry 以测试您的 EntityRepository
  • 注入 MetronomeLoginData 以绕过您的 GuardAuthenticator 保护
  • 使用 MetronomeFormDataBuilderMetronomeEntityFormDataBuilder 模拟 Symfony Forms
  • 验证 Symfony FlashBag 的内容

DEPRECATION NOTICE

该项目不再处于活跃开发状态。
最初,当 Symfony 没有完全支持测试的依赖注入时,该项目被创建。
此外,创建可用于 Symfony 内核的模拟也非常困难。
当时测试 Symfony 控制器相当困难。现在不再是这种情况了。

多年来,Symfony 和 PHPUnit 在这些方面都取得了巨大的进步。
因此,Metronome 正在变得越来越过时。
它对项目来说更多的是负担而不是好处。
使用改进的依赖注入系统,无需 Metronome 就可以很好地测试 Symfony 控制器。

如果您想从 Metronome 迁移,请查看 PHPUnit 中的模拟。
这些模拟可以轻松地传递给测试时的控制器构造函数。

安装

您可以使用 composer 通过 Packagist 安装 Metronome。

composer require-dev rwslinkman/metronome

由于 Metronome 是测试工具,因此在生产环境中不需要。
最新版本是 3.0.0
旧版本可能适用于 Symfony 3 项目,但这可能很困难,且不受支持。
对于前沿开发,请指向 dev-developdev-master 版本。

用法

Metronome 与 PHPUnit 和 Symfony 的 WebTestCase 结合使用,后者扩展了 PHPUnit 的 TestCase

MetronomeBuilder

使用 MetronomeBuilder,您可以创建一个完整的应用环境。
到数据库的连接和 EntityManager 会自动模拟。
您可以注入自己的内核,或者使用现成的MetronomeKernel。
设置完成后,注入您需要的服务、存储库和其他对象。调用build函数会返回一个MetronomeEnvironment,它允许您向应用程序发送GETPOSTPUT请求。

为您的应用程序创建一个没有模拟的环境(排除Doctrine)

$clientBuilder = new MetronomeTestClientBuilder();
$builder = new MetronomeBuilder($clientBuilder->build());
$environment = $builder->build();

$response = $environment->get("/");
$this->assertEquals(200, $response->getStatusCode());

测试控制器

使用MetronomeBuilder设置一个MetronomeEnvironment
最基础的测试会从Symfony\HttpFoundation返回一个Response
确保您的测试扩展了WebTestCase,以便使用Symfony的Client进行HTTP请求。

class IndexControllerTest extends WebTestCase
{
    /** @var MetronomeBuilder */
    private $testEnvBuilder;
    
    public function setUp() {
        $clientBuilder = new MetronomeTestClientBuilder();
        $clientBulder->controller(IndexController::class);
        $this->testEnvBuilder = new MetronomeBuilder($clientBuilder->build());
        $this->testEnvBuilder->setupController(IndexController::class);
    }

    public function test_givenApp_whenGetIndex_thenShouldReturnOK() {
        /** @var MetronomeEnvironment */
        $environment = $builder->build();
        
        /** @var Response */
        $response = $environment->get("/");
        
        $this->assertEquals(200, $response->getStatusCode());
    }
}

如果您在Symfony的services.yaml中配置了自定义服务,那么您可以在测试中模拟这些服务。
当测试Controllers时,这很方便。您可以使用MetronomeDynamicMockBuilder轻松地模拟类。

class IndexControllerTest extends WebTestCase
{
    /** @var MetronomeBuilder */
    private $testEnvBuilder;
    
    public function setUp() {
        $clientBuilder = new MetronomeTestClientBuilder();
        $clientBulder->controller(IndexController::class);
        $this->testEnvBuilder = new MetronomeBuilder($clientBuilder->build());
        $this->testEnvBuilder->setupController(IndexController::class);
    }
    
    public function test_givenApp_whenGetIndex_andEmptyList_thenShouldReturnOK() {
        $mockBuilder = new MetronomeDynamicMockBuilder(UserService::class);
        $mockBuilder->method("getUsers", array());
        $this->testEnvBuilder->injectObject("myapp.user_service", $mockBuilder->build());
        
        /** @var MetronomeEnvironment */
        $testEnv = $this->testEnvBuilder->build();

        /** @var Response */
        $result = $testEnv->get("/");
        
        $this->assertEquals(200, $result->getStatusCode());
    }
}

对于您作为服务注入的更多静态组件,您可以使用ServiceInjector返回基本值。
可选地,添加一些setter来设置模拟函数的期望结果。
MetronomeBuilder有一个特殊的injectService()函数来接受这类注入。

class ProductServiceInjector implements ServiceInjector { 
    private $loadAllProducts;

    /**
     * @return array Key => Value array of methods to mock and their respective results
     */
    public function inject()
    {
        return array(
            "loadAllProducts" => $this->loadAddProducts,
        );
    }

    /**
     * @return string The service name as defined in config.yml
     */
    public function serviceName()
    {
        return "rwslinkman.products";
    }

    /**
     * @return string Full namespace for the service to mock
     */
    public function serviceClass()
    {
        return '\rwslinkman\Service\ProductService';
    }
}

参数和定义

在测试Controllers时,您需要处理由Symfony注入的构造函数参数。
Metronome提供了MetronomeArgument,当Symfony需要时,它会将模拟目录注入到Controller构造函数中。
提供参数名称和您想注入的服务,然后在您的MetronomeBuilder上调用setupController。Metronome提供了一些预制的参数。

$clientBuilder = new MetronomeTestClientBuilder();
$clientBuilder->controller(ProjectController::class);
$this->builder = new MetronomeBuilder($clientBuilder->build());
$this->builder->setupController(ProjectController::class, array(
    new MetronomeServiceArgument("projectsService", "my_app.projects_service"),
    new MetronomeFormFactoryArgument("formFactory"),
    new MetronomeSessionArgument("session")
));

注意:在构建MetronomeEnvironment时必须将服务注入到容器中。

Symfony可能希望在调用控制器之前加载系统服务。
在这种情况下,向测试客户端添加一个MetronomeDefinition

$clientBuilder = new MetronomeTestClientBuilder();
$clientBuilder->controller(ProjectController::class);
$clientBuilder->addDefinition(new MetronomeDefinition(TokenStorage::class, TokenStorageInterface::class));
$clientBuilder->addDefinition(new MetronomeDefinition(AuthenticationUtils::class));

模拟数据库和EntityManager

使用Doctrine EntityManager的类可以使用Metronome进行测试。
这些通常是您的Symfony应用程序中的服务。
您可以将RepoInjector类注入以模拟您的EntityRepository

$metronomeBuilder = new MetronomeDoctrineMockBuilder();
$metronomeBuilder->injectRepo(new ProductRepoInjector());
$entityManagerMock = $metronomeBuilder->buildEntityManager(ProductRepository::class);

$service = new ProductService($this->entityManager);

RepoInjectorServiceInjector非常相似。
它围绕单个DoctrineEntity

class ProductRepoInjector implements RepoInjector {
    private $findAll;
    
    /**
     * @return array Key => Value array of methods to mock and their respective results
     */
    public function inject()
    {
        return array(
            "findAll" => $this->findAll,
        );
    }

    /**
     * @return mixed Acts as an identifier for the repository
     */
    public function repositoryName()
    {
        return ProductRepository::class;
    }

    /**
     * @return string Full namespace for the repository to mock
     */
    public function repositoryClass()
    {
        return '\rwslinkman\Repository\ProductRepository';
    }
}

测试固定数据

固定数据是Doctrine FixtureBundle的一部分。
Metronome可以用来验证一些Fixture的行为。

use PHPUnit\Framework\TestCase;

class MyFixtureTest extends TestCase
{
    public function test_givenFixture_whenLoad_shouldAlwaysPersist() {
        $envBuilder = new MetronomeDoctrineMockBuilder();
        $mockEm = $envBuilder->buildEntityManager();

        $fixture = new MyFixture();
        $fixture->load($mockEm);

        $mockEm->shouldHaveReceived("flush");
    }
}

绕过Guard身份验证系统

在测试控制器时,您可以通过将MetronomeLoginData注入到环境构建器中来绕过您的GuardAuthenticator
根据Symfony文档,认证器在您的services.yaml中定义。
此服务标识符用于创建MetronomeLoginData。使用此功能在测试期间绕过在security.yaml中配置的防火墙。

使用requiresLogin函数不是强制性的。如果您想测试您的防火墙,请省略调用。

class AdminControllerTest extends WebTestCase
{
    /** @var MetronomeBuilder */
    private $testEnvBuilder;
    /** @var */ MetronomeLoginData */
    private $loginData;
    
    public function setUp() {
        $clientBuilder = new MetronomeTestClientBuilder();
        $clientBulder->controller(AdminController::class);
        $this->testEnvBuilder = new MetronomeBuilder($clientBuilder->build());
        $this->testEnvBuilder->setupController(AdminController::class);
        
        $myUser = new MyUser(); // implements UserInterface
        $this->loginData = new MetronomeLoginData($myUser, "rwslinkman.my_authenticator");
    }
    
    public function test_givenApp_whenGetIndex_andEmptyProductList_thenShouldReturnOK() {
        // Add this line to actually use the login data. 
        $this->testEnvBulder->requiresLogin($loginData);
        
        /** @var MetronomeEnvironment */
        $testEnv = $this->testEnvBuilder->build();

        /** @var Response */
        $result = $testEnv->get("/admin"); // A route that is protected by the firewall in security.yaml
        
        $this->assertEquals(200, $result->getStatusCode());
    }
}

在控制器中测试表单

大多数Symfony网站在它们的控制器中都有表单,这些表单可以很容易地用Metronome进行测试。
Metronome提供了两个构建器,它们构建一个MetronomeFormData对象。
这些数据可以注入到MetronomeBuilder中,以模拟一个表单。

如果您将正在修改的对象的实例提供给Symfony FormBuilder,则提交有效表单时将直接更新。
您可以使用MetronomeEntityFormDataBuilder来模拟这个更新的对象。

$entity = new MyEntity();
$entityFormBuilder = new MetronomeEntityFormDataBuilder();
$entityFormBuilder
    ->formData($doctrineEntity)
    ->isValid(true);
$formData = $$entityFormBuilder->build();

如果您有更简单的表单,您可以直接使用输入数据,那么您可以使用MetronomeFormDataBuilder
它允许直接将值注入到表单字段中。

// Simple forms
$formBuilder = new MetronomeFormDataBuilder();
$formBuilder
    ->isValid(true)
    ->formData("form_field_address", "some address")
    ->formData("form_field_zipcode", "123456");
$formData = $formBuilder->build();

通过将其注入到MetronomeBuilder,可以轻松使用构建的表单数据。

$clientBuilder = new MetronomeTestClientBuilder();
$testEnvBuilder = new MetronomeBuilder($clientBuilder->build());
$envBuilder->injectForm($formData);

$testEnv = $this->envBuilder->build();
$testEnv->post("/register");

您可以使用injectForm多次。
FormFactory模拟将按注入顺序返回表单。
在某些情况下,您可能想编写针对第二个表单的特定测试。要跳过第一个表单,您可以在实际的MetronomeFormData之前注入MetronomeNonSubmittedFormMetronomeInvalidForm

请注意,此示例使用CSS选择器,需要symfony/css-selector依赖项。

验证FlashBag数据

用户会话中的FlashBag可以成为您网站的便捷工具。
Metronome允许您通过MetronomeEnvironment访问FlashBag
这可以帮助您更好地验证GET或POST请求的结果。

在下面的示例中,假设页面没有要显示的日志消息,并在闪存消息中报告这一点。

$clientBuilder = new MetronomeTestClientBuilder();
$testEnvBuilder = new MetronomeBuilder($clientBuilder->build());
$testEnv = $testEnvBuilder->build();

$testEnv->get("/admin/logs");

$flash = $testEnv->getFlashBag();
$this->assertNotEmpty($flash);

getFlashBag函数返回一个关联数组,其中包含每个键的条目。
每个键条目也是一个数组,包含与该键关联的闪存消息。