Rewrite PHPUnit test after the at() matcher deprecation - php

I used to have a PHPUnit code like that (probably not a good one as I am not able to rewrite it):
$authorizator->expects($this->at(0))
->method('isAllowed')
->willReturn($hasSuperPrivilege);
if (!$hasSuperPrivilege) {
$authorizator->expects($this->at(1))
->method('isAllowed')
->willReturn($hasStandardPrivilege);
}
How to achieve the same behavior after the deprecation of at() method?
I don't know how many times the method will be called, so probably the willReturnOnConsecutiveCalls way is not the right one?
If the first call returns false, then the isAllowed() method must be called twice, if it returned true, then only once.
Related issue: https://github.com/sebastianbergmann/phpunit/issues/4297
Tested method:
public function canUserdoSomething(Foo $resource, Identity $identity): bool
{
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'superPrivilege'
)
) {
return true;
}
if (
$this->authorizator->isAllowed(
$identity,
$resource,
'standardPrivilege'
)
) {
return $resource->hasSomeProperty();
}
return false;
}

I'd go with a handwritten fake so you don't have to worry about the number of calls. I did it inline in my example for brevity but if you're using the authorizator in multiple test, it makes sense to extract it into a separate class.
/**
* #covers \MyClass
*/
class MyClassTest extends TestCase
{
private $subject;
private $authorizator;
/**
* #testWith ["superPrivilege", false, true]
* ["standardPrivilege", true, true]
* ["standardPrivilege", false, false]
* ["invalidPrivilege", true, false]
*/
public function testCanUserdoSomething($privelege, $resourceProperty, $expected)
{
$identity = new Identity();
$resource = new Foo($resourceProperty);
$this->authorizator->addPrivilege($identity, $resource, $privelege);
$result = $this->subject->canUserdoSomething($resource, $identity);
self::assertEquals($expected, $result);
}
protected function setUp(): void
{
parent::setUp();
$this->authorizator = new class implements Authorizator {
private $privileges = [];
public function addPrivilege(Identity $identity, Foo $resource, string $privilege): void
{
$this->privileges[] = spl_object_id($identity) . spl_object_id($resource) . $privilege;
}
public function isAllowed(Identity $identity, Foo $resource, string $privilege): bool
{
return in_array(spl_object_id($identity) . spl_object_id($resource) . $privilege, $this->privileges);
}
};
$this->subject = new MyClass($this->authorizator);
}
}

Related

How to use autowired services when using PhpUnit?

Was testing an application and was repeatable getting the infamous new entity not configured to cascade persist error. I was surprised since I wasn't even creating new entities, and after digging into it, it appears to be relate to using different instances of the EntityManager object (I have confirmed that they are working with the same database, however) which I guess makes sense since each test will have a transaction applied. The only way I was able to get rid of the errors was to use the entityManager in the container instead of the autowired ones. While it works, it is a bit of a kludge and I would like to know the right way of doing this. Thank you
namespace App\Tests;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
class MyTest extends ApiTestCase
{
/**
* #dataProvider getData
*/
public function testWhichDoesNotWork(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
/**
* #dataProvider getData
*/
public function testWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$em = static::getContainer()->get(EntityManagerInterface::class);
$user = $service->getUser();
$randomId = $service->getRandomEntityId($user->getTenant(), $class);
$randomEntity = $em->getRepository($class)->find($randomId);
$randomEntity->setSomething('something');
$em->persist($randomEntity);
$em->flush();
}
/**
* #dataProvider getData
*/
public function testAnotherWhichWorks(int $id, string $class)
{
$service = static::getContainer()->get(MyService::class);
$service->setNewEntityManager(static::getContainer()->get(EntityManagerInterface::class));
$user = $service->getUser();
$randomEntity = $service->getRandomEntity($user->getTenant(), $class);
$randomEntity->setSomething('something');
$service->saveEntity($randomEntity);
}
public function getData(): array
{
return [
[123, SomeClass::class]
];
}
}
namespace App\Test\Service;
final class MyService
{
public function __construct(private EntityManagerInterface $entityManager)
{}
public function setNewEntityManager(EntityManagerInterface $entityManager):self
{
$this->entityManager = $entityManager;
return $this;
}
public function getDatabase():string
{
return $this->entityManager->getConnection()->getDatabase();
}
public function getUser(int $id):User
{
return $this->entityManager->getRepository(User::class)->find($id);
}
public function getRandomId(Tenant $tenant, string $class):int
{
$meta = $this->entityManager->getClassMetadata($class);
$_sql = 'SELECT %s FROM public.%s WHERE tenant_id=? OFFSET floor(random() * (SELECT COUNT(*) FROM public.%s WHERE tenant_id=?)) LIMIT 1;';
$sql = sprintf($_sql, $meta->getSingleIdentifierFieldName(), $meta->getTableName(), $meta->getTableName());
return $this->entityManager->getConnection()->prepare($sql)->execute([$tenant->getId(), $tenant->getId()])->fetchOne();
}
public function getRandomEntity(Tenant $tenant, string $class):object
{
return $this->entityManager->getRepository($class)->find($this->getRandomId($tenant, $class));
}
public function saveEntity(object $entity):self
{
$this->entityManager->persist($entity);
$this->flush();
return $this;
}
}
services:
app.test.my.service:
alias: App\Test\Service\MyService
public: true

PHP Nesting functions OOP

I am using a library, and it has the following process to attach many operations to one event:
$action = (new EventBuilder($target))->addOperation($Operation1)->addOperation($Operation2)->addOperation($Operation3)->compile();
I am not sure how to dynamically add operations depending on what I need done.
Something like this
$action = (new EventBuilder($target));
while (some event) {
$action = $action->addOperation($OperationX);
}
$action->compile();
I need to be able to dynamically add operations in while loop and when all have been added run it.
Your proposed solution will work. The EventBuilder provides what is known as a Fluent Interface, which means that there are methods that return an instance of the builder itself, allowing you to chain calls to addOperation as many times as you want, then call the compile method to yield a result. However you are free to ignore the return value of addOperation as long as you have a variable containing an instance of the builder that you can eventually call compile on.
Take a walk with me...
// Some boilerplate classes to work with
class Target
{
private ?string $name;
public function __construct(string $name)
{
$this->name = $name;
}
public function getName(): string
{
return $this->name;
}
}
class Operation
{
private ?string $verb;
public function __construct(string $verb)
{
$this->verb = $verb;
}
public function getVerb(): string
{
return $this->verb;
}
}
class Action
{
private ?Target $target;
private array $operations = [];
public function __construct(Target $target, array $operations)
{
$this->target = $target;
$this->operations = $operations;
}
/**
* Do the things
* #return array
*/
public function run(): array
{
$output = [];
foreach ($this->operations as $currOperation)
{
$output[] = $currOperation->getVerb() . ' the ' . $this->target->getName();
}
return $output;
}
}
Here is a basic explanation of what your EventBuilder is doing under the covers:
class EventBuilder
{
private ?Target $target;
private array $operations = [];
public function __construct(Target $target)
{
$this->target = $target;
}
/**
* #param Operation $operation
* #return $this
*/
public function addOperation(Operation $operation): EventBuilder
{
$this->operations[] = $operation;
// Fluent interface - return a reference to the instance
return $this;
}
public function compile(): Action
{
return new Action($this->target, $this->operations);
}
}
Let's try both techniques and prove they will produce the same result:
// Mock some operations
$myOperations = [
new Operation('Repair'),
new Operation('Clean'),
new Operation('Drive')
];
// Create a target
$target = new Target('Car');
/*
* Since the EventBuilder implements a fluent interface (returns an instance of itself from addOperation),
* we can chain the method calls together and just put a call to compile() at the end, which will return
* an Action instance
*/
$fluentAction = (new EventBuilder($target))
->addOperation($myOperations[0])
->addOperation($myOperations[1])
->addOperation($myOperations[2])
->compile();
// Run the action
$fluentResult = $fluentAction->run();
// Traditional approach, create an instance and call the addOperation method as needed
$builder = new EventBuilder($target);
// Pass our mocked operations
while (($currAction = array_shift($myOperations)))
{
/*
* We can ignore the result from addOperation here, just keep calling the method
* on the builder variable
*/
$builder->addOperation($currAction);
}
/*
* After we've added all of our operations, we can call compile on the builder instance to
* generate our Action.
*/
$traditionalAction = $builder->compile();
// Run the action
$traditionalResult = $traditionalAction->run();
// Verify that the results from both techniques are identical
assert($fluentResult == $traditionalResult, 'Results from both techniques should be identical');
// Enjoy the fruits of our labor
echo json_encode($traditionalResult, JSON_PRETTY_PRINT).PHP_EOL;
Output:
[
"Repair the Car",
"Clean the Car",
"Drive the Car"
]
Rob Ruchte thank you for detailed explanation, one thing I did not include was that each operation itself had ->build() call and I needed to move that to each $builder for it to work.

How can I combine contextual binding and service location?

Disclaimer: I would have preferred a more generic question like "How do I keep track of the state of a recursive method?" on the code review stack exchange site, as that better describes where the problem is currently at. But the constraint on that board is that code must first be working
Background: I have a container that
can recursively hydrate and inject constructor arguments
concretes can be provided to it for use to circumvent hydration
provision context exist in two states: for classes being auto-wired, and for classes using the container as a service locator
Three of these target behaviours function as expected, save when the last 2 are combined.
Main problem: when 3a is followed by 3b, container uses an incorrect context, and I don't know how to inspect that particular state since the hydrator/service locator is recursive. Unit testing the individual methods all work correctly. Integration test of either of the two works. But at the level of multi layer hydration, I can't mock out any of the involved parts, thus, I have no way of determining what context is used there
I feel the problem is more of a philosophical one, where more than one answer is applicable. But instead of down-voting, kindly migrate to the appropriate stack exchange site
The code is in PHP, but if you aren't conversant with it, pseudo-code or a verbal solution is welcome. An acceptable solution may even be a test that demonstrates how to simulate and observe container state after hydration and service location. I already have this, but it fails
public function test_hydrated_class_with_getClass_correctly_uses_needs () {
$ourB = new BCounter; // given
$this->container->provideSelf();
$this->container->whenTypeAny()->needsAny([
BCounter::class => $ourB
]);
$this->assertSame( // then
$this->container->getClass($this->aRequires)
->getInternalB(), // when
$this->ourB
);
}
Relevant parts of the container below
<?php
use ReflectionMethod, ReflectionClass, ReflectionFunction, ReflectionType, ReflectionFunctionAbstract, ReflectionException;
class Container {
const UNIVERSAL_SELECTOR = "*";
private $provisionedNamespaces = [], // NamespaceUnit[]
$hydratingForStack = [], // String[]. Doubles as a dependency chain. #see [lastHydratedFor] for main usage
$internalMethodHydrate = false, // Used when [getMethodParameters] is called directly without going through instance methods such as [instantiateConcrete]
$hydratingArguments = false,
$constructor = "__construct",
$externalHydrators = [], $externalContainerManager,
$interfaceHydrator,
$provisionContext, // the active Type before calling `needs`
$provisionSpace; // same as above, but for namespaces
protected $provisionedClasses = []; // ProvisionUnit[]
public function __construct () {
$this->initializeUniversalProvision();
}
public function initializeUniversalProvision ():void {
$this->provisionedClasses[self::UNIVERSAL_SELECTOR] = new ProvisionUnit;
}
public function getInterfaceHydrator ():InterfaceHydrator {
return $this->interfaceHydrator;
}
/**
* Looks for the given class in this order
* 1) pre-provisioned caller list
* 2) Provisions it afresh if an interface or recursively wires in its constructor dependencies
*
* #param {includeSub} Regular provision: A wants B, but we give C sub-class of B. Sub-classes of A can't obtain B unless this parameter is used
*
* #return A class instance, if found
*/
public function getClass (string $fullName, bool $includeSub = false) {
$concrete = $this->decorateProvidedConcrete($fullName);
if (!is_null($concrete)) return $concrete;
if ($includeSub && $parent = $this->hydrateChildsParent($fullName))
return $parent;
$externalManager = $this->externalContainerManager;
if (
!is_null($externalManager) &&
$concrete = $externalManager->findInManagers($fullName)
) {
$this->saveWhenImplements($fullName, $concrete);
return $concrete;
}
return $this->initializeHydratingForAction($fullName, function ($className) {
if ($this->getReflectedClass($className)->isInterface())
return $this->provideInterface($className);
return $this->instantiateConcrete($className);
});
}
public function decorateProvidedConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction($fullName, function ($className) {
return new HydratedConcrete(
$this->getProvidedConcrete($className),
$this->lastHydratedFor()
);
});
if (!is_null($freshlyCreated->getConcrete()))
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
); // decorator runs on each fetch (rather than only once), since different callers result in different behavior
}
public function getProvidedConcrete (string $fullName) {
$context = $this->getRecursionContext();
if ($context->hasConcrete($fullName))
return $context->getConcrete($fullName);
$globalContext = $this->provisionedClasses[self::UNIVERSAL_SELECTOR];
if ($globalContext->hasConcrete($fullName)) // current provision doesn't include this class. check in global
return $globalContext->getConcrete($fullName);
}
/**
* Switches unit being provided to universal if it doesn't exist
* #return currently available provision unit
*/
public function getRecursionContext ():ProvisionUnit {
$hydrateFor = $this->lastHydratedFor();
if (!array_key_exists($hydrateFor, $this->provisionedClasses))
$hydrateFor = self::UNIVERSAL_SELECTOR;
return $this->provisionedClasses[$hydrateFor];
}
/**
* This tells us the class we are hydrating arguments for
*/
public function lastHydratedFor ():?string {
$stack = $this->hydratingForStack;
if(empty($stack) ) return null;
$index = $this->hydratingArguments ? 2: 1; // If we're hydrating class A -> B -> C, we want to get provisions for B (who, at this point, is indexed -2 while C is -1). otherwise, we'll be looking through C's provisions instead of B
$length = count($stack);
return $stack[$length - $index];
}
/**
* Not explicitly decorating objects from here since it calls [getClass]
*/
private function hydrateChildsParent (string $fullName) {
$providedParent = $this->getProvidedParent($fullName);
if (!is_null($providedParent))
return $this->getClass($providedParent);
}
/**
* #return the first provided parent of the given class
*/
private function getProvidedParent (string $class):?string {
$allSuperiors = array_keys($this->provisionedClasses);
$classSuperiors = array_merge(
class_parents($class, true),
class_implements($class, true)
);
return current(
array_intersect($classSuperiors, $allSuperiors)
);
}
private function saveWhenImplements (string $interface, $concrete):void {
if (!($concrete instanceof $interface))
throw new InvalidImplementor($interface, get_class($concrete));
$this->storeConcrete( $interface, $concrete);
}
private function storeConcrete (string $fullName, $concrete):ProvisionUnit {
return $this->getRecursionContext()->addConcrete($fullName, $concrete);
}
private function getReflectedClass (string $className):ReflectionClass {
try {
return new ReflectionClass($className);
}
catch (ReflectionException $re) {
$message = "Unable to hydrate ". $this->lastHydratedFor() . ": ". $re->getMessage();
$hint = "Hint: Cross-check its dependencies";
throw new HydrationException("$message. $hint");
}
}
/**
* Wrap any call that internally attempts to read from [lastHydratedFor] in this i.e. calls that do some hydration and need to know what context/provision they're being hydrated for
*/
public function initializeHydratingForAction (string $fullName, callable $action) {
$this->initializeHydratingFor($fullName);
$result = $action($fullName);
$this->popHydratingFor($fullName);
return $result;
}
/**
* Tells us who to hydrate arguments for
*/
protected function initializeHydratingFor (string $fullName):void {
$isFirstCall = is_null($this->lastHydratedFor());
$hydrateFor = $isFirstCall ? $this->lastCaller(): $fullName;
$this->pushHydratingFor($hydrateFor);
}
private function lastCaller ():string {
$stack = debug_backtrace (2 ); // 2=> ignore concrete objects and their args
$caller = "class";
foreach ($stack as $execution)
if (array_key_exists($caller, $execution) && $execution[$caller] != get_class()) {
return $execution[$caller];
}
}
/**
* Updates the last element in the context hydrating stack, to that whose provision dependencies should be hydrated for
*/
protected function pushHydratingFor (string $fullName):void {
$this->hydratingForStack[] = $fullName;
}
/**
* #param {completedHydration} To guarantee push-pop consistency. When the name of what is expected to be removed doesn't match the last item in stack, it indicates we're currently hydrating an interface (where its name differs from concretes involved). When this happens, we simply ignore popping our list since those concretes were not the ones that originally got pushed
*/
private function popHydratingFor (string $completedHydration):void {
if (end($this->hydratingForStack) == $completedHydration)
array_pop($this->hydratingForStack);
}
/**
* #throws InvalidImplementor
*
* #return Concrete of the given [Interface] if it was bound
*/
protected function provideInterface (string $interface) {
$caller = $this->lastHydratedFor();
if ($this->hasRenamedSpace($caller)) {
$newIdentity = $this->relocateSpace($interface, $caller);
$concrete = $this->instantiateConcrete($newIdentity);
}
else {
$concrete = $this->getInterfaceHydrator()->deriveConcrete($interface);
if (!is_null($concrete))
$this->saveWhenImplements($interface, $concrete);
}
if (is_null($concrete))
throw new InvalidImplementor($interface, "No matching concrete" );
return $concrete;
}
/**
* A shorter version of [getClass], but neither checks in cache or contextual provisions. This means they're useful to:
* 1) To hydrate classes we're sure doesn't exist in the cache
* 2) In methods that won't be called more than once in the request cycle
* 3) To create objects that are more or less static, or can't be overidden by an extension
*
* All objects internally derived from this trigger decorators if any are applied
*/
public function instantiateConcrete (string $fullName) {
$freshlyCreated = $this->initializeHydratingForAction ($fullName, function ($className) {
if (!method_exists($className, $this->constructor))
return new HydratedConcrete(new $className, $this->lastHydratedFor() );
return $this->hydrateConcreteForCaller($className);
});
$this->storeConcrete($fullName, $freshlyCreated->getConcrete());
return $this->getDecorator()->scopeInjecting(
$freshlyCreated->getConcrete(),
$freshlyCreated->getCreatedFor()
);
}
public function hydrateConcreteForCaller (string $className):HydratedConcrete {
$dependencies = $this->internalMethodGetParameters(function () use ($className) {
return array_values($this->getMethodParameters($this->constructor, $className));
});
return new HydratedConcrete(
new $className (...$dependencies),
$this->lastHydratedFor()
);
}
public function internalMethodGetParameters (callable $action) {
$this->internalMethodHydrate = true;
$result = $action();
$this->internalMethodHydrate = false;
return $result;
}
/**
* Fetch appropriate dependencies for a callable's arguments
*
* #param {callable}:string|Closure
* #param {anchorClass} the class the given method belongs to
*
* #return {Array} associative. Contains hydrated parameters to invoke given callable with
*/
public function getMethodParameters ( $callable, string $anchorClass = null):array {
$context = null;
if (is_null($anchorClass))
$reflectedCallable = new ReflectionFunction($callable);
else {
$reflectedCallable = new ReflectionMethod($anchorClass, $callable);
if (!$this->internalMethodHydrate)
$this->initializeHydratingFor($anchorClass);
$context = $this->getRecursionContext();
}
$dependencies = $this->populateDependencies($reflectedCallable, $context);
if (is_null($anchorClass)) return $dependencies;
elseif (!$this->internalMethodHydrate)
$this->popHydratingFor($anchorClass);
return $this->getDecorator()->scopeArguments( $anchorClass, $dependencies, $callable);
}
public function populateDependencies (ReflectionFunctionAbstract $reflectedCallable, ?ProvisionUnit $callerProvision):array {
$dependencies = [];
foreach ($reflectedCallable->getParameters() as $parameter) {
$parameterName = $parameter->getName();
$parameterType = $parameter->getType();
if (!is_null($callerProvision) )
$dependencies[$parameterName] = $this->hydrateProvidedParameter($callerProvision, $parameterType, $parameterName);
elseif (!is_null($parameterType))
$dependencies[$parameterName] = $this->hydrateUnprovidedParameter($parameterType);
elseif ($parameter->isOptional() )
$dependencies[$parameterName] = $parameter->getDefaultValue();
else $dependencies[$parameterName] = null;
}
return $dependencies;
}
/**
* Pulls out a provided instance of a dependency when present, or creates a fresh one
*
* #return object matching type at given parameter
*/
private function hydrateProvidedParameter (ProvisionUnit $callerProvision, ReflectionType $parameterType, string $parameterName) {
if ($callerProvision->hasArgument($parameterName))
return $callerProvision->getArgument($parameterName);
$typeName = $parameterType->getName();
if ($callerProvision->hasArgument($typeName))
return $callerProvision->getArgument($typeName);
return $this->hydrateUnprovidedParameter($parameterType);
}
private function hydrateUnprovidedParameter (ReflectionType $parameterType) {
$typeName = $parameterType->getName();
if ( $parameterType->isBuiltin()) {
$defaultValue = null;
settype($defaultValue, $typeName);
return $defaultValue;
}
if (!in_array($typeName, $this->hydratingForStack)) {
$this->hydratingArguments = true;
$concrete = $this->getClass($typeName);
$this->hydratingArguments = false;
return $concrete;
}
if ($this->getReflectedClass($typeName)->isInterface())
throw new HydrationException ("$typeName's concrete cannot depend on its dependency's concrete");
trigger_error("Circular dependency detected while hydrating $typeName", E_USER_WARNING);
}
public function whenType (string $toProvision):self {
if (!array_key_exists($toProvision, $this->provisionedClasses))
$this->provisionedClasses[$toProvision] = new ProvisionUnit;
$this->provisionContext = $toProvision;
return $this;
}
public function whenTypeAny ():self {
return $this->whenType(self::UNIVERSAL_SELECTOR);
}
public function needs (array $dependencyList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateConcretes($dependencyList);
return $this;
}
public function needsAny (array $dependencyList):self {
$this->needs($dependencyList)
->needsArguments($dependencyList);
$this->provisionContext = null;
return $this;
}
public function needsArguments (array $argumentList):self {
if (is_null ($this->provisionContext))
throw new HydrationException("Undefined provisionContext");
$this->provisionedClasses[$this->provisionContext]->updateArguments($argumentList);
return $this;
}
public function provideSelf ():void {
$this->whenTypeAny()->needsAny([get_class() => $this]);
}
protected function getDecorator ():DecoratorHydrator {
return $this->decorator;
}
}
?>
The relevant parts are:
decorateProvidedConcrete (who I want to assert was called twice, but by mocking, getClass can no longer function)
public function test_hydrated_class_with_getClass_reads_provision () {
// given
$container = $this->positiveDouble(Container::class, [
"getDecorator" => $this->stubDecorator(),
"getProvidedConcrete" => $this->returnCallback(function ($subject) {
return $this->positiveDouble($subject, []); // return a stub
})
], [
"getProvidedConcrete" => [2, [
$this->callback(function ($subject) {
return BCounter::class == $subject; // this obviously won't work, since method attempts to hydrate other classes, as well
})
]] // then 1
]);
$this->bootContainer($container);
$this->entityBindings();
// when
$container->getClass($this->aRequires)->getInternalB();
}
getRecursionContext, who I have no way of observing until getClass returns. But I would like to know what context it's working with by the time we're doing the service location. And, that's difficult to figure out since getClass is recursive
Finally, ARequiresBCounter. I want to DI this class with provided BCounter. Then when getInternalB runs, it equally uses the provided BCounter
class ARequiresBCounter {
private $b1, $container, $primitive;
public function __construct (BCounter $b1, Container $container, string $primitive) {
$this->b1 = $b1;
$this->container = $container;
$this->primitive = $primitive;
}
public function getConstructorB ():BCounter {
return $this->b1;
}
public function getInternalB ():BCounter {
return $this->container->getClass(BCounter::class);
}
}

PHP/Laravel Interface for API class realization

I have an interface with search function:
interface Searcher
{
public function search($text,$limit)
}
I have some realization based on API
class APISeach implements Searcher
{
public function search($text,$limit)
{
$params = [
'name' => $sName,
'maxRows' => $iLimit,
];
$response = Http::get('http://some_api_service/search', $params)->json();
}
}
And I have some code which used this Search:
class MyController extends Controller
{
private $seacher;
public function __construct(Searcher $mySeacher)
{
$this->seacher = $mySeacher;
}
public function search($text,$limit=10)
{
$this->seacher->search($text,$limit)
}
}
All looks fine (may be). But what if I need change API realization and it will be required another parameters. For example:
class AnotherAPISeach implements Searcher
{
public function search($text,$language,$fuzzy,$limit)
{
$ApiObjectFromLib = new ApiObjectFromLib();
$response = $ApiObjectFromLib->search($text,$language,$fuzzy,$limit)->json();
}
}
So it's can not implement Searcher interface any more.
Is it exists any way to use interfaces for API functions? All API can required various parameters and it's no good if I need change Interface or Controller code for each API.
You can use variadic arguments
interface Searcher
{
public function search($text, $limit, ...$additional);
}
If that defeats the purpose of the interface is up to you to decide 😉
​​​​​​​
How about something like this (demo)?
public function search(string $name, int $limit = \Search\Limit::DEFAULT)
{
return $this->api->search(
new \Search\Name($name),
new \Search\Limit($limit)
);
}
public function lookup(
string $name,
string $language = \Search\Language::DEFAULT,
bool $fuzzy = \Search\Fuzzy::DEFAULT,
int $limit = \Search\Limit::DEFAULT
) {
return $this->api->search(
new \Search\Name($name),
new \Search\Language($language),
new \Search\Fuzzy($fuzzy),
new \Search\Limit($limit)
);
}
These "specifications" look like this:
namespace Search
{
interface Specification
{
public function __invoke(array $params): array;
}
class Name implements Specification
{
private $name = null;
public function __construct(string $name)
{
$this->name = $name;
}
public function __invoke(array $params): array
{
return [
'name' => $this->name,
];
}
}
class Language implements Specification
{
const DEFAULT = 'en';
private $language = null;
public function __construct(string $language)
{
$this->language = $language;
}
public function __invoke(array $params): array
{
return [
'language' => $this->language ?? 'en',
];
}
}
class Fuzzy implements Specification
{
const DEFAULT = true;
private $fuzzy = null;
public function __construct(bool $fuzzy)
{
$this->fuzzy = $fuzzy;
}
public function __invoke(array $params): array
{
return [
'fuzzy' => $this->fuzzy,
];
}
}
class Limit implements Specification
{
const DEFAULT = 10;
private $max = null;
public function __construct(int $limit)
{
$this->limit = $limit;
}
public function __invoke(array $params): array
{
return [
'maxRows' => $this->limit ?: self::DEFAULT,
];
}
}
}
Which the searchable API composes like this:
interface Searchable
{
public function search(\Search\Specification... $criteria);
}
class Search implements Searchable
{
private $url = '/search';
private $defaults = [
'maxRows' => \Search\Limit::DEFAULT,
];
public function search(\Search\Specification ...$criteria)
{
return \Http::get($this->url, array_reduce(
$criteria,
fn($params, $criteria) => $criteria($params) + $params,
$this->defaults
))->json();
}
}
The Specification Pattern is interesting, since it implies that a request into a domain is really just a chain of decisions that result in a configuration that can be applied elsewhere.
For instance, note how above the $criteria($params) objects are each given the current $params for the request, for which it may override parameters, read and modify a parameter, or potentially incorporate a Specification check to validate parameters.
Note on the array + array syntax, which is a way to merge arrays:
['foo' => 'bar'] + ['foo' => 'baz'] // left takes precedence: ['foo' => 'bar']
Filter/Criteria is very similar; I tend to think of those having a tighter link to the object it's applied to (Repository, Query or Collection) than Specification, which in my mind applies more to what's to be gotten back.

Codecoverage phpunit test issue

I am running phpunit version 9.2 and I would like to know why my method is not covered in the phpunit coverage.
This is my class:
class Methods extends Template
{
const DISABLED_PAYMENT_METHODS_1 = 'free';
const DISABLED_PAYMENT_METHODS_2 = 'adyen_cc';
const DISABLED_PAYMENT_METHODS_3 = 'adyen_oneclick';
protected $paymentMethodList;
protected $storeManager;
protected $logger;
public function __construct(
Context $context,
PaymentMethodList $paymentMethodList,
StoreManagerInterface $storeManager,
LoggerInterface $logger,
array $data = []
) {
$this->paymentMethodList = $paymentMethodList;
$this->storeManager = $storeManager;
$this->logger = $logger;
parent::__construct($context, $data);
}
public function getPaymentMethods()
{
try {
$storeId = $this->storeManager->getStore()->getId();
$paymentList = $this->paymentMethodList->getActiveList($storeId);
$resultPayments = [];
foreach ($paymentList as $payment) {
if ($payment->getCode() !== self::DISABLED_PAYMENT_METHODS_1 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_2 &&
$payment->getCode() !== self::DISABLED_PAYMENT_METHODS_3
) {
$resultPayments[] = $payment;
}
}
return $resultPayments;
} catch (Exception $e) {
$this->logger->error($e->getMessage());
return false;
}
}
}
and this is my test class:
class MethodsTest extends TestCase
{
private $model;
private function getSimpleMock($originalClassName)
{
return $this->getMockBuilder($originalClassName)
->disableOriginalConstructor()
->getMock();
}
public function setUp() : void
{
$context = $this->getSimpleMock(Context::class);
$paymentMethodList = $this->getSimpleMock(PaymentMethodList::class);
$storeManager = $this->getSimpleMock(StoreManagerInterface::class);
$logger = $this->getSimpleMock(LoggerInterface::class);
$this->model = new Methods(
$context,
$paymentMethodList,
$storeManager,
$logger,
[]
);
}
public function testGetPaymentMethods()
{
$stub = $this->createMock(Methods::class);
$stub->method('getPaymentMethods')
->willReturn([]);
try {
$stub->getPaymentMethods();
$this->fail("Expected exception!");
} catch (\Exception $error) {
$this->assertEquals("Expected exception!", $error->getMessage());
}
}
}
When I run the command to get the coverage. I am getting:
I am really curious why my test is not covered or at least the exception part ? Would you please share you ideas why ? and what can i do in order to fix this ? Right now I got a 29 % and I would like to get at least 60% coverage.
Thank you
On this line $stub = $this->createMock(Methods::class); you are creating a mock of the Methods class, so not actually testing the real class.
You will need to use the object you created in your setUp() method, and set up mock returns on the dependencies you passed in (perhaps converting some of them to be class properties).
You should test the real class, as example:
public function testGetPaymentMethods()
{
// define a payment
$paymentFreeCode = $this->createMock(Payment::class);
$paymentFreeCode->method('getcode')
->willReturn("free");
// define a payment
$payment = $this->createMock(Payment::class);
$payment->method('getcode')
->willReturn("invalid-code");
$paymentList = [
$paymentFreeCode,
$payment,
];
// define a store
$store = $this->createMock(Store::class);
$store->method('getId')
->willReturn("my-store-id");
// return store from the store manager
$this->storeManager->method('getStore')
->willReturn(myStore);
// return the payment list
$this->paymentMethodList->method('getActiveList')->with("my-store-id")
->willReturn($paymentList);
// call the real class instrumented with mocks
$paymentMethods = $this->model->getPaymentMethods();
$this->assertIsArray($paymentMethods);
$this->assertCount($paymentMethods, 1);
}

Categories