Les tests unitaires (seconde partie)

J’ai déjà fait un article sur les tests unitaires, mais il s’agissait davantage d’une introduction. A présent, j’aimerais entrer dans le vif du sujet en proposant des cas concrets de tests unitaires avec PHPUnit. Ils seront liés à Symfony2/3 (entités, services, formulaire)

Des tests simples

Commençons par une entité simple

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Media
 *
 * @ORM\Table(name="media")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\MediaRepository")
 */
class Media
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="url", type="string", length=255)
     * @Assert\NotBlank()
     * @Assert\Url()
     */
    private $url;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     * @Assert\NotBlank()
     */
    private $title;

    /**
     * @var float
     *
     * @ORM\Column(name="average", type="float", nullable=true)
     */
    private $average;

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(
     *     targetEntity="AppBundle\Entity\Vote",
     *     mappedBy="media",
     *     cascade={"persist"}
     * )
     */
    private $votes;

    
    /**
     * Constructor
     */
    public function __construct()
    {
        $this->votes = new \Doctrine\Common\Collections\ArrayCollection();
    }

    /**
     * Add vote
     *
     * @param \AppBundle\Entity\Vote $vote
     *
     * @return Media
     */
    public function addVote(\AppBundle\Entity\Vote $vote)
    {
        $vote->setMedia($this);
        $this->votes[] = $vote;

        return $this;
    }

    public function getNewAverageAfterVote()
    {
        if (0 < $count = count($this->votes)) {
            $total = 0;

            foreach ($this->votes as $vote) {
                $total += $vote->getScore();
            }

            $this->average = $total / $count;
        }
    }

    /**
     * Remove vote
     *
     * @param \AppBundle\Entity\Vote $vote
     */
    public function removeVote(\AppBundle\Entity\Vote $vote)
    {
        $this->votes->removeElement($vote);
    }

    /**
     * Get votes
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getVotes()
    {
        return $this->votes;
    }

    /**
     * Average score formatted for display
     * @return string
     */
    public function getDisplayedAverage()
    {
        return (null === $this->average) ? '-' : sprintf('%.1f', $this->average);
    }

    public function hasUserAlreadyVoted(User $user)
    {
        foreach ($this->votes as $vote) {
            if ($vote->getUser() === $user) {
                return true;
            }
        }

        return false;
    }
}

Ici, notre entité Media a des votes. On peut imaginer un système de vidéos, d’images, etc. pour lesquelles les internautes pourront voter. Notre premier test, simple, va porter sur la méthode getNewAverageAfterVoteIfMediaHasVotes.

    public function getNewAverageAfterVote()
    {
        if (0 < $count = count($this->votes)) {
            $total = 0;

            foreach ($this->votes as $vote) {
                $total += $vote->getScore();
            }

            $this->average = $total / $count;
        }
    }

Le but va être de tester deux choses:

  • si le média a déjà des votes, la moyenne des notes est correcte
  • si le média n’a aucun vote, la moyenne des notes est égale à 0

 

Notre premier fichier de test:

<?php

namespace tests\AppBundle\Entity;

use AppBundle\Entity\Media;
use AppBundle\Entity\Vote;

class MediaTest extends \PHPUnit_Framework_TestCase
{
    public function testGetNewAverageAfterVoteIfMediaHasVotes()
    {
        $media = new Media();

        $vote1 = new Vote();
        $vote1->setScore(5);

        $vote2 = new Vote();
        $vote2->setScore(9);

        $vote3 = new Vote();
        $vote3->setScore(2);

        $media->addVote($vote1);
        $media->addVote($vote2);
        $media->addVote($vote3);

        $media->getNewAverageAfterVote();

        $this->assertEquals(5.333333333333333, $media->getAverage());
    }
}

Je suis donc dans le répertoire tests à la racine  du site. Ma classe de test étend PHPUnit_Framework_TestCase.

Je commence par créer une entité Media, trois entités votes, et je rattache les votes au média par la méthode addVote. Pour chaque vote, je lui donne bien sûr un score. Enfin, j’appelle la méthode getNewAverageAfterVote() sur le media, qui va me recalculer l’average.

Puis arrive l’assertion qui vérifie qui la moyenne correspond bien à ce qui est retourné par $media->getAverage().

Pour tester, je lance la commande: vendor/bin/phpunit tests/AppBundle/Entity/MediaTest.php

Ensuite, je vais tester la même chose, mais sans votes:

    public function testGetNewAverageAfterVoteIfMediaHasNoVotes()
    {
        $media = new Media();

        $media->getNewAverageAfterVote();

        $this->assertEquals(0, $media->getAverage());
    }

[su_spacer]

 Le test de la méthode getDisplayedAverage avec un dataProvider

On avait déjà parlé des dataProviders lors du premier article, mais ils vont être redéfinis ici. Le but d’un dataProvider est de rejouer le même test en se basant sur une flopée de données de test, au lieu d’écrire un test par jeu de données.

    /**
     * Average score formatted for display
     * @return string
     */
    public function getDisplayedAverage()
    {
        return (null === $this->average) ? '-' : sprintf('%.1f', $this->average);
    }
    /**
     * @param $actual
     * @param $expected
     * @dataProvider getDisplayedAverageProvider
     */
    public function testGetDisplayedAverage($actual, $expected)
    {
        $media = new Media();
        $media->setAverage($actual);

        $this->assertEquals($expected, $media->getDisplayedAverage());
    }

    public function getDisplayedAverageProvider()
    {
        return [
            [5, '5.0'],
            [5.5, '5.5'],
            [16/3, '5.3'],
            [null, '-'],
        ];
    }

Ici, de manière très simple, je crée un média et je lui set son average avec le paramètre $actual. Puis je vérifie que le paramètre $expected est égal à ce que retourne $media->getDisplayedAverage(). Ici, $actual et $expected vont prendre successivement les données 5 et ‘5.0’, puis 5.5 et ‘5.5’, 16/3 et ‘5.3’ et enfin null et ‘-‘.

Je ne dois pas oublier de déclarer le dataProvider au dessus de la fonction de test par l’annotation @dataprovider.

Sans ce dataProvider, j’aurais dû écrire 4 tests:

    public function testGetDisplayedAverageWithIntegerAverage()
    {
        $media = new Media();
        $media->setAverage(5);

        $this->assertEquals('5.0', $media->getDisplayedAverage());
    }

    public function testGetDisplayedAverageWithFloatAverage()
    {
        $media = new Media();
        $media->setAverage(5.5);

        $this->assertEquals('5.5', $media->getDisplayedAverage());
    }

    public function testGetDisplayedAverageWithFractionAverage()
    {
        $media = new Media();
        $media->setAverage(16/3);

        $this->assertEquals('5.3', $media->getDisplayedAverage());
    }

    public function testGetDisplayedAverageWithNoAverage()
    {
        $media = new Media();
        $media->setAverage(null);

        $this->assertEquals('-', $media->getDisplayedAverage());
    }

C’est plus lourd, n’est-ce pas?

[su_spacer]

 Le test de la méthode hasUserAlreadyVoted

Si un utilisateur a déjà voté, je désire que l’appel à la méthode hasUserAlreadyVoted sur un média, avec un user en paramètre, me retourne true, false sinon. Je vais donc créer un média, trois users, trois votes associés à deux de ces users, et enfin rattacher ces votes à média:

    public function testHasUserAlreadyVoted()
    {
        $media = new Media();
        $user1 = new User();
        $user2 = new User();
        $user3 = new User();
        $user4 = new User();

        $vote1 = new Vote();
        $vote1->setUser($user1);

        $vote2 = new Vote();
        $vote2->setUser($user2);

        $vote3 = new Vote();
        $vote3->setUser($user3);

        $media->addVote($vote1);
        $media->addVote($vote2);
        $media->addVote($vote3);

        $this->assertTrue($media->hasUserAlreadyVoted($user1));
        $this->assertTrue($media->hasUserAlreadyVoted($user2));
        $this->assertTrue($media->hasUserAlreadyVoted($user3));
        $this->assertFalse($media->hasUserAlreadyVoted($user4));
    }

Ici donc, les users 1, 2 et 3 ont voté pour le média, mais pas le user4. Je teste donc assertTrue pour les trois premiers users, et assertFalse pour le dernier.

[su_spacer]

Des tests plus complexes, avec des mocks et des stubs

Mocks et stubs

Sous ces deux noms très barbares se trouvent des notions très simples. Les stubs sont des objets qui implémentent les mêmes méthodes que l’objet réel. Ces méthodes ne font rien et sont configurées pour retourner une valeur spécifique.

Les mocks sont des stubs capables de surcroît de tracer leurs appels (on spécifie uniquement les méthodes qui seront appelées) et de vérifier certaines conditions de ces appels (les exceptions par exemple).

Sur ce site, on peut lire:

  • Stub est centré sur le système testé (SUT). Il lui fourni une « indépendance » sous forme d’un objet véhiculant certaines données qui seront utilisées par le SUT pendant le test. Le stub ne peut jamais faire échouer le test. L’assertion est effectuée directement contre le SUT. Ceci s’appelle « state-based testing » car on vérifie juste l’état de notre SUT à la fin du test et non la manière dont cet état a été obtenu.
  • Mock est centré sur le test. Lors de la création du mock nous enregistrons un certain nombre d’attentes qui sont vérifiées pendant l’exécution du test. Le mock décide ci le test échoue ou réussi donc notre assertion nous la faisons contre le mock et non contre le SUT comme c’était dans le cas du stub. Dans le cas du mock ce n’est pas le résultat final qui nous intéresse mais la manière dont il a été obtenu ; Combien de fois une méthode a été invoquée ? Un évènement a été levé ? Qu’est-ce qu’il a été fait dans cette évènement ?, etc. Nous appelons ce cas « Interactions testing ».

Vous trouverez des exemples dans une présentation que j’avais faite: http://slides.com/jeanpierresaulnier/tests-unitaires

La majorité du temps, ce sont des mocks que vous utiliserez, pour simuler une classe, indiquer combien de fois vous désirez qu’une méthode soit appelée, ce que vous attendez comme donnée simulée pour cette méthode.

Parfois, juste parce que vous aurez besoin d’une dépendance dans un constructeur, vous utilisez simplement un stub. Vous allez rapidement comprendre par l’exemple.

Le test d’un service VoteManager

<?php

namespace AppBundle\Entity\Manager;

use AppBundle\Entity\Media;
use AppBundle\Entity\User;
use AppBundle\Entity\Vote;
use AppBundle\Repository\VoteRepository;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class VoteManager
{
    /**
     * @var voteRepository $voteRepository
     */
    protected $voteRepository;

    /**
     * @var TokenStorageInterface $tokenStorage
     */
    protected $tokenStorage;

    public function __construct(VoteRepository $voteRepository, TokenStorageInterface $tokenStorage)
    {
        $this->voteRepository = $voteRepository;
        $this->tokenStorage = $tokenStorage;
    }

    /**
     * Get a new vote object for current user and given media
     *
     * @param Media $media
     * @return Vote|null
     */
    public function getNewVote(Media $media)
    {
        $user = $this->tokenStorage->getToken()->getUser();

        if (!$user instanceof User) {
            return null;
        }

        $vote = new Vote();
        $vote->setUser($user);
        $vote->setMedia($media);

        return $vote;
    }

    /**
     * Save a vote
     *
     * @param Vote $vote
     */
    public function saveVote(Vote $vote)
    {
        $media = $vote->getMedia();
        $media->addVote($vote);

        $this->voteRepository->save($vote);
    }
}

J’ai un VoteManager qui est très simple. Il prend en dépendance le voteRepository et le tokenStorage, va permettre de créer un nouveau vote en fonction de l’utilisateur connecté, et également de sauver un vote.

Comme nous avons des dépendances, et que nous n’allons pas véritablement faire un appel en bdd, ou chercher un utilisateur connecté (ça, ça serait pour un test fonctionnel), il va falloir simuler tous ces comportements (on emploie généralement le terme mocker, même si on créé un stub, ce qui peut prêter à confusion.)

 

Créons nos premiers stubs

<?php

namespace tests\AppBundle\Entity\Manager;

use AppBundle\Entity\Media;
use AppBundle\Entity\User;
use AppBundle\Entity\Vote;
use AppBundle\Repository\VoteRepository;
use AppBundle\Entity\Manager\VoteManager;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class VoteManagerTest extends \PHPUnit_Framework_TestCase
{
    protected $repository;
    protected $token;
    protected $tokenStorage;
    protected $voteManager;

    public function setUp()
    {
        $this->token = $this->getMock(TokenInterface::class);
        $this->tokenStorage = $this->getMock(TokenStorageInterface::class);
        $this->repository = $this->getMock(VoteRepository::class, ['save'], [], "", false);
        $this->voteManager = $this->getMock(VoteManager::class, ['save'], [$this->repository, $this->tokenStorage]);
    }
}

Ici, j’ai remis les dépendances que j’avais dans le VoteManager, à savoir le voteRepository et le tokenStorage. J’en ai rajouté deux autres: le token et le voteManager. Pourquoi? Car à un moment donné, le tokenStorage va avoir besoin de faire appel à la méthode getToken() et devra retourner un token. Sauf que l’on ne veut pas retourner un véritable token (d’une classe étendant AbstractToken et implémentant l’interface TokenInterface), mais seulement une simulation, un stub de ce token.

Analysons les quatre stubs:

J’ai simplement créé un stub qui va me renvoyer un token simulé, et j’utilise son interface pour cela:

$this->token = $this->getMock(TokenInterface::class);

 

Je fais de même avec le tokenStorage. Je dois juste le passer dans un constructeur lors de la création du voteManager, je n’ai pas besoin qu’il fasse quelque chose de particulier pour le moment:

$this->tokenStorage = $this->getMock(TokenStorageInterface::class);

 

Ensuite, pour le voteRepository, je suis allé un petit peu plus loin. J’ai fait un stub partiel, c’est à dire que je ne vais simuler que la méthode save (puisque je ne vais pas vraiment sauver en bdd). Je n’ai pas besoin d’arguments dans le constructeur, donc je mets []. Je n’ai pas besoin de définir un mockClassName non plus, je mets donc «  ». Et enfin, je veux désactiver le constructeur, donc je mets false en 5ème argument:

$this->repository = $this->getMock(VoteRepository::class, ['save'], [], "", false);

 

Enfin, il y a le voteManager. De même, comme c’est lui que je vais tester, mais que je ne peux faire appel à sa méthode save, je vais faire un stub partiel et dire que je ne vais simuler que la méthode save. J’ai des arguments dans le constructeur, que je désire prendre en compte, et les indique dans en troisième paramètre:

$this->voteManager = $this->getMock(VoteManager::class, ['save'], [$this->repository, $this->tokenStorage]);

 

Le premier test et nos premiers mocks

    public function testGetNewVote()
    {
        $user = new User();
        $media = new Media();
        $vote = new Vote();
        $vote->setUser($user);
        $vote->setMedia($media);

        $this->tokenStorage
            ->expects($this->once())
            ->method('getToken')
            ->willReturn($this->token);

        $this->token
            ->expects($this->once())
            ->method('getUser')
            ->willReturn($user);

        $this->assertEquals($vote, $this->voteManager->getNewVote($media));
    }

Nous rentrons ici dans le vif du sujet. Nous allons transformer les stubs token et tokenStorage en mock avec la méthode expects.

Ici, je créer un user, un média, un vote, et je lis le user et le média au vote. Rappelez-vous ce que fait la méthode getNewVote du VoteManager:

    /**
     * Get a new vote object for current user and given media
     *
     * @param Media $media
     * @return Vote|null
     */
    public function getNewVote(Media $media)
    {
        $user = $this->tokenStorage->getToken()->getUser();

        if (!$user instanceof User) {
            return null;
        }

        $vote = new Vote();
        $vote->setUser($user);
        $vote->setMedia($media);

        return $vote;
    }

 

Je dois donc d’abord mocker la méthode getToken du tokenStorage en disant qu’elle sera appelée une fois et retournera une classe implémentant TokenInterface:

$this->tokenStorage
            ->expects($this->once())
            ->method('getToken')
            ->willReturn($this->token);

C’est parfait. Mais voilà, une fois que l’on a appelé la méthode getToken(), on voit que l’on appelle ensuite la méthode getUser(). Il faut aussi mocker ça en disant que nous allons appeler une fois cette méthode et retourner un user:

$this->token
            ->expects($this->once())
            ->method('getUser')
            ->willReturn($user);

 

Bien, une fois tout ceci fait, nous pouvons facile vérifier que le vote retourné par la méthode getNewVote($media) est bien un objet ayant les mêmes valeurs que celui que nous avons créé au début du test:

$user = new User();
        $media = new Media();
        $vote = new Vote();
        $vote->setUser($user);
        $vote->setMedia($media);

en faisant appel à cette assertion:

$this->assertEquals($vote, $this->voteManager->getNewVote($media));

[su_spacer]

Un second test avec des mocks

Nous allons tester la méthode saveVote():

    /**
     * Save a vote
     *
     * @param Vote $vote
     */
    public function saveVote(Vote $vote)
    {
        $media = $vote->getMedia();
        $media->addVote($vote);

        $this->voteRepository->save($vote);
    }

 

Nous allons donc mocker la méthode save du voteRepository. Par contre, ici, nous voulons vérifier aussi que $media va appeler une fois la méthode addVote(), et nous allons donc devoir la mocker.

    public function testSaveVote()
    {
        $media = $this->getMock(Media::class);
        $vote = new Vote();
        $vote->setMedia($media);

        $media
            ->expects($this->once())
            ->method('addVote')
            ->with($vote);

        $this->repository
            ->expects($this->once())
            ->method('save')
            ->with($vote);

        $this->voteManager->saveVote($vote);
    }

[su_spacer]

Tester les violations

Imaginons que j’ai une entité appelée Contact, qui va me servir pour le formulaire de contact. Disons que j’ai une liste correspondant au champ knowledge. Ce champs sera nourri par une liste déroulante de réponses à la question « Comment nous avez-vous connu? ». Parfait. Mais nous voulons aussi permettre de cocher « autre », et dans ce cas là, un champs texte va apparaitre (je vais appeler le champs « other »):

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

class Contact
{
    [...]

    /**
     * @var string
     *
     * @ORM\Column(name="knowledge", type="string", length=255, nullable=true)
     */
    private $knowledge;

    /**
     * @var string
     *
     * @ORM\Column(name="other", type="string", length=255, nullable=true)
     */
    private $other;


    /**
     * Set knowledge
     *
     * @param string $knowledge
     *
     * @return Contact
     */
    public function setKnowledge($knowledge)
    {
        $this->knowledge = $knowledge;

        return $this;
    }

    /**
     * Get knowledge
     *
     * @return string
     */
    public function getKnowledge()
    {
        return $this->knowledge;
    }

    /**
     * Set other
     *
     * @param string $other
     *
     * @return Contact
     */
    public function setOther($other)
    {
        $this->other = $other;

        return $this;
    }

    /**
     * Get other
     *
     * @return string
     */
    public function getOther()
    {
        return $this->other;
    }

    /**
     * @param ExecutionContextInterface $context
     * @Assert\Callback
     */
    public function validate(ExecutionContextInterface $context)
    {
        if ('autre' === $this->getKnowledge() && null === $this->getOther()) {
            $context->buildViolation(
                "Vous devez remplir ce champ si vous avez coché 'autre'"
            )
                ->atPath('other')
                ->addViolation();
        }
    }
}

Ce que je veux, c’est que si l’internaute coche « autre » et qu’il ne remplit pas ce champs texte, une violation soit lancée sur le champs other. Nous allons donc tester cela:

[su_spacer]

Un premier test avec violation

 public function testViolation()
    {
        $contact = new Contact();

        $formData = [
            'knowledge' => 'autre',
            'other' => null,
        ];

        $contact = $this->fromArray($contact, $formData);

        $constraintViolationBuilder = $this->getMock(ConstraintViolationBuilderInterface::class);
        $constraintViolationBuilder
            ->expects($this->once())
            ->method('atPath')
            ->with('other')
            ->willReturn($constraintViolationBuilder);

        $constraintViolationBuilder
            ->expects($this->once())
            ->method('addViolation');

        $executionContext = $this->getMock(ExecutionContextInterface::class);
        $executionContext
            ->expects($this->once())
            ->method('buildViolation')
            ->with("Vous devez remplir ce champ si vous avez coché 'autre'")
            ->willReturn($constraintViolationBuilder);

        $contact->validate($executionContext);
    }

Tout d’abord, nous nous disons, en voyant la méthode validate qu’il faut mocker ExecutionContextInterface. Seul petit problème, la fonction buildViolation va renvoyer une ConstraintViolationBuilderInterface qui appellera atPath et addViolation. Il faut donc aussi mocker ceci.

Du coup, on commence par mocker une ConstraintViolationBuilderInterface:

$constraintViolationBuilder = $this->getMock(ConstraintViolationBuilderInterface::class);

Et l’on va dire que l’on va mocker dans l’ordre les appels atPath et buildViolation:

$constraintViolationBuilder
            ->expects($this->once())
            ->method('atPath')
            ->with('other')
            ->willReturn($constraintViolationBuilder);

        $constraintViolationBuilder
            ->expects($this->once())
            ->method('addViolation')

 

Enfin, nous pouvons mocker l’ExecutionContextInterface qui va appeler buildViolation et retourner une ConstraintViolationInterface, et enfin faire appeler à la méthode validate:

$executionContext = $this->getMock(ExecutionContextInterface::class);
$executionContext
    ->expects($this->once())
    ->method('buildViolation')
    ->with("Vous devez remplir ce champ si vous avez coché 'autre'")
    ->willReturn($constraintViolationBuilder);

$contact->validate($executionContext);

 [su_spacer]

 Un second test sans violation

Identique au premier test, mais on vérifie qu’on n’appelle jamais la plupart des méthodes.

public function testViolationWillNotAddViolation()
    {
        $contact = new Contact();

        $formData = [
            'knowledge' => 'facebook',
            'other' => null,
        ];

        $contact = $this->fromArray($contact, $formData);

        $constraintViolationBuilder = $this->getMock(ConstraintViolationBuilderInterface::class);
        $constraintViolationBuilder
            ->expects($this->never())
            ->method('atPath')
            ->with('other')
            ->willReturn($constraintViolationBuilder);

        $constraintViolationBuilder
            ->expects($this->never())
            ->method('addViolation');

        $executionContext = $this->getMock(ExecutionContextInterface::class);
        $executionContext
            ->expects($this->never())
            ->method('buildViolation')
            ->with("Vous devez remplir ce champ si vous avez coché 'autre'")
            ->willReturn($constraintViolationBuilder);

        $contact->validate($executionContext);
    }

 

Test d’un mail envoyé

<?php
namespace AppBundle\Entity\Manager;

use AppBundle\Entity\Contact;
use AppBundle\Service\MailerService;
use Symfony\Component\Translation\TranslatorInterface;

class ContactManager
{
    /**
     * @var \Swift_Mailer
     */
    protected $mailer;

    /**
     * @var \Twig_Environment
     */
    protected $templating;

    /**
     * @var TranslatorInterface $translator
     */
    protected $translator;

    /**
     * @var array
     */
    protected $template;

    /**
     * @var string $from
     */
    protected $from;

    /**
     * @var string $to
     */
    protected $to;

    /**
     * @var MailerService
     */
    protected $mailerService;

    /**
     * ContactManager constructor.
     * @param MailerService $mailerService
     * @param \Twig_Environment $templating
     * @param TranslatorInterface $translator
     * @param $template
     * @param $from
     * @param $to
     */
    public function __construct
    (
        MailerService $mailerService,
        \Twig_Environment $templating,
        TranslatorInterface $translator,
        $template,
        $from,
        $to
    )
    {
        $this->templating = $templating;
        $this->template = $template;
        $this->translator = $translator;
        $this->from = $from;
        $this->to = $to;
        $this->mailerService = $mailerService;
    }

    /**
     * @param Contact $contact
     */
    public function sendMail(Contact $contact)
    {
        $this->mailerService->sendMail(
            $this->from,
            $this->to,
            $this->translator->trans('message_subject', ['%name%' => $contact->getFirstName() . ' ' . $contact->getLastName()], 'contact'),
            $this->templating->render($this->template, ['contact' => $contact])
        );
    }
}

 

J’ai donc un ContactManager qui va se charger d’envoyer le mail. La première chose à faire est de mocker les dépendances:

use AppBundle\Entity\Contact;
use AppBundle\Entity\Manager\ContactManager;
use AppBundle\Service\MailerService;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\TranslatorInterface;

class ContactManagerTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var \Swift_Mailer
     */
    protected $mailer;

    /**
     * @var \Twig_Environment
     */
    protected $templating;

    /**
     * @var TranslatorInterface $translator
     */
    protected $translator;

    /**
     * @var array
     */
    protected $template;

    /**
     * @var string $from
     */
    protected $from;

    /**
     * @var string $to
     */
    protected $to;

    /**
     * @var MailerService
     */
    protected $mailerService;

    /**
     * @var ContactManager
     */
    protected $contactManager;

    public function setUp()
    {
        $transport = $this->getMock(\Swift_Transport::class);
        $this->mailer = $this->getMock(\Swift_Mailer::class, [], [$transport]);
        $this->templating = $this->getMock(\Twig_Environment::class, ['render'], [], '', false);
        $this->translator = $this->getMock(Translator::class, ['trans'], [], '', false);
        $this->template = 'Bundle:Controller:Method';
        $this->from = 'from@test.fr';
        $this->to = 'to@test.fr';
        $this->mailerService = $this->getMock(MailerService::class, ['sendMail'], [$this->mailer]);
        $this->contactManager = new ContactManager($this->mailerService, $this->templating, $this->translator, $this->template, $this->from, $this->to);
    }

 

Ensuite, on teste l’envoi du mail et on vérifie que le template appelé est le bon, avec les appels corrects de méthodes:

    public function testSendMail()
    {
        $contact = new Contact();
        $contact->setAdditionalInformation('some more information');
        $contact->setCellphone('0123456789');
        $contact->setEmail('contact.email@test.fr');
        $contact->setFirstName('firstName');
        $contact->setLastName('lastName');
        $contact->setKnowledge('pub_papier');

        $this->translator
            ->expects($this->exactly(2))
            ->method('trans')
            ->with('message_subject', ['%name%' => $contact->getFirstName() . ' ' . $contact->getLastName()], 'contact')
            ->willReturn('some translation');

        $this->templating
            ->expects($this->exactly(2))
            ->method('render')
            ->with($this->template, ['contact' => $contact])
            ->willReturn('The rendered template');

        $this->mailerService
            ->expects($this->once())
            ->method('sendMail')
            ->with(
                $this->from,
                $this->to,
                $this->translator->trans('message_subject', ['%name%' => $contact->getFirstName() . ' ' . $contact->getLastName()], 'contact'),
                $this->templating->render($this->template, ['contact' => $contact])
                );

        $this->contactManager->sendMail($contact);
    }

 

Rédigé par

One comment

  1. I appreciate, result in I discoveresd exactly what I was taling a look for.
    You’ve ended myy four day lpng hunt! Good Bless you
    man. Have a nice day. Bye

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.