phpUnit equalTo won't look inside my objects - phpunit

I'm writing this test:
public function update_emails_in_gmail(): void
{
$emailId = new EmailId(self::EMAIL_ID);
$sender = new EmailAddress(self::SENDER_ADDRESS);
$sentAt = new DateTimeImmutable();
$email = new Email($emailId, $sender, $sentAt);
$labelToAdd = new Label(self::LABEL_ADD_THIS, self::LABEL_ADD_THIS);
$labelToRemove = new Label(self::LABEL_REMOVE_THIS, self::LABEL_REMOVE_THIS);
$removeLabel = new RemoveLabel($labelToRemove);
$addLabel = new AddLabel($labelToAdd);
$this->gmailService
->expects($this->once())
->method('modifyMessage')
->with($this->equalTo($emailId), $this->equalTo([$addLabel, $removeLabel]))
;
$email
->addLabel($labelToAdd)
->removeLabel($labelToRemove)
;
$this
->gmailRepository
->updateEmail($email);
}
And this is the code for updateEmail:
/**
* #param Email $email
* #return void
*/
public function updateEmail(Email $email): void
{
$this
->gmail
->modifyMessage($email->id(), [new AddLabel(new Label("", "")), new RemoveLabel(new Label("", ""))]);
}
I was expecting the test to fail, since self::LABEL_ADD_THIS and self::LABEL_REMOVE_THIS are both not empty but phpUnit is saying the test passed...
Any idea what I'm doing wrong?

Related

Symfony mock service when test console command

How to mock some service when test console command. I have some console command, in this command I get some service and I want mock this service
console command
const APP_SATISFACTION_REPORT = 'app:satisfactionrepor';
protected function configure()
{
$this
->setName(self::APP_SATISFACTION_REPORT)
->setDescription('Send Satisfaction Report');
}
/**
* #param InputInterface $input
* #param OutputInterface $output
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$container = $this->getContainer();
$serviceCompanyRepo = $container->get('app.repository.orm.service_company_repository');
$satisfactionReport = $container->get('app.services.satisfaction_report');
/** #var ServiceCompany $serviceCompany */
foreach ($serviceCompanyRepo->findAll() as $serviceCompany) {
try {
$satisfactionReport->sendReport($serviceCompany);
} catch (\Exception $e) {
$io->warning(sprintf(
'Failed to send satisfaction report for service company with ID %s',
$serviceCompany->getId()
));
}
}
}
and my tests
/** #var Console\Application $application */
protected $application;
protected $container;
/** #var BufferedOutput $output */
protected $output;
/**
* #var ServiceCompanyRepository
*/
private $serviceCompanyRepository;
prepare console command
public function setUp()
{
parent::setUp();
$entityManager = $this->getEntityManager();
$this->serviceCompanyRepository = $entityManager->getRepository(ServiceCompany::class);
static::bootKernel();
$this->container = static::$kernel->getContainer();
$this->application = new Console\Application(static::$kernel);
$this->application->setAutoExit(false);
$master = new SatisfactionReportCommand();
$this->application->add($master);
}
public function setUpMaster() {
$this->output = new BufferedOutput();
$this->application->run(new ArgvInput([
'./bin/console',
SatisfactionReportCommand::APP_SATISFACTION_REPORT,
]), $this->output);
}
public function testGetMasterOutput()
{
$this->loadFixture(ServiceCompany::class);
/** #var ServiceCompany[] $serviceCompanies */
$serviceCompanies = $this->serviceCompanyRepository->findAll();
$this->assertCount(2, $serviceCompanies);
$client = self::createClient();
mock service app.services.satisfaction_report
$service = $this->getMockService($serviceCompanies);
and set it in container
$client->getContainer()->set('app.services.satisfaction_report', $service);
$this->setUpMaster();
$output = $this->output->fetch();
}
protected function getMockService($serviceCompanies)
{
$service = $this->getMockBuilder(SatisfactionReport::class)
->setMethods(['sendReport'])
->disableOriginalConstructor()
->getMock();
$service
->expects($this->exactly(2))
->method('sendReport')
->withConsecutive(
[$serviceCompanies[0]],
[$serviceCompanies[1]]
);
return $service;
}
How to mock app.services.satisfaction_report? Set in container app.services.satisfaction_report not help me
I had same problem but I resolved it.
I have base class:
class TestCase extends WebTestCase
{
/** #var Application */
private $application;
private $mailServiceMock;
protected function setMailService(MailService $mailServiceMock): void
{
$this->mailServiceMock = $mailServiceMock;
}
protected function getApplication(): Application
{
static::bootKernel();
static::$kernel->getContainer()->get('test.client');
$this->setMocks();
$this->application = new Application(static::$kernel);
$this->application->setCatchExceptions(false);
$this->application->setAutoExit(false);
return $this->application;
}
protected function execute(string $action, array $arguments = [], array $inputs = []): CommandTester
{
$tester = (new CommandTester($this->getApplication()->find($action)))->setInputs($inputs);
$tester->execute($arguments);
return $tester;
}
private function setMocks(): void
{
if ($this->mailServiceMock) {
static::$kernel->getContainer()->set('mail', $this->mailServiceMock);
}
}
}
And test class
class SendEmailCommandTest extends TestCase
{
public function testExecuteSendingError(): void
{
$mailServiceMock = $this->getMockBuilder(MailService::class)->disableOriginalConstructor()
->setMethods(['sendEmail'])->getMock();
$mailServiceMock->method('sendEmail')->willThrowException(new \Exception());
$this->setMailService($mailServiceMock);
$tester = $this->execute(SendEmailCommand::COMMAND_NAME, self::NORMAL_PAYLOAD);
$this->assertEquals(SendEmailCommand::STATUS_CODE_EMAIL_SENDING_ERROR, $tester->getStatusCode());
}
}
As you can see I set mail service right after booting kernel.
And here you can see my services.yaml:
services:
mail.command.send.email:
autowire: true
class: App\Command\SendEmailCommand
arguments: ["#logger", "#mail"]
tags:
- {name: console.command, command: "mail:send.email"}
If you create the commands as a service, where the framework injects the services automatically (either autowired, or with an explicit argument list) into a constructor (tip: in the command, call the parent::__construct()), then the test can create whatever mock or other replacement service that matches the parameter typehint (or interface).
here is my example class:
class MainCommandTest extends IntegrationTestCase
{
/**
* #var MainCommand
*/
protected $subject;
/**
* #var Application
*/
protected $application;
/**
* sets test subject
*
* #return void
*/
public function setUp()
{
parent::setUp();
static::bootKernel();
$readStreams = new ReadStreams();
$udpStreamMock = $this->getMockBuilder(UdpStream::class)->disableOriginalConstructor()->setMethods(['readIncomingStreams'])->getMock();
$udpStreamMock->expects($this->once())->method('readIncomingStreams')->willReturn($readStreams);
static::$kernel->getContainer()->set(UdpStream::class, $udpStreamMock);
$application = new Application($this::$kernel);
$this->subject = $this->getService(MainCommand::class);
$application->add( $this->subject);
$this->application = $application;
}
/**
* Tests command in $subject command,
*
* #return void
*/
public function testCommand()
{
$command = $this->application->find( $this->subject->getName());
$commandTester = new CommandTester($command);
$commandTester->execute(
[
'command' => $this->subject->getName()
]
);
$this->stringContains($commandTester->getDisplay(true), 'finished');
$this->assertEquals($commandTester->getStatusCode(), 0);
}
}

oEmbed in Symfony 3 implementation

I want to allow user creating oEmbed content to my website.
There is a lot of information how to implement it but none about creating a enpoint.
Quick question: is there some ready php code samples for creating (returning) oEmbed response for third party websites?
If no, how to create it? Ex youtube link looks like this:
http://www.youtube.com/oembed?url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DiwGFalTRHDA
and the response is :
{
"html": "<iframe width=\"459\" height=\"344\" src=\"https://www.youtube.com/embed/iwGFalTRHDA?feature=oembed\" frameborder=\"0\" allowfullscreen></iframe>",
"thumbnail_height": 360,
"thumbnail_width": 480,
"provider_name": "YouTube",
"author_url": "https://www.youtube.com/user/KamoKatt",
"thumbnail_url": "https://i.ytimg.com/vi/iwGFalTRHDA/hqdefault.jpg",
"author_name": "KamoKatt",
"provider_url": "https://www.youtube.com/",
"type": "video",
"version": "1.0",
"width": 459,
"title": "Trololo",
"height": 344
}
My question is: how they know which video it is? They are using regexp to parse videoID?
What is 'best practice' for this kind of request? Should it be created as Controller, Service, Provider or how?
Finally I have created it like this:
I have created a service:
namespace AppBundle\Service;
use AppBundle\Entity\PlaceMarker;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Config\Definition\Exception\Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception as Ex;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class EmbedService
{
CONST THUMBNAIL_WIDTH = 400;
CONST THUMBNAIL_HEIGHT = 300;
CONST HTML_WIDTH = 459;
CONST HTML_HEIGHT = 344;
/**
* #var string
*/
protected $baseUrl;
/**
* #var array
*/
private $params = [];
/**
* #var EntityManager
*/
private $entityManager;
/** #var \Twig_Environment */
private $twig;
public function __construct($baseUrl, EntityManager $entityManager, \Twig_Environment $twig)
{
$this->baseUrl = strtolower($baseUrl);
$this->entityManager = $entityManager;
$this->twig = $twig;
}
/**
*
* #param Request $request
* #param null $format
* #throws \Exception
*/
public function handleRequest(Request $request, $format = null)
{
$this->params = [];
// $baseUrl = $request->getSchemeAndHttpHost();
/** handle url parameter */
$this->params['url'] = $request->query->get('url');
if (!$this->params['url']) {
throw new Ex\BadRequestHttpException('A URL parameter must be provided');
}
/** check if url matches site url */
$this->params['path'] = substr($request->query->get('url'), strlen($this->baseUrl));
if (substr(strtolower($request->query->get('url')), 0, strlen($this->baseUrl)) !== strtolower($this->baseUrl)) {
throw new Ex\BadRequestHttpException('This is not a valid URL parameter');
}
/** handle format parameters */
$this->params['format'] = $format;
if (null === $format) {
$this->params['format'] = $request->query->get('format', 'json');
}
if (!in_array($this->params['format'], ['json', 'xml'])) {
throw new \Exception('Disallowed format parameter \'' . $this->params['format'] . '\'');
}
$maxWidth = $request->query->get('maxwidth', null);
if (is_numeric($maxWidth) || is_null($maxWidth)) {
$this->params['maxWidth'] = $maxWidth;
}
$maxHeight = $request->query->get('maxheight', null);
if (is_numeric($maxHeight) || is_null($maxHeight)) {
$this->params['maxHeight'] = $maxHeight;
}
$this->params['attr'] = $this->matchRoute($this->params['path']);
}
public function matchRoute($path)
{
$route1 = new Route('/place-marker/{id}/show', ['controller' => 'PlaceMarkerController']);
$routes = new RouteCollection();
$routes->add('place-marker', $route1);
$context = new RequestContext('/');
$matcher = new UrlMatcher($routes, $context);
try {
return $matcher->match($path);
} catch (\Exception $ex) {
throw new Ex\NotFoundHttpException('No implemented!');
}
}
/**
* #return array
*/
public function create()
{
if (!$this->params) {
throw new Exception('Unable to create oEmbed. Did you use handleRequest() first?');
}
$oEmbed = [
'version' => '1.0',
'provider_url' => $this->baseUrl,
'provider_name' => 'Pixdor.com'
];
switch ($this->params['attr']['_route']) {
case 'place-marker':
/**
* #var PlaceMarker $pMarker
*/
$pMarker = $this->entityManager->getRepository(PlaceMarker::class)->find($this->params['attr']['id']);
$oEmbed['title'] = $pMarker->getName();
$oEmbed['url'] = $pMarker->getUrl();
$oEmbed['description'] = $pMarker->getDescription();
$oEmbed['caption'] = $pMarker->getCaption();
$oEmbed['html'] = $this->twig->render(':place_marker:embed.html.twig');
$oEmbed['width'] = self::HTML_WIDTH;
$oEmbed['height'] = self::HTML_HEIGHT;
$oEmbed['rich'] = 'photo';
break;
}
$oEmbed['thumbnail_width'] = isset($this->params['maxWidth']) ? $this->params['maxWidth'] : self::THUMBNAIL_WIDTH;
$oEmbed['thumbnail_height'] = isset($this->params['thumbnail_height']) ? $this->params['thumbnail_height'] : self::THUMBNAIL_HEIGHT;
$oEmbed['thumbnail_url'] = sprintf('http://placehold.it/%dx%d', $oEmbed['thumbnail_width'], $oEmbed['thumbnail_height']);
return $oEmbed;
}
/**
* Returns format type (json|xml)
* #return string
*/
public function getFormat()
{
if (!$this->params) {
throw new Exception('Unknown format type. Did you use handleRequest() first?');
}
return $this->params['format'];
}
}
services.yml
app.embed:
class: AppBundle\Service\EmbedService
arguments: ["%base_url%", "#doctrine.orm.entity_manager", "#twig"]
parameters.yml
base_url: "http://project.local"
next in the controller:
/**
* #Route("/oembed.{_format}",
* name="embed",
* defaults={"_format": null}
* )
* #param Request $request
* #param $_format
* #return \Symfony\Component\HttpFoundation\Response
* #throws \Exception
*/
public function generateAction(Request $request, $_format)
{
$oEmbed = $this->get('app.embed');
$oEmbed->handleRequest($request, $_format);
$view = View::create()
->setFormat($oEmbed->getFormat())
->setStatusCode(200)
->setData($oEmbed->create());
return $this->handleView($view);
}
you call it like this:
http://project.local/oembed?url=http://project.local/place-marker/4/show

Codeception improve readiness unit test

I'm new to test, I'm using codeception and phpunit to do some TDD.
However, my methods have a lot of code. Do I used best practices ? Is there a way to improve the readiness of my code, can it be more clean ?
class NewsFormHandlerTest extends \Codeception\Test\Unit
{
/**
* #var \UnitTester
*/
protected $tester;
protected function _before()
{
}
protected function _after()
{
}
private function getFormMock(){
return $this->getMockBuilder(FormInterface::class)
->disableOriginalConstructor()
->getMock();
}
private function getNewsManagerMock(){
return $this->getMockBuilder(INewsManager::class)
->disableOriginalConstructor()
->getMock();
}
// tests
public function testShouldHandleASuccessfulFormSubmissionForAddANews()
{
// prepare
$request = new \Symfony\Component\HttpFoundation\Request();
$news = new News();
$form = $this->getFormMock();
$form->expects($this->once())
->method('isValid')
->will($this->returnValue(true));
$form->expects($this->once())
->method('submit');
$form->expects($this->once())
->method('getData')
->will($this->returnValue($news));
$newsManager = $this->getNewsManagerMock();
$newsManager->expects($this->once())
->method('add');
$user = Stub::make(WebserviceUser::class, []);
// test
$handler = new NewsFormHandler($newsManager, $user);
$newsReturned = $handler->handle($form, $request, NewsFormHandler::ADD);
// assert
$this->assertInstanceOf(News::class, $newsReturned);
$this->assertEquals($news, $newsReturned);
}
public function testShouldHandleASuccessfulFormSubmissionForEditANews()
{
// prepare
$request = new \Symfony\Component\HttpFoundation\Request();
$news = new News();
$form = $this->getFormMock();
$form->expects($this->once())
->method('isValid')
->will($this->returnValue(true));
$form->expects($this->once())
->method('submit');
$form->expects($this->once())
->method('getData')
->will($this->returnValue($news));
$newsManager = $this->getNewsManagerMock();
$newsManager->expects($this->once())
->method('edit');
$user = Stub::make(WebserviceUser::class, []);
// test
$handler = new NewsFormHandler($newsManager, $user);
$newsReturned = $handler->handle($form, $request, NewsFormHandler::EDIT);
// assert
$this->assertInstanceOf(News::class, $newsReturned);
$this->assertEquals($news, $newsReturned);
}
public function testFailFormWithInvalidData()
{
// prepare
$request = new \Symfony\Component\HttpFoundation\Request();
$form = $this->getFormMock();
$form->expects($this->once())
->method('isValid')
->will($this->returnValue(false));
$newsManager = $this->getNewsManagerMock();
$newsManager->expects($this->never())
->method('edit');
$this->expectException(InvalidFormException::class);
$user = Stub::make(WebserviceUser::class, []);
// test
$handler = new NewsFormHandler($newsManager, $user);
$newsReturned = $handler->handle($form, $request, NewsFormHandler::ADD);
// assert
$this->assertNull($newsReturned);
}
}
You can probably extract body of testShouldHandleASuccessfulFormSubmissionForAddANews and testShouldHandleASuccessfulFormSubmissionForEditANews to other method with parameter like $action = 'add'|'edit' (or use your defined contants NewsFormHandler::EDIT etc), as they're almost the same.
You can extract mock creation from above methods to a single parametrized method, as the process is almost the same (pass the differences as method arguments and let it do the dirty work).
You can also add some readability by using BDD style, as in example in page http://codeception.com/docs/05-UnitTests#BDD-Specification-Testing

Retrieve all controller action that have a specific annotation - Symfony2

I have created an annotation class called #Module, and a command GenerateModulesCommand. What I want is to find all controller actions that have the #Module annotation.
Example :
/**
*
* #Module(name='sidebar', enabled=true')
*/
public function sidebarAction($name) {
$ape = new ArrayParamsExtension ();
return $this->render('ModuleManagerBundle:Default:sidebar.html.twig', $ape->getArrayParams($name));
}
I want to be able to look at the specific properties in the #Module (name, enabled, etc...)
So far, this is my execute method from my Command :
protected function execute(InputInterface $input, OutputInterface $output) {
$path = $this->getApplication()->getKernel()->locateResource('#ModuleManagerBundle');
$driver = new PHPDriver($path);
$classes = $driver->getAllClassNames();
foreach ($classes as $key => $class) {
$reader = new AnnotationReader();
$annotationReader = new CachedReader(
$reader, new ArrayCache()
);
$reflClass = new ReflectionClass("\Controller\\" . $reportableClass);
$annotation = $annotationReader->getClassAnnotation(
$reflClass, 'Custom_Annotation'
);
if (is_null($annotation)) {
unset($classes[$key]);
}
}
$output->writeln($path);
}
I found this code on sof, but I don't know how to search all Controller classes and all the Actions inside them..
You can try this in your function
use Doctrine\Common\Annotations\AnnotationReader;
your function
public function getControllersWithAnnotationModules()
{
$allAnnotations = new AnnotationReader();
$controllers = array();
foreach ($this->container->get('router')->getRouteCollection()->all() as $route) {
$defaults = $route->getDefaults();
if (isset($defaults['_controller'])) {
$controllerAction = explode(':', $defaults['_controller']);
$controller = $controllerAction[0];
if (!isset($controllers[$controller]) && class_exists($controller)) {
$controllers[$controller] = $controller;
}
}
}
$controllersWithModules = array();
foreach($controllers as $controller){
$reflectionClass = new \ReflectionClass($controller);
$module = $allAnnotations->getClassAnnotation($reflectionClass,'Acme\YourBundle\Module');
if($module)
$controllersWithModules[] = $controller;
}
return $controllersWithModules ;
}

Symfony2 and ParamConverter(s)

Accessing my route /message/new i'm going to show a form for sending a new message to one or more customers. Form model has (among others) a collection of Customer entities:
class MyFormModel
{
/**
* #var ArrayCollection
*/
public $customers;
}
I'd like to implement automatic customers selection using customers GET parameters, like this:
message/new?customers=2,55,543
This is working now by simply splitting on , and do a query for getting customers:
public function newAction(Request $request)
{
$formModel = new MyFormModel();
// GET "customers" parameter
$customersIds = explode($request->get('customers'), ',');
// If something was found in "customers" parameter then get entities
if(!empty($customersIds)) :
$repo = $this->getDoctrine()->getRepository('AcmeHelloBundle:Customer');
$found = $repo->findAllByIdsArray($customersIds);
// Assign found Customer entities
$formModel->customers = $found;
endif;
// Go on showing the form
}
How can i do the same using Symfony 2 converters? Like:
public function newAction(Request $request, $selectedCustomers)
{
}
Answer to my self: there is not such thing to make you life easy. I've coded a quick and dirty (and possibly buggy) solution i'd like to share, waiting for a best one.
EDIT WARNING: this is not going to work with two parameter converters with the same class.
Url example
/mesages/new?customers=2543,3321,445
Annotations:
/**
* #Route("/new")
* #Method("GET|POST")
* #ParamConverter("customers",
* class="Doctrine\Common\Collections\ArrayCollection", options={
* "finder" = "getFindAllWithMobileByUserQueryBuilder",
* "entity" = "Acme\HelloBundle\Entity\Customer",
* "field" = "id",
* "delimiter" = ",",
* }
* )
*/
public function newAction(Request $request, ArrayCollection $customers = null)
{
}
Option delimiter is used to split GET parameter while id is used for adding a WHERE id IN... clause. There are both optional.
Option class is only used as a "signature" to tell that converter should support it. entity has to be a FQCN of a Doctrine entity while finder is a repository method to be invoked and should return a query builder (default one provided).
Converter
class ArrayCollectionConverter implements ParamConverterInterface
{
/**
* #var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
function apply(Request $request, ConfigurationInterface $configuration)
{
$name = $configuration->getName();
$options = $this->getOptions($configuration);
// Se request attribute to an empty collection (as default)
$request->attributes->set($name, new ArrayCollection());
// If request parameter is missing or empty then return
if(is_null($val = $request->get($name)) || strlen(trim($val)) === 0)
return;
// If splitted values is an empty array then return
if(!($items = preg_split('/\s*'.$options['delimiter'].'\s*/', $val,
0, PREG_SPLIT_NO_EMPTY))) return;
// Get the repository and logged user
$repo = $this->getEntityManager()->getRepository($options['entity']);
$user = $this->getSecurityContext->getToken()->getUser();
if(!$finder = $options['finder']) :
// Create a new default query builder with WHERE user_id clause
$builder = $repo->createQueryBuilder('e');
$builder->andWhere($builder->expr()->eq("e.user", $user->getId()));
else :
// Call finder method on repository
$builder = $repo->$finder($user);
endif;
// Edit the builder and add WHERE IN $items clause
$alias = $builder->getRootAlias() . "." . $options['field'];
$wherein = $builder->expr()->in($alias, $items);
$result = $builder->andwhere($wherein)->getQuery()->getResult();
// Set request attribute and we're done
$request->attributes->set($name, new ArrayCollection($result));
}
public function supports(ConfigurationInterface $configuration)
{
$class = $configuration->getClass();
// Check if class is ArrayCollection from Doctrine
if('Doctrine\Common\Collections\ArrayCollection' !== $class)
return false;
$options = $this->getOptions($configuration);
$manager = $this->getEntityManager();
// Check if $options['entity'] is actually a Dcontrine one
try
{
$manager->getClassMetadata($options['entity']);
return true;
}
catch(\Doctrine\ORM\Mapping\MappingException $e)
{
return false;
}
}
protected function getOptions(ConfigurationInterface $configuration)
{
return array_replace(
array(
'entity' => null,
'finder' => null,
'field' => 'id',
'delimiter' => ','
),
$configuration->getOptions()
);
}
/**
* #return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager()
{
return $this->container->get('doctrine.orm.default_entity_manager');
}
/**
* #return \Symfony\Component\Security\Core\SecurityContext
*/
protected function getSecurityContext()
{
return $this->container->get('security.context');
}
}
Service definition
arraycollection_converter:
class: Acme\HelloBundle\Request\ArrayCollectionConverter
arguments: ['#service_container']
tags:
- { name: request.param_converter}
It's late, but according to latest documentation about #ParamConverter, you can achieve it follow way:
* #ParamConverter("users", class="AcmeBlogBundle:User", options={
* "repository_method" = "findUsersByIds"
* })
you just need make sure that repository method can handle comma (,) separated values

Resources