【问题标题】:Symfony2 Unit Testing a serviceSymfony2 单元测试服务
【发布时间】:2015-04-24 07:55:14
【问题描述】:

我对 symfony 还是很陌生,真的很喜欢它。

我正处于创建和设置服务的阶段,服务本身使用 2 个依赖项:

  • 返回 json 数据的数据 API(这是一个单独的库, 我已经实现为服务并附带了自己的单元测试)。
  • Doctrine Entity Manager。

服务使用api提取所需的数据,然后循环遍历数据并检查数据是否已经存在,如果存在则更新现有实体并将其持久化,否则它会创建一个新实体分配数据并坚持下去。

我现在需要为此编写一个单元测试,我没有只使用 symfony2 教程中的 PHPUnit,这些教程正在测试来自控制器的响应。

我该如何为此服务编写单元测试? 特别是模拟我通常会从 api 中提取的数据。 然后检查该条目是否需要更新或创建?

一个代码示例将非常有帮助,因此我可以将其用作模板来为我创建的其他类似服务创建测试。

这是我要测试的服务:

<?php

namespace FantasyPro\DataBundle\DataManager;

use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;

class StadiumParser {
    /**
     * @var EntityManager $em
     */
    private  $em;
    /**
     * @var Client $client
     */
    private $client;

    public function __construct( EntityManager $em, Client $client) {
        $this->em = $em;
        $this->client = $client;
    }

    /**
     * @return array
     */
    public Function parseData(){

        //var_dump($this);
        $stadiumData = $this->client->Stadiums();
        //var_dump($stadiumData);
        //get the Repo
        $repo = $this->em->getRepository('DataBundle:Stadium');

        $log = array();

        foreach ($stadiumData as $stadium) {
            // Get the current stadium in the list from the database
            $criteria = array( 'stadiumID' => $stadium['StadiumID'] );
            $currentStadium = $repo->FindOneBy( $criteria );

            if ( ! $currentStadium) {
                $currentStadium = new Stadium(); //no stadium with the StadiumID exists so create a new stadium

                $logData = [
                    'action'   => 'Added Stadium',
                    'itemID'   => $stadium['StadiumID'],
                    'itemName' => $stadium['Name']
                ];
                $log[] = $logData;
            } else {
                $logData = [
                    'action'   => 'Updated Stadium',
                    'itemID'   => $stadium['StadiumID'],
                    'itemName' => $stadium['Name']
                ];
                $log[] = $logData;
            }
            $currentStadium->setStadiumID( $stadium['StadiumID'] );
            $currentStadium->setName( $stadium['Name'] );
            $currentStadium->setCity( $stadium['City'] );
            $currentStadium->setState( $stadium['State'] );
            $currentStadium->setCountry( $stadium['Country'] );
            $currentStadium->setCapacity( $stadium['Capacity'] );
            $currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
            $this->em->persist( $currentStadium );
        }
        $this->em->flush();
        return $log;
    }
}

****** 更新 ******* 在阅读了 ilpaijin 的回答之后。

我已经简化了服务,所以它不再返回日志,我最初有这个,所以我可以通过将日志发送到我的控制器中的树枝模板来检查添加的内容,我最终计划让它运行为一个命令,以便我可以通过 cron 作业运行它,因此不需要日志位。

我现在在我的构造中设置实体,因为我不知道如何将实体作为注入的依赖项传递。 现在使用createNewStadium() 方法抓取一个新实体。

更新的服务:

namespace FantasyPro\DataBundle\DataManager;

use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;

class StadiumParser {
    /**
     * @var EntityManager $em
     */
    private  $em;
    /**
     * @var Client $client
     */
    private $client;
    /**
     * @var Stadium Stadium
     */
    private $stadium;

    public function __construct( EntityManager $em, Client $client) {
        $this->em = $em;
        $this->client = $client;
    }

    /**
     * Gets a list of stadiums using $this->client->Stadiums.
     * loops through returned stadiums and persists them
     * when loop has finished flush them to the db
     */
    public Function parseData(){
        $data = $this->client->Stadiums();
        //get the Repo
        $repo = $this->em->getRepository('DataBundle:Stadium');

        foreach ($data as $item) {
            // Get the current stadium in the list
            $criteria = array( 'stadiumID' => $item['StadiumID'] );
            $currentStadium = $repo->FindOneBy( $criteria );

            if ( ! $currentStadium) {
                $currentStadium = $this->createNewStadium; //no stadium with the StadiumID use the new stadium entity
            }
            $currentStadium->setStadiumID( $item['StadiumID'] );
            $currentStadium->setName( $item['Name'] );
            $currentStadium->setCity( $item['City'] );
            $currentStadium->setState( $item['State'] );
            $currentStadium->setCountry( $item['Country'] );
            $currentStadium->setCapacity( $item['Capacity'] );
            $currentStadium->setPlayingSurface( $item['PlayingSurface'] );
            $this->em->persist( $currentStadium );
        }
        $this->em->flush();
    }

    // Adding this new method gives you the ability to mock this dependency  when testing  
    private function createNewStadium()
    {
        return new Stadium();
    }
}

【问题讨论】:

  • Services 只不过是一个简单的类,你应该和一个简单的 PHP 类有同样的方法。 class StadiumParserTest extends PHPUnit_Framework_TestCase {} 模拟 em,客户端并测试您的 parseData。
  • 这是我不知道该怎么做的嘲弄,我应该创建静态 JSON 文件并传递这些文件而不是从 api 获取数据吗?来自 api 的数据可能会随着时间的推移而变化,尽管它返回的数据始终具有相同的类型,但值可能会有所不同。

标签: symfony testing service phpunit


【解决方案1】:

您基本上需要的是使用所谓的“Test doubles”对服务进行单元测试。

这意味着您应该模拟您的服务所具有的依赖项,这样您就可以仅单独测试服务,而无需真正依赖依赖项,而只能使用它们的模拟版本,具有硬编码的值或行为。

基于您的实际实现的真实示例是不可能的,因为您有紧密耦合的部门作为$currentStadium = new Stadium();。您应该在构造函数中或通过 getter/setter 传递这样的 deps,以便能够在单元测试时模拟它。

一旦完成,一个非常具有指示性的示例将是:

// class StadiumParser revisited and simplified
class StadiumParser 
{
    private $client;

    public function __construct(Client $client) 
    {
        $this->client = $client;
    }

    public function parseData()
    {
        $stadiumData = $this->client->Stadiums();

        // do something with the repo

        $log = array();

        foreach ($stadiumData as $stadium) {
            $logData = [
                'action'   => 'Added Stadium',
                'itemID'   => $stadium['StadiumID'],
                'itemName' => $stadium['Name']
            ];
            $log[] = $logData;
        } 

        // do something else with Doctrine

        return $log;
    }
}

和测试

// StadiumParser Unit Test
class StadiumParserTest extends PHPUnit_Framework_TestCase 
{
    public function testItParseDataAndReturnTheLog()
    {
        $client = $this->getMock('FantasyDataAPI\Client');

        // since you class is returning a log array, we mock it here
        $expectedLog = array(
            array(
                'action'   => 'Added Stadium',
                'itemID'   => $stadium['StadiumID'],
                'itemName' => $stadium['Name']
            )
        );

        // this is the mocked or test double part. 
        // We only need this method return something without really calling it
        // So we mock it and we hardcode the expected return value
        $stadiumData = array(
            array(
                "StadiumID" => 1,
                "Name" => "aStadiumName"
            )
        );

        $client->expects($this->once())
            ->method('Stadiums')
            ->will($this->returnValue($stadiumData));

        $stadiumParser = new StadiumParser($client);

        $this->assertEquals($expectedLog, $stadiumParser->parseData());
    }
}

我自愿省略了 EntityManager 部分,因为我想你应该看看与 how to unit test code interacting with the database 相关的 Symfony 文档


-----EDIT2-----

是的,他是对的,你不应该。想到的一种可能的方法是在受保护/私有方法中提取实体的创建。类似的东西:

// class StadiumParser
public Function parseData()
{
    ...

    foreach ($stadiumData as $stadium) {
        ...

        if ( ! $currentStadium) {
            $currentStadium = $this->createNewStadium();
        ...
}

// Adding this new method gives you the ability to mock this dependency when testing  
private function createNewStadium()
{
    return new Stadium();
}

-----EDIT3-----

我想建议您另一种方法。如果Stadium 实体在不同的服务或相同的不同部分中需要,这应该是一个更好的选择。我提议的是Builder 模式,但Factory 也可以是这里的一个选项。浏览一下它们的差异。 正如您所看到的,从方法中提取了一些代码,更好地在类之间分配了责任,并使您和您的队友更清晰、更容易阅读。而且您已经知道如何在测试时模拟它。

class StadiumParser 
{
    private  $stadiumBuilder;
    ...

    public function __construct( StadiumBuilder $builder, ...) {
        $this->stadiumBuilder = $stadiumBuilder;
        ...
    }

    public Function parseData()
    {
        ...

        foreach ($stadiumData as $stadium) {
            ...
            $currentStadium = $repo->FindOneBy( $criteria );

            if ( ! $currentStadium) {
                $currentStadium = $this->stadiumBuilder->build($currentStadium, $stadium);
            }

            $this->em->persist($currentStadium);
            ...

在某个地方,您有这个返回 Stadium 实例的新 Builder。这样,您的 StadiumParser 服务不再与实体耦合,但 StadiumBuilder 就是它。逻辑是这样的:

// StadiumBuilder class

namespace ???

use FantasyPro\DataBundle\Entity\Stadium;

class StadiumBuilder 
{
    // depending on the needs but this should also has a different name
    // like buildBasic or buildFull or buildBlaBlaBla or buildTest 
    public function build($currentStadium = null, $stadium)
    {
        if (!$currentStadium) {
            $currentStadium = new Stadium();
        }

        $currentStadium->setStadiumID( $stadium['StadiumID'] );
        $currentStadium->setName( $stadium['Name'] );
        $currentStadium->setCity( $stadium['City'] );
        $currentStadium->setState( $stadium['State'] );
        $currentStadium->setCountry( $stadium['Country'] );
        $currentStadium->setCapacity( $stadium['Capacity'] );
        $currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );

        return $currentStadium; 
    }
}

【讨论】:

  • 感谢您的回答,这是一个很大的帮助,我被告知注入实体是错误的,不是正确的做法,尽管没有说明为什么,我会在单元测试,看看我是怎么做的。
  • 不客气。如果您对此主题有任何其他问题,请告诉我,我在这里。
  • 是否可以将实体作为依赖项注入?我是否必须将实体注册为服务然后注入?或者这是一个很大的不?在构建此服务时,我最初想将实体作为依赖项传递,但无法弄清楚我如何尝试使用 '::FantasyPro/DataBundle/Entity/Stadium' 但有人告诉我我不应该这样做。
  • 我更新了我的问题,你更新了你的问题!,我使用的方法(通过构造函数方法)是否可以,或者最好使用你的createNewStadium() 方法来实现它?
  • 刚刚添加了 EDIT3 作为要考虑的新选项
猜你喜欢
  • 1970-01-01
  • 2013-10-17
  • 2012-11-27
  • 2011-02-03
  • 2019-05-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多