This is my function for which I am trying to test if it throws an exception or not:
public function myFunction(): bool
{
$dateInterval = new \DateTime();
$dateInterval->sub(new \DateInterval('PT24H'));
/** #var \PDOStatement $stmt */
$stmt = $this->pdo->prepare('SELECT SUM(`values`) FROM `event_tracker` WHERE `identifier` = ? AND `mutated_at` >= ? AND `source` = ? AND `customer` = ?');
$stmt->execute([$this->identifier, $dateInterval->getTimestamp(), $this->source, $this->customer]);
$sum = $stmt->fetchColumn(0);
if ($this->checkSourceType($this->source) && $sum >= $this->amountLimitCMS[$this->storeId] ){
$this->exceptionMessage($this->amountLimitCMS[$this->countryIso], $dateInterval->format('H:i d-m-Y'), $this->source);
}
if (!$this->checkSourceType($this->source) && $sum >= $this->amountLimitMagento[$this->storeId] ){
$this->exceptionMessage($this->amountLimitMagento[$this->storeId], $dateInterval->format('H:i d-m-Y'), $this->source);
}
return true;
}
This is my unitTest function:
public function testAmountCheckForCMS()
{
$query = [
'store_id' => 13,
'shipping_countryiso2' => 'DK',
'amount' => 4000,
];
$customer = '000003090';
$source = 'fulfillment_prod';
$container = new Container();
$container['db'] = function ($container) {
return $this->createMock(\PDO::class);
};
$dateInterval = new \DateTime();
$dateInterval->sub(new \DateInterval('PT24H'));
$ordersPerCustomer = new AmountPerCustomer($container, $customer, $query, $source);
$fetchAllMock = $this
->getMockBuilder('PDOStatement')
->setMethods(['execute'])
->getMock();
$fetchAllMock
->expects($this->once())->method('fetchColumn')
->will($this->returnValue($query['amount']));
try {
$ordersPerCustomer->assertPassedCriteria();
$this->fail("Expected Exception has not been raised.");
}catch (\Exception $error) {
$this->assertEquals($error->getMessage(), "Total order amount event given parameters exceed sum {$query['amount']} since {$dateInterval->format('H:i d-m-Y')} from source {$source}");
}
}
As you in see, in my function, which I would like to test, the execute and fetchColumn functions are used. How can I mock them ? Right now, when I run my tests I am getting this error message:
Trying to configure method "fetchColumn" which cannot be configured because it does not exist, has not been specified, is final, or is static
Any idea how can I fix this ? Thank you!
I know I am late here but you need to set that method in mockBuilder so that it can be mocked.
here is the sample code -
$this->getMockBuilder(PDOStatement::class)
->disableOriginalConstructor()
->setMethods(["fetchColumn"])
->getMock();
Related
I've got quite a bizarre situation. I've got a piece of code that never gave any issues in the past. Since last night it behaves like this.
Before updating a model the id of that model goes to true. The function below is from a controller and gets called with a POST request. The request gets validated and when the model has not been exported it gets exported to another system. If the export is successful, the model gets updated with the appropriate values. The id does not get set in any stage of this process.
I've added comments to the code to give you an idea where, what happens.
public function export(Request $request, VeniceService $service, Invoice $invoice)
{
$invoice = $invoice->load([
'user', 'customer', 'extension.referenceValues.definition', 'lines'
]);
$this->enforce([
new CheckCstNum($invoice->customer),
new CheckReferences($invoice->extension),
], 432);
if ($invoice->to_export) {
DB::beginTransaction();
try {
var_dump($invoice->id); // returns the id
$data = $service->export($invoice);
var_dump($invoice->id); // returns the true
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
var_dump($invoice->id); // This returns true
var_dump($invoice); // All the values are correct, except the id, this is set to true
$invoice->save(); // With the id as true, this throws an exception. Duplicate entries for PRIMARY key id, '1'
DB::commit();
$service->attachPdf($invoice, Printer::print($invoice)->output());
} catch (VeniceException $e) {
DB::rollBack();
return $e->render($request);
} catch (\Exception $e) {
DB::rollBack();
return response()->json($e->getMessage(), 500);
}
}
return new InvoiceResource($invoice->refresh()); // returns the invoice, but the id is still true
}
$this->service->export() resolves to this function. Before this happens, the id is still the original id of the model.
public function export($model)
{
return $this->call($model, __FUNCTION__);
}
protected function call($model, $function)
{
$class = $this->getClassName($model);
$method = "{$function}{$class}";
return $this->$method($model);
}
public function exportInvoice($invoice)
{
var_dump($invoice->id); // Returns the id
$veniceInvoice = (new VeniceInvoiceResource($invoice))->toArray(request());
var_dump($invoice->id); // Returns true...
return $this->request('POST', 'venice/invoices/' . $this->bookSales, [
RequestOptions::JSON => $veniceInvoice
]);
}
$veniceInvoice = (new VeniceInvoice($invoice))->toArray(request()); After this line the id is set as true. This really does not make any sense as it has always worked, and the model does not get manipulated in any way.
One last bit of code. But I do not think this has anything to do with the issue.
VeniceInvoiceResource.php
public function toArray($request)
{
$pdf = Printer::print($this->resource)->output();
$lines = $this->customer->standard_base == 10 ? VeniceInvoiceLineIC::collection($this->lines) : VeniceInvoiceLine::collection($this->lines);
$refs = $this->extension->referenceValues->map(function ($item) {
return [
'index' => 0,
'quantity' => 0,
'unit_price' => 0,
'description' => $item->definition->name . ' ' . $item->value,
'vat_code' => 0,
'ic_code' => 0,
];
})->toArray();
$details = array_merge($refs, $lines->toArray($request));
return [
'cst_num' => $this->customer->cst_num,
'book' => ($this->book === 'VKPCN') ? $this->book : config('venice.config.sales.book'),
'amount' => $this->total,
'vat_amount' => $this->total,
'exp_date' => carbon(config('venice.config.sales.date'))->addDays($this->customer->exp_term)->format('d/m/Y'),
'doc_date' => carbon(config('venice.config.sales.date'))->format('d/m/Y'),
'vat_system' => $this->customer->vat_system,
'bf_code' => $this->customer->bf_code,
'doc_type' => ($this->doc_type === 'slsCreditnote') ? 1 : 0,
'pdf' => base64_encode($pdf),
'pdfName' => $this->date->format('Ym') . '-' . $this->id . '.pdf',
'remark' => 'Clockwork ' . $this->date->format('Y F') . ' ' . $this->user->name,
'details' => $details,
];
}
For now I've added a temporary fix to mitigate the issue. I've created a clone of the $invoice. later I set the id of the original invoice to the cloned invoice id.
...
$invoice_copy = clone $invoice;
if ($invoice->to_export) {
DB::beginTransaction();
try {
$data = $service->export($invoice);
$invoice->book_date = Carbon::now();
$invoice->doc_num = $data['doc_num'];
$invoice->sys_num = $data['sys_num'];
$invoice->tsm_num = $data['tsm_num'];
$invoice->to_export = false;
$invoice->is_ticked = false;
$invoice->id = $invoice_copy->id;
$invoice->save();
DB::commit();
...
After a lot of debugging I have pinpointed where the id is set to true. I still don't know why.
In VeniceInvoiceResource $this->id before the PDF generation, the id is still the original invoice id. After the Printer, the id istrue.
If I look at the contructor for the resources, found in Illuminat\Http\Resources\JsonResource (Resource extends JsonResource) I see that $this->resource is set to the incomming value, in this case the $invoice.
/**
* Create a new resource instance.
*
* #param mixed $resource
* #return void
*/
public function __construct($resource)
{
$this->resource = $resource;
}
While in VeniceInvoiceResource $this->resource gets passed to the Printer instance. In the resource $this also has the values of the invoice.
/**
* Load items to print.
*
* #param $items
* #return $this
* #throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function print($items, $toPrint = null)
{
$items = is_array($items) || $items instanceof Collection ? $items : func_get_args();
foreach ($items as $item) {
if ($item instanceof Printable) {
foreach ($item->printData($toPrint) as $key => $data) {
switch($key) {
case 'merge':
$this->mergeOutput($data);
break;
case 'mergeFile':
$this->mergeFile($data);
break;
default:
$this->toPrint[] = $this->view->make($key, $data)->render();
}
}
} elseif ($item instanceof Renderable) {
$this->toPrint[] = $item->render();
} elseif (is_string($item)) {
$this->toPrint[] = $item;
} else {
throw new \InvalidArgumentException('Invalid argument');
}
}
return $this;
}
In the print method, $this->toPrint[] = $this->view->make($key, $data)->render(); gets used in this case. The output method looks like this.
/**
* Get the output as string.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
public function output()
{
return $this->generate();
}
/**
* Generate and merge.
*
* #return string
* #throws \iio\libmergepdf\Exception
*/
protected function generate()
{
$data = !empty($this->toPrint) ? $this->toPrint : [''];
$result = $this->pdf->getOutputFromHtml($data);
if (!empty($this->toMerge)) {
$this->merger->addRaw($result);
foreach ($this->toMerge as $item) {
$this->merger->addRaw($item);
}
$result = $this->merger->merge();
}
$this->toPrint = null;
$this->toMerge = null;
return $result;
}
In the print service nothing gets manipulated, it simply prints collections and items to a PDF format.
The last edit, because I found the line that caused all this. But I don't fully understand why it sets the id to true.
In Printer::print there is a call to a method on the model, printData() this method has an if statement to solve a problem we had with two invoices that needed some special treatment. There was not much time so we decided a simple if statement was sufficient enough for this situation.
if ($this->id = 4128 || $this->id === 4217) {
$vat_amount = 0;
$vat_label = '';
}
if you look closely you see that the first condition is not a condition... There is the problem, and the fix was simple. Remove this if statement as we don't need it any more. The invoices 4128 & 4217 already got printed and are archived. They do not need to be processed anymore.
Looks like you found the issue in your printData() method.
For why id ends up as true, it's due to the differing operator precedences.
The comparison operators (===) have a higher precedence than the logical operator (||), so the comparisons are done before the logical comparison. So, if the comparison operator had been correct, this is what would have been run (parens added for clarity):
($this->id === 4128) || ($this->id === 4217)
However, because the first operator was actually an assignment instead of a comparison, this changed the order of operation. The comparion and logical operators have a higher precedence than the assignment operator, so they are executed first. This is what was actually run (parens added for clarity):
$this->id = (4128 || $this->id === 4217)
So, id got assigned to the result of the logical comparison. Since all non-zero numbers evaluate to true, the logical comparison evaluated to true, and therefore id got set to true.
Recently I have been working on an LDAP authentication provider for MediaWiki. In my mind, I have been trying to tackle this issue for a number of days now and cannot come up with a solution.
Context
The way I have developed this plugin is to allow configuration of a number of servers to which we will connect. If we cannot connect to one server, we will try the next... And so on until all are exhausted.
To facilitate this, I have a function in my class that loops over the servers attempting a connection until one succeeds:
private function connect( LdapAuthenticationRequest $req ) {
$dn = $this->config->get( 'BindDN' )[$req->domain];
$pass = $this->config->get( 'BindPass' )[$req->domain];
$servers = $this->config->get( 'Servers' )[$req->domain];
$encryption = $this->config->get( 'EncryptionType' )[$req->domain];
if ( false === $dn ) {
$msgkey = 'ldapauth-attempt-bind-search';
$bind_with = [ null, null ];
} else {
$msgkey = 'ldapauth-attempt-bind-dn-search';
$bind_with = [ $dn, $pass ];
}
$message = new Message( $msgkey, [
'dn' => "{$dn}#{$req->domain}",
] );
$this->logger->info( $message->text() );
foreach ( $servers as $server ) {
if ( false === $server ) {
continue;
}
$ldap = Ldap::create( 'ext_ldap', [
'host' => $server,
'encryption' => $encryption
] );
// Attempt bind - on failure, throw an exception
try {
call_user_func_array( [ $ldap, 'bind' ], $bind_with );
$this->server = $server;
$this->encryption = $encryption;
// log successful bind
$msgkey = 'ldapauth-bind-success';
$message = wfMessage( $msgkey )->text();
$this->logger->info( $message );
return $ldap;
} catch ( SymException $e ) {
if ( false === $dn ) {
$msgkey = 'ldapauth-no-bind-search';
} else {
$msgkey = 'ldapauth-no-bind-dn-search';
}
$message = new Message( $msgkey, [
'dn' => "{$dn}#{$req->domain}",
] );
$message = $message->text();
$this->logger->info( $message );
$this->logger->debug( $e->getMessage() );
}
}
I have been trying to come up with a better way to do this, one that would permit me to better unit-test this class, but thus far I am drawing blanks.
A big part of the reason that I am stuck on this issue is that Symfony's LDAP adapter is essentially hard-coupled into my code, as the call to connect is a static call into Symfony's codebase. i.e. I cannot pass in a connector instance of some description that would then attempt the connection. Do I simply wrap Ldap::create with my own connection wrapper, perhaps?
Since you are using Symfony, I guess your best bet would be to inject LDap object using the framework's dependency injection. However I am not an expert in Symfony. So as a simple hack, I would do it like this :
private function connect($req)
{
$dn = $this->config->get('BindDN')[$req->domain];
$pass = $this->config->get('BindPass')[$req->domain];
$servers = $this->config->get('Servers')[$req->domain];
$encryption = $this->config->get('EncryptionType')[$req->domain];
if (false === $dn) {
$msgkey = 'ldapauth-attempt-bind-search';
$bind_with = [null, null];
} else {
$msgkey = 'ldapauth-attempt-bind-dn-search';
$bind_with = [$dn, $pass];
}
$message = new Message($msgkey, [
'dn' => "{$dn}#{$req->domain}",
]);
$this->logger->info($message->text());
foreach ($servers as $server) {
if (false === $server) {
continue;
}
$ldap = $this->createLDAPObject($server, $encryption);
// Attempt bind - on failure, throw an exception
try {
call_user_func_array([$ldap, 'bind'], $bind_with);
$this->server = $server;
$this->encryption = $encryption;
// log successful bind
$msgkey = 'ldapauth-bind-success';
$message = wfMessage($msgkey)->text();
$this->logger->info($message);
return $ldap;
} catch (SymException $e) {
if (false === $dn) {
$msgkey = 'ldapauth-no-bind-search';
} else {
$msgkey = 'ldapauth-no-bind-dn-search';
}
$message = new Message($msgkey, [
'dn' => "{$dn}#{$req->domain}",
]);
$message = $message->text();
$this->logger->info($message);
$this->logger->debug($e->getMessage());
}
}
}
/**
* #param $server
* #param $encryption
* #return mixed
*/
public function createLDAPObject($server, $encryption)
{
return Ldap::create('ext_ldap', [
'host' => $server,
'encryption' => $encryption
]);
}
Then, you can mock the member method createLDAPObject instead of mocking the static method Ldap::create, which should be easier.
However, I would recommend that you refactor your code, so that it is more readable and testable.
1- First of all, call_user_func_array() is not really test-friendly, and I think your requirements here are not too dynamic so you can replace that line with $ldap->bind($bind_with[0],$bind_with[1]);
2- Your connect method is too large to be tested. Please read about Code Smells - Long Methods
3- The method can be refactored into smaller version by decoupling the presentation from the logic. Like for example you are getting the Message object to get the text from the $msgkey just to log it, which is not helping in code readability and test-ability.
These are my first thoughts on the thing :)
Happy coding & testing :)
I wanted to know that can we create a phpunit mock when we do not know initially the number of arguments for the method
Normally we would do something like this when we know the number of method invocations
mockResponse->expects($this->exactly(2))
-> method('tmpFunc')
->withConsecutive(['header1'], ['header2']);
What I would like to do is get it to be something more dynamic
function mockMethod($n, $params) // $params is an array of strings
{
$mockResponse = $this->getMockBuilder('PMA\libraries\Response')
->disableOriginalConstructor()
->setMethods(array('tempFunc', 'headersSent'))
->getMock();
if($n > 1)
{
$mockResponse->expects($this->exactly($n))
->method('tempFunc')
->withConsecutive( //todo );
$mockResponse->expects($this->any())
->method('headersSent')
->with()
->will($this->returnValue(false));
}
}
So for example if $n = 2 and $params = array('HTTP/1.1 303 See Other', 'Location: index.php?lang=en') then the function should do this
$mockResponse = $this->getMockBuilder('PMA\libraries\Response')
->disableOriginalConstructor()
->setMethods(array('tempFunc', 'headersSent'))
->getMock();
$mockResponse->expects($this->exactly($n))
->method('tempFunc')
->withConsecutive([$params[1]], [$params[2]]);
$mockResponse->expects($this->any())
->method('headersSent')
->with()
->will($this->returnValue(false));
How should I replace the todo so that if $n = 2 then each string will be sent as an argument to tempFunc().
public function tempFunc($text)
{
header($text);
}
public function headersSent()
{
return headers_sent();
}
I finally got the answer from someone I knew
$header_method = $mockResponse->expects($this->exactly(count($param)))
->method('tmpFunc');
call_user_func_array(array($header_method, 'withConsecutive'), $param);
I´m looking for a solution to make the andX part of a doctrine DQL statement dynamic according to passed arguments:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args) {
// Get the query builder
$qb = Shopware()->Models()->createQueryBuilder();
$params = new ArrayCollection();
foreach($args as $key => $arg):
$params->set($key,$arg);
endforeach;
$qb->select('discount')
->from('PhaBase\Models\Discount','discount')
->where(
$qb->expr()->andX(
$qb->expr()->eq('discount.kdNr',':kdNr'),
$qb->expr()->eq('discount.pzn',':pzn'),
$qb->expr()->isNotNull('discount.kdNr'),
$qb->expr()->isNotNull('discount.pzn')
)
)
->setParameters($params->toArray());
$discount = null;
try {
$discount = $qb->getQuery()->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
I want to build the content for the andX() argument in the same way like the Parameters, but I don´t know how to pass dynamic arguments to this method.
Thanks for any ideas,
Michael
Here´s the solution - just found it:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args) {
// Get the query builder
$qb = Shopware()->Models()->createQueryBuilder();
$params = new ArrayCollection();
$conditions = new ArrayCollection();
foreach($args as $key => $arg):
$params->set($key,$arg);
$conditions->add($qb->expr()->eq('discount.'.$key,':'.$key));
endforeach;
$conditions = call_user_func_array(array($qb->expr(), 'andX'), $conditions->toArray());
$qb->select('discount')
->from('PhaBase\Models\Discount','discount')
->where($conditions)
->setParameters($params->toArray());
$discount = null;
try {
$discount = $qb->getQuery()->getOneOrNullResult();
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
// If a discount was found calculate the price for this discount and return it.
if (!is_null($discount)):
$discountPrice = $this->calculateDiscountPrice($article, $discount, $customer);
You can add() additional expressions directly to andX() or orX()
Instead of adding your expressions to an ArrayCollection you can add them directly to an andX expression (and only add that to the main query if there are actually expressions inside of it). Same goes for the required parameters.:
private function getDiscountPrice(int $weighting, Article $article, Customer $customer, array $args)
{
// Early out if there's nothing to do.
if (empty($args)) {
return null;
}
$qb = Shopware()->Models()->createQueryBuilder()
->select('discount')
->from(PhaBase\Models\Discount::class,'discount')
;
$conditions = $qb->expr()->andX();
// Instead of setting each parameter individually (see below) you could set them
// all at once without the need for an ArrayCollection(), like you used, because your
// $args array is already in the correct format.
// $qb->setParameters($args);
foreach($args as $key => $arg) {
$conditions->add('discount.'.$key.' = :'.$key);
$qb->setParameter($key, $arg);
}
// You could check for $conditions->count() > 0 here, but we already did that at the beginning.
try {
return $this->calculateDiscountPrice(
$article,
$qb->where($conditions)->getQuery()->getOneOrNullResult(),
$customer
);
} catch (NonUniqueResultException $e) {
// #TODO Add log entry to inform about Exception
return null;
}
}
A little late to the party but maybe it helps someone else.
Here is a function which uses Zend DB / Tablegateway :
public function listAttestations($sSidx = null, $sSord = null, $iOffset = 0, $iLimit = 0)
{
try {
$resultSet = $this->tableGateway->select(
function (Select $select) use ($sSidx, $sSord, $iOffset, $iLimit) {
if ($sSidx != null && $sSord != null) {
$select->order($sSidx.' '.$sSord);
}
$select->join(
'f_travclient',
'syndic_client_id = f_travclient.travClient_id',
array('syndic' => 'nom')
);
$select->offset($iOffset);
$select->limit($iLimit);
}
);
return $resultSet;
} catch (\Exception $e) {
throw new \Exception($e);
}
}
I use PHPUnit to do unit tests. Perhaps, I don't know how to make the function which crosses my previous method. I thought this could be functional :
public function testListAttestations()
{
$resultSet = new ResultSet();
$mockTableGateway = $this->getMock('Zend\Db\TableGateway\TableGateway', array('select'), array(), '', false);
$mockTableGateway->expects($this->once())
->method('select')
->with()
->will($this->returnValue($resultSet));
$attestTable = new FMaiAttestationTable($mockTableGateway, $this->adapter, $this->sql);
$this->assertSame($resultSet, $attestTable->listAttestations('maiAttestation_id', 'ASC', 0, 30));
}
But this doesn't go further the :
function (Select $select) use ($sSidx, $sSord, $iOffset, $iLimit) {
Could somebody help me ? Thanks.
You can fetch and use any arguments given to a mocked method with returnCallback():
$mockTableGateway->expects($this->once())
->method('select')
->with()
->will($this->returnCallback(function($function) use ($resultSet) {
// do something with the function, for example:
if(!is_callable($function)) {
return NULL;
}
call_user_func($function, new Select());
return $resultSet;
}));
Still, you might want to reconsider your current code as it is not necessary to write it nested like this. You could, for example, get an instance of Select yourself and use it with selectWith()
public function listAttestations($sSidx = null, $sSord = null, $iOffset = 0, $iLimit = 0)
{
try {
$select = new Select();
if ($sSidx != null && $sSord != null) {
$select->order($sSidx.' '.$sSord);
}
$select->join(
'f_travclient',
'syndic_client_id = f_travclient.travClient_id',
array('syndic' => 'nom')
);
$select->offset($iOffset);
$select->limit($iLimit);
$resultSet = $this->tableGateway->selectWith($select);
return $resultSet;
} catch (\Exception $e) {
throw new \Exception($e);
}
}
In this case it is now possible to test if your method puts the Select together the way you want. You can simply create another Select object in your test the way you expect it to be:
$resultSet = new ResultSet();
$sSidx = 'maiAttestation_id';
$sSord = 'ASC';
$iOffset = 0;
$iLimit = 30;
$select = new Select();
$select->order($sSidx.' '.$sSord);
$select->join(
'f_travclient',
'syndic_client_id = f_travclient.travClient_id',
array('syndic' => 'nom')
);
$select->offset($iOffset);
$select->limit($iLimit);
$mockTableGateway = $this->getMock('Zend\Db\TableGateway\TableGateway',
array('selectWith'), array(), '', false);
$mockTableGateway->expects($this->once())
->method('selectWith')
->with($select)
->will($this->returnValue($resultSet));
$attestTable = new FMaiAttestationTable($mockTableGateway, $this->adapter, $this->sql);
$this->assertEquals($resultSet, $attestTable->listAttestations($sSidx, $sSord, $iOffset, $iLimit));
If the Select object used by the mocked method selectWith looks any different from the one you created, PHPUnit will throw an error in your test.