How to get full control of MockObject's behavior? - phpunit

I need to create a PHPUnit test double that:
uses the original implementation of some methods
proxies some other methods to the original implementation but allows me to spy on them
replaces yet other methods with NOP stubs with fixed return values and doesn't call the original implementation
How do I?
Between ->getMockBuilder()->getMock(), ->createMock(), ->createPartialMock(), ->createTestProxy(), and ->getMockBuilder()->enableProxyingToOriginalMethods()->setProxyTarget() I seem to be able to satisfy at most two of the above three requirements, but not all three.

Solved by mock chaining:
protected function getMixedMock(
string $className,
array $constructorArgs,
array $stubMethods,
array $proxyMethods
) : array
{
$stub = $this->getMockBuilder($className)
->setMethods($stubMethods)
->getMock();
$proxy = $this->getMockBuilder(Local::class)
->setConstructorArgs($constructorArgs)
->setMethods(array_unique(array_merge($stubMethods, $proxyMethods)))
->enableProxyingToOriginalMethods()
->setProxyTarget($stub) // <-- CHAINING HERE
->getMock();
return [$stub, $proxy];
}
public function test()
{
[$stub, $proxy] = $this->getMixedMock(
LocalFSAdapter::class,
["/"],
['write'],
['has']
);
//methods that must be stubbed should be spied on using $stub
//methods that must be proxied should be spied on using $proxy
//use $proxy for testing
}

Related

Assert function has been called once phpunit

I'm new to PHPUnit and i'm wondering how to assert that a function from a service has been called.
I tried to mock my service that implement the function ofDatetimeRange :
$mock = $this->getMockBuilder(QueryBuilder::class)
->onlyMethods(['ofDatetimeRange'])
->getMock();
And then just call the function that suppose to call the service and finally assert that have been call once or never.
Here is the test case :
/**
* Test that there is no date filter.
*
* #return void
*/
public function testNoDateFilter()
{
$mock = $this->getMockBuilder(QueryBuilder::class)
->onlyMethods(['ofDatetimeRange'])
->getMock();
$engine = new Engine();
$engine->prepareQuery(); <--- this should call QueryBuilder::ofDatetimeRange
$mock->expects($this->exactly(1))->method('ofDatetimeRange');
}
Expectation failed for method name is "ofDatetimeRange" when invoked 1
time(s). Method was expected to be called 1 times, actually called 0
times.
It looks like my engine doesn't use the mocked instance...
Is there something i'm doing wrong ?
Note that $engine->prepareQuery() should call ofDatetimeRange method of QueryBuilder class.
You're right, $engine object in your test doesn't use the mock object.
I'm not sure how exactly Engine class looks like, but to be able to use mock you usualy should design your class to have mocked object dependency.
class Engine
{
private QueryBuilder $qb;
public function __construct(QueryBuilder $qb)
{
$this->qb = $qb;
}
}
And then you pass the mock object in your test case:
$engine = new Engine($mock);
This should work.

Removing some schemas/models from API-Platforms Swagger/OpenAPI documentation output

API-Platform will generate Swagger/OpenAPI route documentation and then below documentation for the Schemas (AKA Models) (the docs show them as "Models" but current versions such as 2.7 show them as "Schemas").
Where is the content generated to show these schemas/models? How can some be removed? The functionality to display them is part of Swagger-UI, but API-Platform must be responsible for providing the JSON configuration and thus which to change API-Platform and not Swagger-UI. Note that this post shows how to add a schema but not how to remove one. Is there any documentation on the subject other than this which doesn't go into detail?
As seen by the output below, I am exposing AbstractOrganization, however, this class is extended by a couple other classes and is not meant to be exposed, but only schemas for the concrete classes should be exposed. Note that my AbstractOrganization entity class is not tagged with #ApiResource and is not shown in the Swagger/OpenAPI routing documentation but only the schema/model documentation.
Thank you
I am pretty certain there are better ways to implement this, however, the following will work and might be helpful for others.
<?php
declare(strict_types=1);
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class OpenApiRouteHider implements OpenApiFactoryInterface {
public function __construct(private OpenApiFactoryInterface $decorated, private TokenStorageInterface $tokenStorage)
{
}
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
// Use $user to determine which ones to remove.
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/tenants'=>['post', 'get'], // Remove only post and get operations
'/tenants/{uuid}'=>['delete'], // Remove only delete operation
'/chart_themes'=>'*',
'/chart_themes/{id}'=>['put', 'delete', 'patch'],
];
}
}

Decorate all services that implement the same interface by default?

I have a growing number of service classes that share a common interface (let's say BarService and BazService, that implement FooInterface).
All of these need to be decorated with the same decorator. Reading the docs, I know that I can do:
services:
App\BarDecorator:
# overrides the App\BarService service
decorates: App\BarService
Since I have to use the same decorator for different services I guess I would need to do:
services:
bar_service_decorator:
class: App\BarDecorator
# overrides the App\BarService service
decorates: App\BarService
baz_service_decorator:
class: App\BarDecorator
# overrides the App\BazService service
decorates: App\BazService
Problem is: this gets repetitive, quickly. And every time a new implementation of FooInterface is created, another set needs to be added to the configuration.
How can I declare that I want to decorate all services that implement FooInterface automatically, without having to declare each one individually?
A compiler pass allows to modify the container programmatically, to alter service definitions or add new ones.
First you'll need a way to locate all implementations of FooInterface. You can do this with the help of autoconfigure:
services:
_instanceof:
App\FooInterface:
tags: ['app.bar_decorated']
Then you'll need to create the compiler pass that collects all FooServices and creates a new decorated definition:
// src/DependencyInjection/Compiler/FooInterfaceDecoratorPass.php
namespace App\DependencyInjection\Compiler;
use App\BarDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FooInterfaceDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(BarDecorator::class)) {
// If the decorator isn't registered in the container you could register it here
return;
}
$taggedServices = $container->findTaggedServiceIds('app.bar_decorated');
foreach ($taggedServices as $id => $tags) {
// skip the decorator, we do it's not self-decorated
if ($id === BarDecorator::class) {
continue;
}
$decoratedServiceId = $this->generateAliasName($id);
// Add the new decorated service.
$container->register($decoratedServiceId, BarDecorator::class)
->setDecoratedService($id)
->setPublic(true)
->setAutowired(true);
}
}
/**
* Generate a snake_case service name from the service class name
*/
private function generateAliasName($serviceName)
{
if (false !== strpos($serviceName, '\\')) {
$parts = explode('\\', $serviceName);
$className = end($parts);
$alias = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($className)));
} else {
$alias = $serviceName;
}
return $alias . '_decorator';
}
}
Finally, register the compiler pass in the kernel:
// src/Kernel.php
use App\DependencyInjection\Compiler\FooInterfaceDecoratorPass;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FooInterfaceDecoratorPass());
}
}
Interesting! I think that's going to be tricky... but maybe with some hints here you might come up with a solution that fits your needs
find all Decorators... not sure if there's an easier way in that case but I use tags for that. So create a DecoratorInterface add auto tag it...
loop through the definitions and and modify and set the decorated service
e. g. in your Kernel or AcmeAwesomeBundle do
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(DecoratorInterface::class)
->addTag('my.decorator.tag');
$decoratorIds = $container->findTaggedServiceIds('my.decorator.tag');
foreach ($decoratorIds as $decoratorId) {
$definition = $container->getDefinition($decoratorId);
$decoratedServiceId = $this->getDecoratedServiceId($definition);
$definition->setDecoratedService($decoratedServiceId);
}
}
private function getDecoratedServiceId(Definition $decoratorDefinition): string
{
// todo
// maybe u can use the arguments here
// e.g. the first arg is always the decoratedService
// might not work because the arguments are not resolved yet?
$arg1 = $decoratorDefinition->getArgument(0);
// or use a static function in your DecoratorInterface like
// public static function getDecoratedServiceId():string;
$class = $decoratorDefinition->getClass();
$decoratedServiceId = $class::getDecoratedServiceId();
return 'myDecoratedServiceId';
}
I'm pretty sure this is not complete yet but let us know how you solved it

Passing data provider to setUp() in PHPUnit

I'm currently trying to pass data from my data provider to the setUp()-method in PHPUnit.
Background: I am using PHPUnit for running frontend-tests in different browsers. The browser should be defined inside the data provider and needs to be known by the setUp()-method.
I understand, that a data provider initially is executed before the setUp()-method (as setUpBeforeClass()) is called. Therefore setUp()-data can not be passed to a data provider. But it should work the other way round, shouldn't it?
Does PHPUnit generate its own temporarily testclasses with data from the data provider "integrated"?
Of course: a workaround could be, to read the XML-file in the setUp()-method again. But that's the last option, I'd consider...
EDIT: Provided a small snippet:
part of dataProvider():
public function dataProvider()
{
$this->xmlCnf = $data['config'];
var_dump($this->xmlCnf); // array with config is exposed
// [...]
}
And the setUp()-method:
protected function setUp()
{
var_dump($this->xmlCnf); // NULL
//[...]
}
In case this is useful to anyone:
The following code should work:
public function dataProvider()
{
return [ [ /* dataset 1 */] , ... ]
}
protected setUp() {
parent::setUp();
$arguments = $this->getProvidedData();
// $arguments should match the provided arguments for this test case
}
/**
* #dataProvider dataProvider
*/
public function testCase(...$arguments) {
}
The getProvidedData method seems to have been available since PHPUnit 5.6 (which was either shortly before or after this question was originally asked)
we can make the xmlCnf to static
private static $xmlCnf;
public function provider(){
self::$xmlCnf = 'hello';
var_dump(self::$xmlCnf); //hello
return [...];
}
public function setUp() {
var_dump(self::$xmlCnf); //hello
parent::setUp();
}

how to use Symfony methods Action excluding the "Action" word

I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.
Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]
Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.

Resources