Why does PHPSpec fail to test thrown exceptions using ::shouldThrow()? - phpspec

I am trying to test whether a method throws an exception in PHPSpec. Here is the method being tested; it is either running a Callable or a controller's action. I am trying to test whether the exception at the end is being thrown.
function dispatch($target, $params=array())
{
// call closure?
if( is_callable( $target ) ) {
call_user_func_array( $target, $params );
return true;
}
// call controller
list($controllerClass, $actionMethod) = explode('#', $target);
$controllerClass = $this->controllerNamespace . '\\' . $controllerClass;
if (!class_exists($controllerClass)) {
throw new \Exception('Controller not found: '.$controllerClass);
}
}
Here is the PHPSpec test case:
function it_throws_an_exception_if_the_controller_class_isnt_callable()
{
$this->shouldThrow('\Exception')->duringDispatch('Nonexistentclass#Nonexistentmethod', array());
}
This is consistent with the documentation on PHPSpec:
http://www.phpspec.net/en/latest/cookbook/matchers.html#throw-matcher
The problem is if I comment the throw new \Exception line, this test still passes. It doesn't appear to test the method at all. What am I doing wrong?

Create a new exception class, throw it in dispatch() instead of \Exception and check in phpspec if that exception is thrown.
From the behaviour you described I suspect dispatch() throws an \Exception before reaching the if (! class_exists()) statement (even that line could be the culprit if the autoloader throws an exception).
I pasted your function into a class of my project (it happened I was working with phpspec right now) and the spec worked flawless in both cases (when the exception is thrown and when the throw \Exception line was commented out).

Related

phpspec test logic in constructor

I have a logic like this
public function __construct(DataFetcherInterface $fetcher, Alert $alert) {
$this->data = $fetcher->getData();
foreach($this->data as $info) {
try {
$fetcher->assoicateAddress($info);
} catch (Exception $e) {
$alert->sendAlert($info)
}
}
}
and I want to test logic here, for example if sendAlert() will be called in case of exception.
public function it_should_throw_exception(
DataFetcherInterface $fetcher,
Alert $alert
): void {
$fetcher->getData()->willReturn(
// data here
)->shouldBeCalled();
$fetcher->assoicateAddress(
// data here
)->willThrow(Excpetion::class)->shouldBeCalled();
$alert->sendAlert()->shouldBeCalled(
// data here
);
$this->beConstructedWith($fetcher, $logger); // <== not sure how to do it here
}
and it's not working, error:
- it should throw exception
some predictions failed:
DataFetcherInterface\P3:
No calls have been made that match:
DataFetcherInterface\P3->getData()
but expected at least one.
I know php spec is for TDD, and I should start from tests, but at the moment I start adding tests I already had some come, for new code I'll use TDD approach.
Is it a bad practice to have such logic in constructor?? I have other methods in the class as well, but I want it to be instantiated with all needed data, and I'm using Visitor pattern to get all data while initializing object

Handle Exception From Within Method

I am implementing payments for my website using the API of an external service (ie. the service of the payment provider).
Let's say the user clicks 'BUY', and then we go to my controller which says something along the lines of:
public function buyFunction() {
$result = $this->ExternalService->pay();
if ($result->success == true) {
return 'We are happy';
}
}
I have also created the aforementioned externalService which has the pay() method:
class ExternalService {
public function pay() {
response = //Do stuff with Guzzle to call the API to make the payment
return response;
}
}
Now, sometimes things go wrong.
Let's say the API returns an error - which means that it throws a GuzzleException - how do I handle that?
Ideally, if there is an error, I would like to log it and redirect the user to a page and tell him that something went wrong.
What I've tried
I have tried using a try/catch statement within the pay() function and using abort(500) but this doesn't allow me to redirect to the page I want to.
I have tried using a try/catch statement within the pay() function and using return redirect('/mypage') but this just returns a Redirect object to the controller, which then fails when it tries to call result->success
I have tried using number 2 but also adding a try/catch block to the controller method, but nothing changed.
In the end, I have found two solutions. In both, I use a try/catch block inside the pay() method. Then I either return 0; and check in the controller if (result == 0) or I use abort( redirect('/mypage') ); inside the try/catch block of the pay() method.
What is the right way to handle this?
How to use the try/catch blocks?
In my experience, avoid handling exceptions let them pass through and handle them accordingly with try catches. This is the most pragmatic approach. Alternatively you will end up checking result is correct in weird places, eg. if ($result) {...}. Just assume it went good, except if the exception is thrown. Bonus: never do Pokemon catches with Exception $e unless you specifically needs it!
class ExternalService {
public function pay() {
try {
response = $client->get(...);
} catch (BadResponseException $exception) {
Log::warning('This should not happen check payment api: ' . $exception->getMessage());
throw new PaymentException('Payment did not go through');
}
return response;
}
}
Assuming you have your own Exception.
class PaymentException extends HttpException
{
public function __construct(?\Exception $previous = null)
{
parent::__construct(Response::HTTP_BAD_REQUEST, 'Unexpected error processing the payment', $previous);
}
}
This enables you to handle the flow in a controller, where it would make sense to handle the redirect. Sometimes if the exception is very integral or common to the web app, it can also be handled by the exception handler instead.
class PaymentController {
public function pay(PaymentService $service) {
try {
$payment = $service->buyFunction();
} catch (PaymentException $exception) {
return redirect()->route('app.payment.error');
}
return view('app.payment.success', compact('payment'));
}
}

How to mock a Job object in Laravel?

When it comes to Queue testing in Laravel, I use the provided Queue Fake functionality. However, there is a case where I need to create a Mock for a Job class.
I have the following code that pushes a job to a Redis powered queue:
$apiRequest->storeRequestedData($requestedData); // a db model
// try-catch block in case the Redis server is down
try {
App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run');
$apiRequest->markJobQueued();
} catch (\Exception $e) {
//handle the case when the job is not pushed to the queue
}
I need to be able to test the code in the catch block. Because of that, I'm trying to mock the Job object in order to be able to create a faker that will throw an exception.
I tried this in my Unit test:
ProcessRunEndpoint::shouldReceive('dispatch');
That code returns: Error: Call to undefined method App\Jobs\ProcessRunEndpoint::shouldReceive().
I also tried to swap the job instance with a mock object using $this->instance() but it didn't work as well.
That said, how can I test the code in the catch block?
According to the docs, you should be able to test jobs through the mocks provided for the Queue.
Queue::assertNothingPushed();
// $apiRequest->storeRequestedData($requestedData);
// Use assertPushedOn($queue, $job, $callback = null) to test your try catch
Queue::assertPushedOn('run', App\Jobs\ProcessRunEndpoint::class, function ($job) {
// return true or false depending on $job to pass or fail your assertion
});
Making the line App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run'); throw an exception is a bit complicated. dispatch() just returns an object and onQueue() is just a setter. No other logic is done there. Instead, we can make everything fail by messing with the configuration.
Instead of Queue::fake();, override default queue driver with one that just won't work: Queue::setDefaultDriver('this-driver-does-not-exist'); This will make every job dispatch fail and throw an ErrorException.
Minimalist example:
Route::get('/', function () {
try {
// Job does nothing, the handle method is just sleep(5);
AJob::dispatch();
return view('noError');
} catch (\Exception $e) {
return view('jobError');
}
});
namespace Tests\Feature;
use App\Jobs\AJob;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class AJobTest extends TestCase
{
/**
* #test
*/
public function AJobIsDispatched()
{
Queue::fake();
$response = $this->get('/');
Queue::assertPushed(AJob::class);
$response->assertViewIs('noError');
}
/**
* #test
*/
public function AJobIsNotDispatched()
{
Queue::setDefaultDriver('this-driver-does-not-exist');
$response = $this->get('/');
$response->assertViewIs('jobError');
}
}
I found a solution. Instead of using a facade for adding a job to the queue (App\Jobs\ProcessRunEndpoint::dispatch($apiRequest)->onQueue('run');), I injected it into the action of the controller:
public function store(ProcessRunEndpoint $processRunEndpoint)
{
// try-catch block in case the Redis server is down
try {
$processRunEndpoint::dispatch($apiRequest)->onQueue('run');
} catch (\Exception $e) {
//handle the case when the job is not pushed to the queue
}
}
With this, the job object is resolved from the container, so it can be mocked:
$this->mock(ProcessRunEndpoint::class, function ($mock) {
$mock->shouldReceive('dispatch')
->once()
->andThrow(new \Exception());
});
Although still not sure why the shouldReceive approach doesn't work for the facade: https://laravel.com/docs/8.x/mocking#mocking-facades

Throwing a \Doctrine\DBAL\Driver\DriverException in a unit test mock

Redacting unit tests, I am confronted to this problem. A piece of code that I want to test catches a \Doctrine\DBAL\Exception\RetryableException. The first constructor in the classes chain is the one of DriverException and is built like this :
/**
* #param string $message The exception message.
* #param \Doctrine\DBAL\Driver\DriverException $driverException The DBAL driver exception to chain.
*/
public function __construct($message, \Doctrine\DBAL\Driver\DriverException $driverException)
{
$exception = null;
if ($driverException instanceof Exception) {
$exception = $driverException;
}
parent::__construct($message, 0, $exception);
$this->driverException = $driverException;
}
I feel like I am confronted to the problem of the egg and the chicken, here. How can I instanciante a class that takes an instance of itself as mandatory argument in the first place ?
Note: I won't mark this auto-response as a solution, it is more a workaround.
Instead of throwing the right exception in my unit test mock, I have created a simpler one, extending Exception but still implementing the original interface RetryableException, as it's the interface that is caught in the code I am testing. While not being what I wanted to do, it does the job in my precise case.
Here is how I have an actual instance of DriverException in my unit tests, using an anonymous class instead of a mock:
<?php
declare(strict_types=1);
use Doctrine\DBAL\Driver\Exception as TheDriverException;
use Doctrine\DBAL\Exception\DriverException;
use PHPUnit\Framework\TestCase;
final class MyTest extends TestCase
{
// ... the rest of the test case
private function getDriverExceptionWithCode(int $code): DriverException
{
$theDriverException = new class($code) extends \Exception implements TheDriverException {
public function __construct(int $code)
{
parent::__construct('oh no, you broke it :(', $code);
}
public function getSQLState(): ?string
{
return null;
}
};
return new DriverException($theDriverException, null);
}
}
In my case I needed to unit test a situation where the code is catching a DriverException with a specific code, but you can extend the code as you wish, or make it simpler. The only thing you need is to implement getSQLState, after all.
Hope this helps whoever stumbles on this question from their favorite search engine.

Error creating session context via php sdk

The goal is to create a session context via the PHP V2 SDK like this:
$session = $this->contextsClient->sessionName($this->projectId, $this->sessionId);
$contextName = $this->contextsClient->contextName($this->projectId, $this->sessionId, 'test-context-name');
$context = new Context();
$context->setName($contextName);
$context->setLifespanCount(2);
$context->setParameters(["test-param-key" => "test-param-value"]);
return $this->contextsClient->createContext($session, $context);
The code works fine without the $context->setParameters(["test-param-key" => "test-param-value"]); part. I need to add parameters to the context though.
The error I get is:
Exception {#3554
#message: "Expect message.",
#file: "/home/vagrant/code/vendor/google/protobuf/php/src/Google/Protobuf/Internal/GPBUtil.php",
#line: 197,
}
I followed the errors trail and the problem is Google's code in line 197:
public static function checkMessage(&$var, $klass)
{
if (!$var instanceof $klass && !is_null($var)) {
throw new \Exception("Expect message.");
}
}
is trying to assert if the array passed to the setParameters function is an instance of \Google\Protobuf\Struct class in this snippet right here
public function setParameters($var)
{
GPBUtil::checkMessage($var, \Google\Protobuf\Struct::class);
$this->parameters = $var;
return $this;
}
I would be really glad if someone could help me. I spent a lot of hours trying to figure this out and nothing yet
As the error message states, the parameters need to be of type \Google\Protobuf\Struct. Also, each of the values need to be of type \Google\Protobuf\Value. For your particular example, you could do the following:
$paramValue = new \Google\Protobuf\Value();
$paramValue->setStringValue("test-param-value");
$parameters = new \Google\Protobuf\Struct();
$parameters->setFields(["test-param-key" => $paramValue]);
$context->setParameters($parameters);
You can look the implementation of these two classes here:
https://github.com/google/protobuf/blob/master/php/src/Google/Protobuf/Value.php
https://github.com/google/protobuf/blob/master/php/src/Google/Protobuf/Struct.php

Resources