单元测试持久层 - Symfony

2022-01-25 00:00:00 php symfony phpunit

I want to test persistence in Symfony2. I wonder whether it is better mock entities and provide to entity manager or whether it is better mock entity menager and pass entity to manager ?

I first option but entity manager throw exception than object is not entity doctrine. How test persistence symfony in PHPUNIT?

解决方案

Rather than writing unit tests you should write integration tests for the persistence layer.

There's a rule in unit testing "Don't mock what you don't own".

You don't own Doctrine classes nor interfaces, and you can never be sure that assumptions you made to the interfaces you mocked are true. Even if they're true at the time you write the test, you cannot be sure that Doctrine's behaviour didn't change over time.

Whenever you use 3rd party code you should write an integration test for whatever uses it. Integration test will actually call the database and make sure that doctrine works the way you think it works.

That's why it's good to decouple from 3rd party stuff.

In case of doctrine it's very easy. One of the things you could do is to introduce an interface for each of your repositories.

interface ArticleRepository
{
    /**
     * @param int $id
     *
     * @return Article
     *
     * @throws ArticleNotFoundException
     */
    public function findById($id);
}

You can easily mock or stub such an interface:

class MyServiceTest extends PHPUnit_Framework_Test_Case
{
    public function test_it_does_something()
    {
        $article = new Article();
        $repository = $this->prophesize(ArticleRepository::class);
        $repository->findById(13)->willReturn($article);

        $myService = new MyService($repository->reveal());
        $myService->doSomething();

        // ...
    }
}

The interface will be implemented with doctrine:

use DoctrineORMEntityRepository;

class DoctrineArticleRepository extends EntityRepository implement ArticleRepository
{
    public function findById($id)
    {
        // call doctrine here
    }
}

The implementation is what you're gonna write integration tests for. In your integration test for the repository you'll actually call the database:

use SymfonyBundleFrameworkBundleTestKernelTestCase;

class ArticleTest extends KernelTestCase
{
    private $em;
    private $repository;

    protected function setUp()
    {
        self::bootKernel();

        $this->em = static::$kernel->getContainer()
            ->get('doctrine')
            ->getManager();
        $this->repository = $this->em->getRepository(Article::class);
    }

    public function test_it_finds_an_article()
    {
         $this->createArticle(13);

         $article = $this->repository->findById(13);

         $this->assertInstanceOf(Article::class, $article);
         $this->assertSame(13, $article->getId());
    }

    /**
     * @expectedException ArticleNotFoundException
     */
    public function test_it_throws_an_exception_if_article_is_not_found()
    {
        $this->repository->findById(42);
    }

    protected function tearDown()
    {
        parent::tearDown();

        $this->em->close();
    }

    private function createArticle($id)
    {
        $article = new Article($id);
        $this->em->persist($article);
        $this->em->flush();
        // clear is important, otherwise you might get objects stored by doctrine in memory
        $this->em->clear();
    }
}

Everywhere else, you'll use the interface which will be stubbed (or mocked). Everywhere else you won't hit the database. This makes your tests fast.

Entities in your stubs can be simply created or stubbed. Most of the time I create entity objects rather then mock them.

相关文章