I have a symfony2 application. It abstracts a bunch of external APIs, all of them implementing an ExternalApiInterface.
Each ExternalApiInterface has a lot of methods, e.g. fetchFoo and fetchBar.
Now, I want to write a service that measures the time of each method call of an instance of an ExternalApiInterface.
My current thinking is to implement a StopWatchExternalApiDecorator, that wraps each method call. Yet this approach leads, in my understanding, to code duplication.
I think I am going to use the StopWatch component for the time measurement, yet this feels odd:
class StopWatchExternalApiDecorator implements ExternalApiInterface {
public function __construct(ExternalApiInterface $api, Stopwatch $stopWatch)
{
$this->api = $api;
$this->stopWatch = $stopWatch;
}
public function fetchBar() {
$this->stopWatch->start('fetchBar');
$this->api->fetchBar()
$this->stopWatch->stop('fetchBar');
}
public function fetchFoo() {
$this->stopWatch->start('fetchFoo');
$this->api->fetchFoo()
$this->stopWatch->stop('fetchFoo');
}
}
It seems like I am hurting the DNRY (do not repeat yourself) approach. Am I using the right pattern for this kind of problem, or is there something else more fit? More fit in the sense of: One place to do all the measurement, and no code duplication.
I also dislike of having to touch the decorator in case there will be a new method in the interface. In my mind, that should be independent.
i am thinking of some apis i worked on that use one generic function for calls and a method parameter
heres some very basic pseudocode
public function call($method = 'fetchBar',$params=array()){
$this->stopWatch->start($method);
$this->{"$method"}($params);
$this->stopWatch->stop($method);
}
private function fetchBar(){
echo "yo";
}
maybe that helps
I went with the decorator approach, just on a different level.
In my architecture, api service was using an HttpClientInterface, and each request was handled in the end with a call to doRequest. So there, the decorator made most sense without code duplication:
<?php
namespace Kopernikus\BookingService\Component\Http\Client;
use Kopernikus\BookingService\Component\Performance\PerformanceEntry;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* ProfileClientDecorator
**/
class ProfileClientDecorator implements HttpClientInterface
{
/**
* #var Stopwatch
*/
private $stopwatch;
/**
* #var HttpClientInterface
*/
private $client;
/**
* #var LoggerInterface
*/
private $logger;
/**
* ProfileClientDecorator constructor.
* #param HttpClientInterface $client
* #param Stopwatch $stopwatch
* #param LoggerInterface $logger
*/
public function __construct(HttpClientInterface $client, Stopwatch $stopwatch, LoggerInterface $logger)
{
$this->client = $client;
$this->stopwatch = $stopwatch;
$this->logger = $logger;
}
/**
* #param RequestInterface $request
*
* #return ResponseInterface
*/
public function doRequest(RequestInterface $request)
{
$method = $request->getMethod();
$response = $this->doMeasuredRequest($request, $method);
$performance = $this->getPerformance($method);
$this->logPerformance($performance);
return $response;
}
/**
* #param RequestInterface $request
* #param string $method
*
* #return ResponseInterface
*/
protected function doMeasuredRequest(RequestInterface $request, $method)
{
$this->stopwatch->start($method);
$response = $this->client->doRequest($request);
$this->stopwatch->stop($method);
return $response;
}
/**
* #param $method
* #return PerformanceEntry
*/
protected function getPerformance($method)
{
$event = $this->stopwatch->getEvent($method);
$duration = $event->getDuration();
return new PerformanceEntry($duration, $method);
}
/**
* #param PerformanceEntry $performance
*/
protected function logPerformance(PerformanceEntry $performance)
{
$context = [
'performance' => [
'duration_in_ms' => $performance->getDurationInMs(),
'request_name' => $performance->getRequestName(),
],
];
$this->logger->info(
"The request {$performance->getRequestName()} took {$performance->getDurationInMs()} ms",
$context
);
}
}
And in my services.yml:
performance_client_decorator:
class: Kopernikus\Component\Http\Client\ProfileClientDecorator
decorates: http.guzzle_client
arguments:
- #performance_client_decorator.inner
- #stopwatch
- #logger
Related
I'm currently migrating a ZF2 application to ZF3.
Mostly everything is going smoothly but I'm stuck on one thing.
In my Module.php, I have an ACL management using zend-permissions-acl.
class Module
{
protected $defaultLang = 'fr';
public function onBootstrap(MvcEvent $e)
{
$eventManager = $e->getApplication()->getEventManager();
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
if (!$e->getRequest() instanceof ConsoleRequest){
$eventManager->attach(MvcEvent::EVENT_RENDER_ERROR, array($this, 'onRenderError'));
$eventManager->attach(MvcEvent::EVENT_RENDER, array($this, 'onRender'));
$eventManager->attach(MvcEvent::EVENT_FINISH, array($this, 'onFinish'));
$this->initAcl($e);
$eventManager->attach('route', array($this, 'checkAcl'));
}
}
public function checkAcl(MvcEvent $e) {
$app = $e->getApplication();
$sm = $app->getServiceManager();
$route = $e -> getRouteMatch() -> getMatchedRouteName();
$authService = $sm->get('AuthenticationService');
$jwtService = $sm->get('JwtService');
$translator = $sm->get('translator');
$identity = null;
try {
$identity = $jwtService->getIdentity($e->getRequest());
} catch(\Firebase\JWT\ExpiredException $exception) {
$response = $e->getResponse();
$response->setStatusCode(401);
return $response;
}
if(is_null($identity) && $authService->hasIdentity()) { // no header being passed on... we try to use standard validation
$authService->setJwtMode(false);
$identity = $authService->getIdentity();
}
$userRole = 'default';
$translator->setLocale($this->defaultLang);
if(!is_null($identity))
{
$userRole = $identity->getType();
//check if client or prospect
if($userRole >= User::TYPE_CLIENT)
{
$userManagementRight = UserRight::CREATE_USERS;
if($identity->hasRight($userManagementRight))
$userRole = 'userManagement';
}
$translator->setLocale($identity->getLang());
}
if (!$e->getViewModel()->acl->isAllowed($userRole, null, $route)) {
$response = $e -> getResponse();
$response->setStatusCode(403);
return $response;
}
public function initAcl(MvcEvent $e) {
//here is list of routes allowed
}
}
My issue here is that I'm still using the getServiceManager and therefore getting the deprecated warning : Usage of Zend\ServiceManager\ServiceManager::getServiceLocator is deprecated since v3.0.0;
Basically, I just need to inject dependencies into Module.php.
I guess otherwise I would have to move the checkAcl to the Controller directly and inject the ACL in them ? Not sure what is the proper way of doing this.
Any feedback on this would be greatly appreciated.
Regards,
Robert
To solve the issue you should use a Listener class and Factory. It would also help you with more separation of concerns :)
You seem quite capable of figuring stuff out, judging by your code. As such, I'm just going to give you an example of my own, so you should fill yours in with your own code (I'm also a bit lazy and do not wish to rewrite everything when I can copy/paste my code in ;) )
In your module.config.php:
'listeners' => [
// Listing class here will automatically have them "activated" as listeners
ActiveSessionListener::class,
],
'service_manager' => [
'factories' => [
// The class (might need a) Factory
ActiveSessionListener::class => ActiveSessionListenerFactory::class,
],
],
The Factory
<?php
namespace User\Factory\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use Interop\Container\ContainerInterface;
use User\Listener\ActiveSessionListener;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
class ActiveSessionListenerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var ObjectManager $entityManager */
$entityManager = $container->get(EntityManager::class);
/** #var AuthenticationService $authenticationService */
$authenticationService = $container->get(AuthenticationService::class);
return new ActiveSessionListener($authenticationService, $entityManager);
}
}
The Listener
<?php
namespace User\Listener;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\ORM\EntityManager;
use User\Entity\User;
use Zend\Authentication\AuthenticationService;
use Zend\EventManager\Event;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Mvc\MvcEvent;
/**
* Class ActiveSessionListener
*
* #package User\Listener
*
* Purpose of this class is to make sure that the identity of an active session becomes managed by the EntityManager.
* A User Entity must be in a managed state in the event of any changes to the Entity itself or in relations to/from it.
*/
class ActiveSessionListener implements ListenerAggregateInterface
{
/**
* #var AuthenticationService
*/
protected $authenticationService;
/**
* #var ObjectManager|EntityManager
*/
protected $objectManager;
/**
* #var array
*/
protected $listeners = [];
/**
* CreatedByUserListener constructor.
*
* #param AuthenticationService $authenticationService
* #param ObjectManager $objectManager
*/
public function __construct(AuthenticationService $authenticationService, ObjectManager $objectManager)
{
$this->setAuthenticationService($authenticationService);
$this->setObjectManager($objectManager);
}
/**
* #param EventManagerInterface $events
*/
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$events->attach(MvcEvent::EVENT_ROUTE, [$this, 'haveDoctrineManagerUser'], 1000);
}
/**
* #param Event $event
*
* #throws \Doctrine\Common\Persistence\Mapping\MappingException
* #throws \Doctrine\ORM\ORMException
*/
public function haveDoctrineManagerUser(Event $event)
{
if ($this->getAuthenticationService()->hasIdentity()) {
// Get current unmanaged (by Doctrine) session User
$identity = $this->getAuthenticationService()->getIdentity();
// Merge back into a managed state
$this->getObjectManager()->merge($identity);
$this->getObjectManager()->clear();
// Get the now managed Entity & replace the unmanaged session User by the managed User
$this->getAuthenticationService()->getStorage()->write(
$this->getObjectManager()->find(User::class, $identity->getId())
);
}
}
/**
* #return AuthenticationService
*/
public function getAuthenticationService() : AuthenticationService
{
return $this->authenticationService;
}
/**
* #param AuthenticationService $authenticationService
*
* #return ActiveSessionListener
*/
public function setAuthenticationService(AuthenticationService $authenticationService) : ActiveSessionListener
{
$this->authenticationService = $authenticationService;
return $this;
}
/**
* #return ObjectManager|EntityManager
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* #param ObjectManager|EntityManager $objectManager
*
* #return ActiveSessionListener
*/
public function setObjectManager($objectManager)
{
$this->objectManager = $objectManager;
return $this;
}
}
The important bits:
The Listener class must implement ListenerAggregateInterface
Must be activated in the listeners key of the module configuration
That's it really. You then have the basic building blocks for a Listener.
Apart from the attach function you could take the rest and make that into an abstract class if you'd like. Would save a few lines (read: duplicate code) with multiple Listeners.
NOTE: Above example uses the normal EventManager. With a simple change to the above code you could create "generic" listeners, by attaching them to the SharedEventManager, like so:
/**
* #param EventManagerInterface $events
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$sharedManager = $events->getSharedManager();
$sharedManager->attach(SomeClass::class, EventConstantClass::SOME_STRING_CONSTANT, [$this, 'callbackFunction']);
}
public function callbackFunction (MvcEvent $event) {...}
Currently I am working on a project where we are trying to create a RESTful API. This API uses some default classes, for example the ResourceController, for basic behaviour that can be overwritten when needed.
Lets say we have an API resource route:
Route::apiResource('posts', 'ResourceController');
This route will make use of the ResourceController:
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Repositories\ResourceRepository;
class ResourceController extends Controller
{
/**
* The resource class.
*
* #var string
*/
private $resourceClass = '\\App\\Http\\Resources\\ResourceResource';
/**
* The resource model class.
*
* #var string
*/
private $resourceModelClass;
/**
* The repository.
*
* #var \App\Repositories\ResourceRepository
*/
private $repository;
/**
* ResourceController constructor.
*
* #param \Illuminate\Http\Request $request
* #return void
*/
public function __construct(Request $request)
{
$this->resourceModelClass = $this->getResourceModelClass($request);
$this->repository = new ResourceRepository($this->resourceModelClass);
$exploded = explode('\\', $this->resourceModelClass);
$resourceModelClassName = array_last($exploded);
if (!empty($resourceModelClassName)) {
$resourceClass = '\\App\\Http\\Resources\\' . $resourceModelClassName . 'Resource';
if (class_exists($resourceClass)) {
$this->resourceClass = $resourceClass;
}
}
}
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$this->validate($request, $this->getResourceModelRules());
$resource = $this->repository->create($request->all());
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
$resource = $this->repository->show($id);
$resource = new $this->resourceClass($resource);
return response()->json($resource);
}
...
/**
* Get the model class of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelClass(Request $request)
{
if (is_null($request->route())) return '';
$uri = $request->route()->uri;
$exploded = explode('/', $uri);
$class = str_singular($exploded[1]);
return '\\App\\Models\\' . ucfirst($class);
}
/**
* Get the model rules of the specified resource.
*
* #param \Illuminate\Http\Request $request
* #return string
*/
private function getResourceModelRules()
{
$rules = [];
if (method_exists($this->resourceModelClass, 'rules')) {
$rules = $this->resourceModelClass::rules();
}
return $rules;
}
}
As you can maybe tell we are not making use of model route binding and we make use of a repository to do our logic.
As you can also see we make use of some dirty logic, getResourceModelClass(), to determine the model class needed to perform logic on/with. This method is not really flexible and puts limits on the directory structure of the application (very nasty).
A solution could be adding some information about the model class when registrating the route. This could look like:
Route::apiResource('posts', 'ResourceController', [
'modelClass' => Post::class
]);
However it looks like this is not possible.
Does anybody have any suggestions on how to make this work or how to make our logic more clean and flexible. Flexibility and easy of use are important factors.
The nicest way would be to refactor the ResourceController into an abstract class and have a separate controller that extends it - for each resource.
I'm pretty sure that there is no way of passing some context information in routes file.
But you could bind different instances of repositories to your controller. This is generally a good practice, but relying on URL to resolve it is very hacky.
You'd have to put all the dependencies in the constructor:
public function __construct(string $modelPath, ResourceRepository $repo // ...)
{
$this->resourceModelClass = $this->modelPath;
$this->repository = $repo;
// ...
}
And do this in a service provider:
use App\Repositories\ResourceRepository;
use App\Http\Controllers\ResourceController;
// ... model imports
// ...
public function boot()
{
if (request()->path() === 'posts') {
$this->app->bind(ResourceRepository::class, function ($app) {
return new ResourceRepository(new Post);
});
$this->app->when(ResourceController::class)
->needs('$modelPath')
->give(Post::class);
} else if (request()->path() === 'somethingelse') {
// ...
}
}
This will give you more flexibility, but again, relying on pure URL paths is hacky.
I just showed an example for binding the model path and binding a Repo instance, but if you go down this road, you'll want to move all the instantiating out of the Controller constructor.
After a lot of searching and diving in the source code of Laravel I found out the getResourceAction method in the ResourceRegistrar handles the option passed to the route.
Further searching led me to this post where someone else already managed to extend this registrar en add some custom functionality.
My custom registrar looks like:
<?php
namespace App\Http\Routing;
use Illuminate\Routing\ResourceRegistrar as IlluResourceRegistrar;
class ResourceRegistrar extends IlluResourceRegistrar
{
/**
* Get the action array for a resource route.
*
* #param string $resource
* #param string $controller
* #param string $method
* #param array $options
* #return array
*/
protected function getResourceAction($resource, $controller, $method, $options)
{
$action = parent::getResourceAction($resource, $controller, $method, $options);
if (isset($options['model'])) {
$action['model'] = $options['model'];
}
return $action;
}
}
Do not forget to bind in the AppServiceProvider:
$registrar = new ResourceRegistrar($this->app['router']);
$this->app->bind('Illuminate\Routing\ResourceRegistrar', function () use ($registrar) {
return $registrar;
});
This custom registrar allows the following:
Route::apiResource('posts', 'ResourceController', [
'model' => Post::class
]);
And finally we are able to get our model class:
$resourceModelClass = $request->route()->getAction('model');
No hacky url parse logic anymore!
I had a discussion with my Team Lead, regarding UnitTest, the question was,
In UnitTest do we use Object Mocking or use the Real Object?
I was supporting the Object Mocking concept, as we should only input/output data from Objects.
At the end we agreed to use Real object instead of Mocking so the following was my Test
<?php
namespace App\Services\Checkout\Module\PaymentMethodRules;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
class AdminRule implements PaymentMethodRule
{
/**
* #var boolean
*/
private $isAdmin;
/**
* #var bool
*/
private $isBankTransferAvailable;
/**
* #param boolean $isAdmin
* #param bool $isBankTransferAvailable
*/
public function __construct($isAdmin, $isBankTransferAvailable)
{
$this->isAdmin = $isAdmin;
$this->isBankTransferAvailable = $isBankTransferAvailable;
}
/**
* #param PaymentMethodRuleManager $paymentMethodRuleManager
*/
public function run(PaymentMethodRuleManager $paymentMethodRuleManager)
{
if ($this->isAdmin) {
$paymentMethodRuleManager->getList()->add([Method::INVOICE]);
}
if ($this->isAdmin && $this->isBankTransferAvailable) {
$paymentMethodRuleManager->getList()->add([Method::BANK_TRANSFER]);
}
}
}
<?php
namespace tests\Services\Checkout\Module;
use App\Library\Payment\Method;
use App\Services\Checkout\Module\PaymentMethodList;
use App\Services\Checkout\Module\PaymentMethodRuleManager;
use App\Services\Checkout\Module\PaymentMethodRules\AdminRule;
class AdminRuleTest extends \PHPUnit_Framework_TestCase
{
const IS_ADMIN = true;
const IS_NOT_ADMIN = false;
const IS_BANK_TRANSFER = true;
const IS_NOT_BANK_TRANSFER = false;
/**
* #test
* #dataProvider runDataProvider
*
* #param bool $isAdmin
* #param bool $isBankTransferAvailable
* #param array $expected
*/
public function runApplies($isAdmin, $isBankTransferAvailable, $expected)
{
$paymentMethodRuleManager = new PaymentMethodRuleManager(
new PaymentMethodList([]),
new PaymentMethodList([])
);
$adminRule = new AdminRule($isAdmin, $isBankTransferAvailable);
$adminRule->run($paymentMethodRuleManager);
$this->assertEquals($expected, $paymentMethodRuleManager->getList()->get());
}
/**
* #return array
*/
public function runDataProvider()
{
return [
[self::IS_ADMIN, self::IS_BANK_TRANSFER, [Method::INVOICE, Method::BANK_TRANSFER]],
[self::IS_ADMIN, self::IS_NOT_BANK_TRANSFER, [Method::INVOICE]],
[self::IS_NOT_ADMIN, self::IS_BANK_TRANSFER, []],
[self::IS_NOT_ADMIN, self::IS_NOT_BANK_TRANSFER, []]
];
}
}
My question is, in Unit Test should is use Real Objects or Object Mocking and why?
Second Question, the given Unit test is right or wrong in terms of Unit testing.
The generic answer to such a generic question is: you prefer to use as much of "real" code as possible when doing unit tests. Real code should be default, mocked code is the exception!
But of course, there are various valid reasons to use mocking:
The "real" code does not work in your test setup.
You want to use your mocking framework also to verify that certain actions took place
Example: the code that you intend to test makes a call to some remote service (maybe a database server). Of course that means that you need some tests that do the end to end testing. But for many tests, it might be much more convenient to not do that remote call; instead you would use mocking here - to avoid the remote database call.
Alternatively, as suggested by John Joseph; you might also start with mocking all/most dependencies; to then gradually replace mocking with real calls. This process can help with staying focused on testing exactly "that part" that you actually want to test (instead of getting lost in figuring why your tests using "real other code" is giving you troubles).
IMHO I think it would be good if the original code could be tested directly without any mocking as this would make it less error-prone, and would avoid the debate that if the mocked object behaves almost the same as the original one, but we are not living in the world of unicorns anymore, and mocking is a necessary evil or it is not? This remains the question.
So I think I can rephrase your question to be when to use dummy, fake, stub, or mock?
Generally, the aforementioned terms are known as Test doubles.
As a start, you can check this answer here
Some of the cases when test doubles might be good:
The object under test/System Under Test (SUT) a lot of dependencies, that are required for initialization purposes, and these dependencies would not affect the test, so these dependencies can be dummy ones.
/**
* #inheritdoc
*/
protected function setUp()
{
$this->servicesManager = new ServicesManager(
$this->getDummyEntity()
// ........
);
}
/**
* #return \PHPUnit_Framework_MockObject_MockObject
*/
private function getDummyEntity()
{
return $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods([])
->getMock();
}
SUT has an external dependencies such as an Infrastructure/Resource (e.g. web service, database, cash, file …), then it is a good approach to fake that by using in-memory representation, as one of the reasons to do that is to avoid cluttering this Infrastructure/Resource with test data.
/**
* #var ArrayCollection
*/
private $inMemoryRedisDataStore;
/**
* #var DataStoreInterface
*/
private $fakeDataStore;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->inMemoryRedisDataStore = new Collections\ArrayCollection;
$this->fakeDataStore = $this->getFakeRedisDataStore();
$this->sessionHandler = new SessionHanlder($this->fakeDataStore);
}
/**
* #return \PHPUnit_Framework_MockObject_MockObject
*/
private function getFakeRedisDataStore()
{
$fakeRedis = $this->getMockBuilder(
Infrastructure\Memory\Redis::class
)
->disableOriginalConstructor()
->setMethods(['set', 'get'])
->getMock();
$inMemoryRedisDataStore = $this->inMemoryRedisDataStore;
$fakeRedis->method('set')
->will(
$this->returnCallback(
function($key, $data) use ($inMemoryRedisDataStore) {
$inMemoryRedisDataStore[$key] = $data;
}
)
);
$fakeRedis->method('get')
->will(
$this->returnCallback(
function($key) use ($inMemoryRedisDataStore) {
return $inMemoryRedisDataStore[$key];
}
)
);
}
When there is a need of asserting the state of SUT, then stubs become handy. Usually, this would be confused with a fake object, and to clear this out, fake objects are helping objects and they should never be asserted.
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
public function isSent(): bool;
}
/**
* Class SMSProviderStub
*/
class SMSProviderStub implements Provider\SMSProviderInterface
{
/**
* #var bool
*/
private $isSent;
/**
* #inheritdoc
*/
public function send()
{
$this->isSent = true;
}
/**
* #return bool
*/
public function isSent(): bool
{
return $this->isSent;
}
}
/**
* Class PaymentServiceTest
*/
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Service\PaymentService
*/
private $paymentService;
/**
* #var SMSProviderInterface
*/
private $smsProviderStub;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->smsProviderStub = $this->getSMSProviderStub();
$this->paymentService = new Service\PaymentService(
$this->smsProviderStub
);
}
/**
* Checks if the SMS was sent after payment using stub
* (by checking status).
*
* #param float $amount
* #param bool $expected
*
* #dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount, bool $expected)
{
$this->paymentService->pay($amount);
$this->assertEquals($expected, $this->smsProviderStub->isSent());
}
/**
* #return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should return true' => [
'amount' => 28.99,
'expected' => true,
],
];
}
/**
* #return Provider\SMSProviderInterface
*/
private function getSMSProviderStub(): Provider\SMSProviderInterface
{
return new SMSProviderStub();
}
}
If the behavior of SUT should be checked then mocks most probably will come to the rescue or stubs (Test spy), it can be detected as simple as that most probably no assert statements should be found. for example, the mock can be setup to behave like when it get a call to X method with values a, and b return the value Y or expect a method to be called once or N of times, ..etc.
/**
* Interface Provider\SMSProviderInterface
*/
interface SMSProviderInterface
{
public function send();
}
class PaymentServiceTest extends \PHPUnit_Framework_TestCase
{
/**
* #var Service\PaymentService
*/
private $paymentService;
/**
* #inheritdoc
*/
protected function setUp()
{
$this->paymentService = new Service\PaymentService(
$this->getSMSProviderMock()
);
}
/**
* Checks if the SMS was sent after payment using mock
* (by checking behavior).
*
* #param float $amount
*
* #dataProvider sMSAfterPaymentDataProvider
*/
public function testShouldSendSMSAfterPayment(float $amount)
{
$this->paymentService->pay($amount);
}
/**
* #return array
*/
public function sMSAfterPaymentDataProvider(): array
{
return [
'Should check behavior' => [
'amount' => 28.99,
],
];
}
/**
* #return SMSProviderInterface
*/
private function getSMSProviderMock(): SMSProviderInterface
{
$smsProviderMock = $this->getMockBuilder(Provider\SMSProvider::class)
->disableOriginalConstructor()
->setMethods(['send'])
->getMock();
$smsProviderMock->expects($this->once())
->method('send')
->with($this->anything());
}
}
Corner cases
SUT has a lot of dependencies which are dependent on other things, and to avoid this dependency loop as we are only interested in testing some methods, the whole object can be mocked, but with having the ability to forward the calls to the original methods.
$testDouble = $this->getMockBuilder(Entity\Entity1::class)
->disableOriginalConstructor()
->setMethods(null);
As per Ahmed Kamal's answer, it worked as expected.
I tested the below sample.
Foo.php
<?php
class Foo
{
/**
* Tell Foo class Name
* #param string $name
* #return string
*/
public function tellName(string $name = 'Josh'): string
{
return 'Hi ' . $name;
}
}
FooTest.php
<?php
include('Foo.php');
use PHPUnit\Framework\TestCase;
class FooTest extends TestCase
{
/**
* PHPUnit testing with assertEquals
* #return void
*/
public function testTellName()
{
// create the class object
$mockObj = $this->getMockBuilder(Foo::class)
->disableOriginalConstructor()
->setMethods(null)
->getMock();
// get the object function result by passing the method parameter value
// pass different parameter value to get an invalid result
$result = $mockObj->tellName('John');
// validate the result with assertEquals()
$this->assertEquals('Hi John', $result);
}
}
Error and Success results:
Cheers!
Description:
I have a simple class that creates a symlink to a directory of uploaded files that are only available to registered members. It uses the current users session id in order to generate the random directory for the user. Once the user logs out, the symlink is removed. I would like to unit test the functionality of the class.
Question:
How do I go about properly unit testing this class since most functions are private, and I don't see any reason to make them public?
Here is the code for the PHP class:
<?php
namespace Test\BackEnd\MemberBundle\Library;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\HttpKernel\KernelInterface;
class DirectoryProtector
{
/** #var SessionInterface $_session */
private $_session;
/** #var ContainerInterface $_kernel */
private $_kernel;
/**
* #param SessionInterface $session
* #param KernelInterface $kernel
*/
public function __construct( SessionInterface $session, KernelInterface $kernel )
{
$this->_session = $session;
$this->_kernel = $kernel;
}
/**
* #param bool|false $protect
* Public method to symlink directories
*/
public function protectDirectory($protect = FALSE)
{
if ($protect) {
if ( ! $this->doesDirectoryExists())
symlink($this->getAppDir() . '/uploads', $this->getViewableSessionDirectory());
} else {
if ($this->doesDirectoryExists())
unlink($this->getViewableSessionDirectory());
}
}
/**
* #return bool
* Check to see if viewable session directory exists or not
*/
private function doesDirectoryExists()
{
if (file_exists($this->getViewableSessionDirectory()))
return TRUE;
return FALSE;
}
/**
* #return string
* Get viewable session full directory path
*/
private function getViewableSessionDirectory()
{
return $this->getAppDir() . '/../web/files/' . $this->getSessionId();
}
/**
* #return string
* Return app root directory
*/
private function getAppDir()
{
return $this->_kernel->getRootDir();
}
/**
* #return string
* Return session id
*/
private function getSessionId()
{
return $this->_session->getId();
}
}
Here is the code for the current test class:
<?php
namespace Test\BackEnd\MemberBundle\Tests\Library;
use Test\BackEnd\MemberBundle\Library\DirectoryProtector;
class DirectoryProtectorTest extends \PHPUnit_Framework_TestCase
{
public function testProtectDirectory()
{
//$this->markTestIncomplete("WIP on protect directory.");
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\SessionInterface')
->getMock();
$container = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')
->getMock();
/** #var DirectoryProtector $dp */
$dp = $this->getMockBuilder('Test\BackEnd\MemberBundle\Library\DirectoryProtector')
->setConstructorArgs(array($request, $container))
->setMethods(array(
'getViewableSessionDirectory',
'getAppDir',
'getSessionId'
))
->getMock();
$dp->expects($this->once())
->method('doesDirectoryExists')
->will($this->returnValue(TRUE));
$dp->protectDirectory(TRUE);
}
}
From https://phpunit.de/manual/current/en/test-doubles.html
Limitation: final, private, and static methods
Please note that final, private and static methods cannot be stubbed or mocked. They are ignored by PHPUnit's test double functionality and retain their original behavior.
Is not a good practice to unit test private or protected methods. You should test the public API. Private methods are supposed to be tested indirectly through the API. That said, you can make the method public with reflection:
$instance = new DirectoryProtector(...);
$ref = new \ReflectionClass('DirectoryProtector');
$method = $ref->getMethod('doesDirectoryExists');
$method->setAccessible(true);
$this->assertTrue($method->invoke($instance));
I think your approach is incorrect. You shouldn't mock method, but objects that are passed by injection in constructor.
For the SessionInterface you can pass MockArraySessionStorage. For ContainerInterface I don't know which class you are using, so I add some demo example. Test case:
public function testProtectDirectory()
{
$session = new MockArraySessionStorage();
$session->setId('123123');
$kernel = \Ouzo\Tests\Mock\Mock::create('ContainerInterface');
\Ouzo\Tests\Mock\Mock::when($kernel)->getRootDir()->thenReturn('/root_dir');
$directoryProtector = new DirectoryProtector($session, $kernel);
$directoryProtector->protectDirectory(true);
//asserts
}
and your methods will be returning:
getViewableSessionDirectory -> /root_dir/../web/files/123123
getAppDir -> /root_dir
And remember changing methods accessible is always bad idea.
PS. For mocking I use utils form Ouzo framework. Of course you can use other mocking framework.
I need to add a function in Symfony2 that has to be called on each request. (language detection on requestion & session)
I thought to do this in the constructor of my Controller classes, but there the container is not known / created.
Have you suggestions for this?
You can define your Event Listener
Please, read documentation about event listeners creation.
Here is a listener that redirects to a page with the language set in the user configuration. Adapt it to your needs.
<?php
namespace MyVendor\Listener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Routing\RouterInterface;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Observe;
/**
* #Service
*/
class LanguageListener
{
/**
* #var \Symfony\Component\Security\Core\SecurityContextInterface
*/
private $securityContext;
/**
* #var \Symfony\Component\Routing\RouterInterface
*/
private $router;
/**
* #InjectParams
*
* #param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
* #param \Symfony\Component\Routing\RouterInterface $router
*/
public function __construct(
SecurityContextInterface $securityContext,
RouterInterface $router
) {
$this->securityContext = $securityContext;
$this->router = $router;
}
/**
* #Observe("kernel.request")
*
* #param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
*/
public function forceLanguage(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
$token = $this->securityContext->getToken();
if (!$token) {
return;
}
if (!$this->securityContext->isGranted('ROLE_USER')) {
return;
}
/** #var $request \Symfony\Component\HttpFoundation\Request */
$request = $event->getRequest();
$locale = $request->getLocale();
$route = $request->get('_route');
if ('_' === $route[0]) {
return;
}
/** #var $user \MyVendor\Model\User */
$user = $token->getUser();
if ($user->getConfig()->getLanguage() !== $locale) {
$parameters = array_merge($request->attributes->get('_route_params'), [
'_locale' => $user->getConfig()->getLanguage(),
]);
$path = $this->router->generate($route, $parameters);
$event->setResponse(new RedirectResponse($path));
}
}
}
I believe that it depends on what you are trying to do. For language detection most of the time symfony and its bundles handle virtually everything. That means that if you want to customize the routing you have to extend the routing component by using routing.loader tag..
However if you can use event listeners but I am not sure how many stuff you can change from there.
Either use events as suggested above or if you need something quick.
You can override setContainer method.
namespace My\Namespace;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyController extends Controller
{
private $foo;
public function setContainer(ContainerInterface $container = null)
{
parent::setContainer($container);
$this->foo = 'bar';
}
// your actions
}