I have the following function within an object that I wish to test
public function getUser($credentials, UserProviderInterface $userProvider)
{
$resourceOwner = $this->getAuthClient()->fetchUserFromToken($credentials);
// ... Code to create user if non-existent, update values, etc.
// ... Basically the code I want to test is here
return $user;
}
The getAuthClient() call returns a Client object with the available function of fetchUserFromToken
How can I, in a PHPUnit test, mock the fetchUserFromToken to just return a ResourceOwner object? Because the actual function does a lot of authentication mechanisms and is out of the scope of this test case
I found a php library called Runkit, but that is not the approach I want to peruse. It feels hacky and overkill for the problem at hand.
The getAuthClient() function is defined as follows
private function getAuthClient() {
return $this->clients->getClient('auth');
}
With $this->clients being defined by the constructor
public function __construct(ClientRepo $clients) {
$this->clients = $clients;
}
So, I mocked the ClientRepo and exposed the getClient() method to return a Mock of the AuthClient (regardless of input) so that I can control the return of fetchUserFromToken() call.
public function testGetUser() {
$client = $this->createMock(WebdevClient::class);
$client->expects($this->any())
->method('fetchUserFromToken')
->will($this->returnCallback(function()
{
// TARGET CODE
}));
$clients = $this->createMock(ClientRegistry::class);
$clients->expects($this->any())
->method('getClient')
->will($this->returnCallback(function() use ($client)
{
return $client;
}));
$object = new TargetObject($clients);
$result = $object->getUser(...);
// ... Assertions to follow
}
Related
PostController's store method which calls the service class and service class calls the third party api i.e. line. while storing a post. i want to write testcase if the notify field is true then it sends notification to the user's through line if not then return with error message. i am not getting any idea how to perform this test. here is the code for PostController.php
private Post $post;
private PostService $service;
public function __construct(
PostService $service,
Post $post
) {
$this->service = $service;
$this->post = $post;
}
public function store(PostRequest $request): RedirectResponse
{
$post = $this->service->createPost($request->validated());
if ($request->notify) {
$message = 'lorem ipsum';
$this->service->lineSendToGroup($request->category_big_id, $message);
}
return redirect()->to('/posts')->with('success_message', 'Post created successfully.');
}
PostService.php
use App\Library\Line;
use App\Models\CategoryBig;
class PostService
{
private CategoryBig $categoryBig;
public function __construct(
CategoryBig $categoryBig,
) {
$this->categoryBig = $categoryBig;
}
public function lineSendToGroup(int $categoryBigId, string $message): void
{
$catB = $this->findOrFailCategoryBig($categoryBigId);
Line::send(
$catB->line_message_channel_secret,
$catB->line_message_channel_access_token,
$catB->line_group_id,
$message
);
}
public function findOrFailCategoryBig(int $categoryBigId): CategoryBig
{
return $this->categoryBig->whereId($categoryBigId)->firstOrFail();
}
public function createPost(array $createData): Post
{
$createData += [$data];
return $this->greeting->create($createData);
}
Line.php
namespace App\Library;
use Illuminate\Support\Carbon;
use LINE\LINEBot;
use LINE\LINEBot\HTTPClient\CurlHTTPClient;
use LINE\LINEBot\Event\MessageEvent\TextMessage;
use LINE\LINEBot\MessageBuilder\TextMessageBuilder;
use Log;
class Line
{
public static function send($channel_secret, $access_token, $line_user_id, $message)
{
$http_client = new CurlHTTPClient($access_token);
$bot = new LINEBot($http_client, ['channelSecret' => $channel_secret]);
$textMessageBuilder = new TextMessageBuilder($message);
$response = $bot->pushMessage($line_user_id, $textMessageBuilder);
if ($response->isSucceeded()) {
return true;
} else {
return false;
}
}
}
Test
public function test_notification_when_LINE_notification_is_specified()
{
}
Step 1)
You can write a unit test for Line class.
For ease of use, you can refactor this class and use Laravel Http client instead. Create different stubs for each line's response (e.g 200) then use Laravel testing helpers to mock resposne.
Step 2)
Write a feature test for your controller. In this test you don't care about internal functionality of Line class and just want to make sure method of this class is called with proper arguments.
You can use mockery to achieve that.
Migrate to non-static method on Line class. You can't mock static methods.
For happy path test:
$mock = Mockery::mock(\App\Library\Line::class);
$mock->shouldReceive('send')
->once()
->with($secret, $token, $groupId, $message)
->andReturn(true);
// of course mocking object doesn't do much alone and you need to bind and resolve it.
$this->app->instance(\App\Library\Line::class, $mock);
Then resolve it inside your PostService#lineSendToGroup
resolve(Line::class, [
'token' => $token,
// ... other args
])
For opposite scenario use shouldNotReceive('send') instead.
I have a save function for create new document with mongoDB
public function save(User $user): User
{
$result = $this->usersCollection->insertOne($user->getUser());
$user->setId($result->getInsertedId());
return $user;
}
And Change __construct for implemet test
public function __construct($db = null)
{
if (is_null($db)) {
parent::__construct();
} else {
$this->db = $db;
}
$this->usersCollection = $this->db->users;
}
I write this test for save function
public function testSave()
{
$mongo = \Mockery::mock('MongoDB\Client');
$mongo->shouldReceive('insertOne')->andReturn("ok");
$mongo->shouldReceive('selectDatabase')->andReturnSelf();
$user = new User('jack', '0015005050');
$um = new UserMongoDB($mongo);
$res = $um->save($user);
}
everything works well but my problem is $result->getInsertedId() How to I can Mock this function?
Error : Call to a member function getInsertedId() on string
The return type of the insertOne method must be an instance of InsertOneResult (see docs). At the moment you are returning the string "ok". You could keep going and make insertOne return a mock of InsertOneResult. This may work but you are at the gate to mocking hell. Personally, I'd write integration tests for the save method. Mocking the save method in other unit tests is way easier than mocking the low level MongoDB stuff all over the place.
I'm trying to get my head around type hinting in combination with adapters.
The system fetches XML feeds via different services and updates the database with the changes - I am refactoring to help learn design patterns.
Log Interface:
interface LoggerAdapterInterface {
public function debug($string);
public function info($string);
public function error($string);
}
MonoLog Adapter
class MonoLogAdapter implements LoggerAdapterInterface
{
public $logger;
public function __construct(\Monolog\Logger $logger)
{
$this->logger = $logger;
}
public function debug($string)
{
$this->logger->debug($string);
}
public function info($string)
{
$this->logger->info($string);
}
public function error($string)
{
$this->logger->error($string);
}
}
FeedFactory
class FeedFactory
{
public function __construct()
{
}
public static function build(LoggerAdapter $logger, $feedType)
{
// eg, $feedType = 'Xml2u'
$className = 'Feed' . ucfirst($feedType);
// eg, returns FeedXml2u
return new $className($logger);
}
}
Implementation
// get mono logger
$monoLogger = $this->getLogger();
// create adapter and inject monologger
$loggerAdapter = new MonoLogAdapter($monoLogger);
// build feed object
$Feed = FeedFactory::build($loggerAdapter, 'Xml2u');
Error
PHP Catchable fatal error: Argument 1 passed to FeedFactory::build()
must be an instance of LoggerAdapter, instance of MonoLogAdapter
given, called in /src/shell/feedShell.php on line 64 and defined in
/src/Feeds/FeedFactory.php on line 25
So I am using the LoggerAdapter so that I am not tied to one logging platform. The problem is that when I create a new instance of MonoLogger and try to inject it into the factory - PHP type-hinting does not realize that MonoLogger implements LoggerAdapter.
Am I doing something wrong here?
As #Ironcache suggested - use interface as argument in your build method.
public static function build(LoggerAdapterInterface $logger, $feedType)
{
// eg, $feedType = 'Xml2u'
$className = 'Feed' . ucfirst($feedType);
// eg, returns FeedXml2u
return new $className($logger);
}
note: also check namespace
In my LoadFixture.php, I add reference to all my fixtures like this :
public function load(ObjectManager $manager) {
$user = new user("Dummy");
$this->persist($user);
$this->addReference("user", $user);
}
In my test class I load them like this :
public function setUp() {
if(self::$do_setup){
$this->loadFixtures(array(
"Bundle\\Tests\\Fixtures\\LoadUser"
)) ;
}
}
In my tests I use them like this :
public function testOne() {
$client = $this->createClient($this->getReference("user_a"));
$client->request('GET', '/');
$this->assertStatusCode(200, $client);
self::$do_setup=false;
}
public function testTwo() {
$client = $this->createClient($this->getReference("user_a"));
$client->request('GET', '/home');
$this->assertStatusCode(200, $client);
}
The thing is, technically, I dont need to use setUp() for each test, so I use $do_setup and a if to execute setUp if needed.
But if I dont execute the setUp() in my testTwo, while my fixtures are in my database, $this->getReference("user_a") is giving me an error :
Call to a member function getReferenceRepository() on a non-object
How can I solve that ?
UPDATE
I have found a solution. So I post it here, just in case someone face the same problem as me.
Many thanks to #Damien Flament for his answer, regarding the fact that the TestCase is deleted after each test.
I changed the name of my setUp() method to open(), and my tearDown() method to close().
The first method of the class call the open() method, and now return $this.
The next method is annoted #depends testOne and take a parameter.
With this parameter I can use my references again.
Ex :
// new setUp Metod
public function open() {
if(self::$do_setup){
$this->loadFixtures(array(
"Bundle\\Tests\\Fixtures\\LoadUser"
)) ;
}
}
//new tearDown method
public function close() {
$this->getContainer()->get('doctrine.orm.entity_manager')->getConnection()->close();
}
public function testOne() {
$this->open();
$client = $this->createClient($this->getReference("user_a"));
$client->request('GET', '/');
$this->assertStatusCode(200, $client);
return $this;
}
/**
* #depends testOne
*/
public function testTwo($it) {
$client = $this->createClient($it->getReference("user_a"));
$client->request('GET', '/home');
$this->assertStatusCode(200, $client);
return $it;
}
/**
* #depends testTwo
*/
public function testThree($it) {
$client = $this->createClient($it->getReference("user_a"));
$client->request('GET', '/about');
$this->assertStatusCode(200, $client);
$this->close();
}
I think the TestCase object is deleted and recreated by PHPUnit (I didn't read the PHPUnit source code, but I think it's the more easy way to reset the testing environment for each test).
So your object (probably referenced by a test class object attribute) is probably garbage collected.
To setup fixture once per test class, use the TestCase::setUpBeforeClass() method.
See documention on "Sharing fixtures".
I'm trying to create my first phpunit test and find myself needing to stub a method on an IMailer interface.
interface IMailer
{
public function send($to, $from, $cc, $subject, $body);
public function sent();
}
$mailer = $this->getMockBuilder(
'IMailer',
array('send', 'sent'))->getMock();
$mailer->method('send')->willRreturn(0);
However, I keep getting
PHP Fatal error:
Call to undefined method Mock_Mailer_13fc0a04::method()
in ...Test.php on line 16
a var_dump($mailer); results in
class Mock_IMailer_4c3e02a7#215 (1) {
private $__phpunit_invocationMocker =>
NULL
}
Working with the expect($this->any()) gives a dito error - it seems that the mocked object does not have any mock functionality...
I'm running phpunit 3.7.28, and php 5.5.9, on an ubuntu box.
How come? How can I fix it?
The getMockBuilder function accepts only the className as parameter. The correct way to initialize your mock object methods would be to use setMethods function (see phpunit docs)
$mailer = $this->getMockBuilder('IMailer')
->setMethods(array('send', 'sent'))
->getMock();
Additionally you probably want to have some expects definition also when you use your mock object:
$mailer->expects($this->any())
->method('send')
->willReturn(0);
EDIT
The above holds true for newer phpunit versions. For phpunit 3.7.28 the mock object usage is a bit different (i.e. the expects seems to be mandatory and willReturn is not yet available). For 3.7.28 version you should modify the second part to:
$mailer->expects($this->any())
->method('send')
->will($this->returnValue(0));
I would recommend updating to later phpunit version as it seems to be somewhat difficult to find documentation to this much older releases.
An alternative solution, for anybody that is still using old versions of PHPUnit, but still wants to be able to call method() directly, is to override the default mock object class template.
Copy MockObject/Generator/mocked_class.tpl.dist, and name the copy mocked_class.tpl. Then, just add the code for the method() method to the template:
public function method()
{
$any = new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
$expects = $this->expects($any);
$args = func_get_args();
return call_user_func_array(array($expects, 'method'), $args);
}
This will allow you to call $mock->method() directly. However, you need to still use ->will($this->returnValue(0)) instead of ->willReturn(0). To do that, you need to introduce a custom invocation builder and invocation mocker:
class My_MockObject_Builder_InvocationMocker
extends PHPUnit_Framework_MockObject_Builder_InvocationMocker {
public function willReturn( $value ) {
return $this->will( new PHPUnit_Framework_MockObject_Stub_Return( $value ) );
}
}
class My_MockObject_InvocationMocker
extends PHPUnit_Framework_MockObject_InvocationMocker {
public function expects( PHPUnit_Framework_MockObject_Matcher_Invocation $matcher ) {
return new My_MockObject_Builder_InvocationMocker($this, $matcher);
}
}
And update your template again, to use My_MockObject_InvocationMocker instead of PHPUnit_Framework_MockObject_InvocationMocker.
The full template would then look like this:
{prologue}{class_declaration}
{
protected static $staticInvocationMocker;
protected $invocationMocker;
{clone}{mocked_methods}
public function expects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher)
{
return $this->__phpunit_getInvocationMocker()->expects($matcher);
}
public function method()
{
$any = new PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount;
$expects = $this->expects($any);
$args = func_get_args();
return call_user_func_array(array($expects, 'method'), $args );
}
public static function staticExpects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher)
{
return self::__phpunit_getStaticInvocationMocker()->expects($matcher);
}
public function __phpunit_getInvocationMocker()
{
if ($this->invocationMocker === NULL) {
$this->invocationMocker = new My_MockObject_InvocationMocker;
}
return $this->invocationMocker;
}
public static function __phpunit_getStaticInvocationMocker()
{
if (self::$staticInvocationMocker === NULL) {
self::$staticInvocationMocker = new My_MockObject_InvocationMocker;
}
return self::$staticInvocationMocker;
}
public function __phpunit_hasMatchers()
{
return self::__phpunit_getStaticInvocationMocker()->hasMatchers() ||
$this->__phpunit_getInvocationMocker()->hasMatchers();
}
public function __phpunit_verify()
{
self::__phpunit_getStaticInvocationMocker()->verify();
$this->__phpunit_getInvocationMocker()->verify();
}
public function __phpunit_cleanup()
{
self::$staticInvocationMocker = NULL;
$this->invocationMocker = NULL;
}
}{epilogue}