Logging Codeception Errors - php

I'm pretty new to Codeception and I have come across a problem I cannot figure out. I have about 40 tests in my test suite, and if a test fails, I need to send an email with the reason it failed. For example, if Codeception cannot find an element on the page resulting in a failed test, I need to send an email with just the error, like this:
Failed to verify emailing wish list behaves as expected in ThisClass::thisTest (/home/qauser/codeception_tests///acceptance-mobile/Wishlist/EmailWishlistCest.php)
Couldn't see "Success!","//*[#id="wish-list-confirm-popup"]/div/div/div[1]/h4":
I don't want to send the full stack trace, just the actual error. Does anyone know if this is possible?

Codeception exposes a useful collection of events that will come in handy for this use case. Take a look at the Customization: Events section of Codeception's documentation for more information.
I'd recommend intercepting two of the events described on that page:
The test.fail event, to aggregate information about each failed test.
The test.fail.print event, to process the aggregated data (eg. by sending a summary email) when Codeception has completed the test suite and prints its own summary of the failures to the screen.
To accomplish this, you simply need to build a custom event handler class and register it as an extension in the config file:
# codeception.yml
extensions:
enabled: [MyCustomEventHandler]
# MyCustomEventHandler.php
<?php
// Note: this was drafted using Codeception 2.0. Some of the namespaces
// maybe different if you're using a more-recent version of Codeception.
class MyCustomEventHandler extends \Codeception\Platform\Extension
{
/**
* #var \Exception[]
*/
protected $testFailures = [];
/**
* Maps Codeception events to method names in this class.
*
* Defining an event/method pair in this array essentially subscribes
* the method as a listener for its corresponding event.
*
* #var array
*/
public static $events = [
\Codeception\Events::TEST_FAIL => 'singleTestJustFailed',
\Codeception\Events::TEST_FAIL_PRINT => 'allTestFailuresAreBeingDisplayed',
];
/**
* This method will automatically be invoked by Codeception when a test fails.
*
* #param \Codeception\Event\FailEvent $event
*/
public function singleTestJustFailed(\Codeception\Event\FailEvent $event)
{
// Here we build a list of all the failures. They'll be consumed further downstream.
$this->testFailures[] = $event->getFail();
}
/**
* This method will automatically be invoked by Codeception when it displays
* a summary of all the test failures at the end of the test suite.
*/
public function allTestFailuresAreBeingDisplayed()
{
// Build the email.
$emailBody = '';
foreach ($this->testFailures as $failure) {
// Methods in scope include: $failure->getMessage(), $failure->getFile(), etc.
$emailBody .= $failure->getMessage() . "\r\n";
}
// Now send the email!
}
}
Hope this helps!

Related

How to roll back changes and display error when an event throws an exception in Laravel

TL;DR
I would like to know how I should handle an exception that occurs in an event and subsequently rollback changes made to the database prior to the exception, provide feedback to the user and write a test for all of this too.
Full Details
In my Laravel web application I have four models: Applicant, Application, ApplicationAction and Document. An Applicant can have one Application which can have many ApplicationActions. Also, an Application can have many Documents.
Conceptually, within the application when an ApplicationAction is created it is applied to an Application which in turn has its status attribute updated. For example, if an ApplicationAction is of type reject the Application's status then becomes rejected.
Getting into the code where this all happens, I have an ActionController with a store() method that creates a new ApplicationAction:
ActionController.php
public function store(StoreActionFormRequest $request)
{
$actionDetails = (new ApplicationAction)->createFromRequest($request);
}
ApplicationAction.php
public function createFromRequest(Request $request)
{
return $this->create([
'action' => $request->action,
'action_meta' => $this->actionMeta($request),
'details' => $request->details,
'details_external' => $request->details_external,
'action_by' => $request->user()->id,
'application_id' => $request->application->id,
]);
}
On the creation of a new ApplicationAction the ApplicationActionCreated event is fired with a listener that modifies the Application's status attribute based on ApplicationAction. I am not including the code in as it's not relevant.
When the Application is updated in this process (i.e. its status is changed) an event is triggered: ApplicationUpdated. A listener for this event is the NotifyApplicationWasUpdated listener which sends an email to the Applicant:
NotifyApplicationWasUpdated.php
/**
* Handle the event.
*
* #param ApplicationUpdated $event
* #return void
*/
public function handle(ApplicationUpdated $event)
{
if ($mailable = MailableType::resolve($event->application)) {
$to = $event->application->applicant->user->email;
$from = $event->application->applicant->dispatchEmail('verification');
Mail::to($to)
->bcc($from)
->send(new $mailable($event->application));
}
}
The mailable is determined dynamically (code not relevant) and within the mailable an attachment is added to the email:
A dynamically determined MailableClass
/**
* Build the message.
*
* #return $this
*/
public function build()
{
//... some code
$documents = $this->application->documents->whereIn('type', ['some-document', 'some-other-document']);
$documents->each(function ($document, $key) {
// TODO: check the file exists first
$this->attach(storage_path($document->file_path));
});
return $this->from($from)->view($view);
}
I would like to handle the FileNotFound exception if the file to be used as an attachment cannot be found. If this exception occurs then the email would not be sent, so I thought it would be a good idea to rollback the changes made to the database prior to the exception which would include deleting the newly made ApplicationAction and returning the Application's status to its previous value.
I would also like to provide the user appropriate feedback such as the file intended to be used as an attachment couldn't be found and also that the ApplicationAction was not created and the Application status was not modified.
The approaches I have considered to handling the FileNotFound exception in the event that the Document cannot be found when executing this line of code $this->attach(storage_path($document->file_path)); is to check if the file exists first and if it doesn't then to rollback changes and respond to user with an error:
if(!file_exists(storage_path($document->file_path))) {
// roll back DB changes and show user error page
$this->application->rollback(); // some method that sets the status to its previous value, not implemented it yet
$this->application->lastAction()->delete(); // delete the last action
// then throw an exception to be handled by Laravel exception handler
// which in turn responds to user with an appropriate message?
}
$this->attach(storage_path($document->file_path));
...but I'm a bit unsure on the details of how to rollback the DB changes and show the error to the user. I could either use the above approach, having a Application rollback() method etc. or instead wrapping the controller store() action in a try-block and DB::transaction()/commit(), which would look something like this:
ActionController.php
public function store(StoreActionFormRequest $request)
{
DB::beginTransaction();
try {
$actionDetails = (new ApplicationAction)->createFromRequest($request);
} catch(FileNotFoundException $e) {
DB::rollback();
throw $e;
}
DB::commit();
}
Then, as mentioned before, use the Laravel exception handler which in turn responds to user with an appropriate error message:
Handler.php
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
if ($exception instanceof FileNotFoundException) {
return response()->view('errors.file_not_found', [], 500);
}
return parent::render($request, $exception);
}
If I did use this approach would it be possible to pass some specific text explaining to the user the email was not sent and why? Would I be better off creating my own EmailAttachmentFileNotFoundException and have it hardcoded in the view and using this in Handlers render() function?
Finally, I would most importantly like to ask how may I write a test for all the above? As most of the logic I'm trying to implement is around an event, which is currently occuring synchronously, how would I write a test for this specific event/listener because I already have something testing the controller action in the form of an integration test but it all it tests for is if the event is fired. How would I then write an isolated test of a test that goes smoothly, i.e. the email is sent successfully and secondly how would I write a test that goes wrong and check it's handled correctly, i.e. email attachment is not found and FileNotFound exception is triggered then check the DB stuff is rolled back and the user gets the correct error page?
Any help would be appreciated and if this question can be made any shorter please feel free to edit away!

Custom HandlerWrapper with MonologBundle

I am using Symfony 3.1 and I try to configure Monolog in such a way, that requests from the Googlebot are not logged. For this I wrote a UserAgentProcessorwhich already works as intended. In the next step I tried to write BotFilter which looks like this:
<?php
namespace AppBundle\Handler;
use Monolog\Handler\HandlerWrapper;
class FilterBotsHandler extends HandlerWrapper
{
/**
* {#inheritdoc}
*/
public function isHandling(array $record)
{
if (stripos($record['extra']['userAgent'], 'bot') !== false){
return false;
} else {
return $this->handler->isHandling($record);
}
}
}
This was inspired by the comments in the HandlerWrapper abstract class (take a look here).
Now I want to add that filter to my monolog yml-configuration. I tried adding it to my services but this was not possible as HandlerWrapper needs a Handler instance for its constructor. I researched how I could use the filter without a service but as far as I can see, the monolog bundle only accepts built-in types and the generic service type.
Now the question is: How can I use the filter in my configuration?
I am using Symfony 3.1 and I try to configure Monolog in such a way, that requests from the GoogleBot are not logged...
The quick way to prevent robots visiting your site is put these two lines into the /robots.txt file on your server. Create a robots.txt file in 'web' directory and paste the follows content:
User-agent: *
Disallow: /
https://support.google.com/webmasters/answer/6062608?hl=en&visit_id=1-636097099675465769-3677253464&rd=1
It's the recommended option when you need avoid access fully, meaning your sites will not longer be index by search engines and other bots. You don't need to configure/implement anything in your application to achieve it.
Now, if you need the bot to enter, but you don't want register it in logs. Instead of writing log files somewhere, some handlers are used to filter or modify log entries before sending them to other handlers. One powerful, built-in handler called fingers_crossed is used in the prod environment by default. It stores all log messages during a request but only passes them to a second handler if one of the messages reaches an action_level:
# app/config/config.yml
monolog:
handlers:
filter_for_errors:
type: fingers_crossed
# if *one* log is error or higher, pass *all* to file_log
action_level: error
handler: file_log
# now passed *all* logs, but only if one log is error or higher
file_log:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log"
Thus, in your prod.log file just will register the messages/requests that contains some error, so the bots don't have effect in this level.
More details about this http://symfony.com/doc/current/logging.html
What you try to do is not advisable, because the handler will depend from http request instead of log records, which will be out of context, however you can register its own handler in Symfony easily:
Let's create the custom handler class:
namespace AppBundle\Monolog\Handler;
use Monolog\Handler\AbstractHandler;
class StopBotLogHandler extends AbstractHandler
{
public function isBotRequestDetected()
{
// here your code to detect Bot requests, return true or false
// something like this:
// return isset($_SERVER['HTTP_USER_AGENT']) && preg_match('/bot|crawl|slurp|spider/i', $_SERVER['HTTP_USER_AGENT']);
}
/**
* Checks whether the given record will be handled by this handler.
*
* This is mostly done for performance reasons, to avoid calling processors for nothing.
*
* Handlers should still check the record levels within handle(), returning false in isHandling()
* is no guarantee that handle() will not be called, and isHandling() might not be called
* for a given record.
*
* #param array $record Partial log record containing only a level key (e.g: array('level' => 100) for DEBUG level)
*
* #return bool
*/
public function isHandling(array $record)
{
return $this->isBotRequestDetected();
}
/**
* Handles a record.
*
* All records may be passed to this method, and the handler should discard
* those that it does not want to handle.
*
* The return value of this function controls the bubbling process of the handler stack.
* Unless the bubbling is interrupted (by returning true), the Logger class will keep on
* calling further handlers in the stack with a given log record.
*
* #param array $record The record to handle
*
* #return bool true means that this handler handled the record, and that bubbling is not permitted.
* false means the record was either not processed or that this handler allows bubbling.
*/
public function handle(array $record)
{
// do nothing, just returns true whether the request is detected as "bot", this will break the handlers loop.
// else returns false and other handler will handle the record.
return $this->isBotRequestDetected();
}
}
Whenever you add a record to the logger, it traverses the handler stack. Each handler decides whether it fully handled the record, and if so, the propagation of the record ends there.
Important: Read the phpdoc from isHandling() and handle() methods for more details.
Next, let's register the class as service "without tags":
# app/config/services.yml
services:
monolog.handler.stop_bot_log:
class: AppBundle\Monolog\Handler\StopBotLogHandler
public: false
Then, add its handler to handlers list:
# app/config/config_prod.yml
monolog:
handlers:
# ...
stopbotlog:
type: service
id: monolog.handler.stop_bot_log
priority: 1
Note the type property must be equal to service, id must be the service name before defined and priority must be greater than 0 to ensure that its handler will be executed before that any other handler.
When the GoogleBot performs a request to website application the stopbotlog handler stops all handlers after him and don't register any log message.
Remember it's not the recommended way to do that! According to your needs, implementing option 1 or 2 should be enough.
If you want ignore bot requests for handlers group, you can override the monolog.handler.group.class container parameter and override the group handler behavior:
namespace AppBundle\Handler;
use Monolog\Handler\GroupHandler;
class NoBotGroupHandler extends GroupHandler
{
public function isBotRequestDetected()
{
// here your code to detect Bot requests, return true or false
}
public function handle(array $record)
{
if ($this->isBotRequestDetected()) {
// ignore bot request for handlers list
return false === $this->bubble;
}
return parent::handle($record);
}
}
in your config_prod.yml or services.yml:
parameters:
monolog.handler.group.class: AppBundle\Handler\NoBotGroupHandler
That's it! Now, you can stop bot logs for custom handles list:
# config_prod.yml
monolog:
handlers:
grouped:
type: group
members: [main, console, chromephp]
Finally, if you have difficulty to analyze your logs files I recommend using this amazing tool: https://github.com/EasyCorp/easy-log-handler
It's quite a dirty trick, but if you really need it, you may make it like this.
Supposing you want to wrap a handler with a type stream:
Add a constructor in you FilterBotsHandler:
public function __constructor($path, $level, $bubble, $permissions) {
$this->handler = new Monolog\Handler\StreamHandler($path, $level, $bubble, $permissions);
}
And then redefine a parameter monolog.handler.stream.class:
parameters:
monolog.handler.stream.class: AppBundle\Handler\FilterBotsHandler
Make sure that this parameter will be defined after it was defined by MonologBundle.
That's it. Should work.
You may write CompilerPass in your AppBundle which adds configurator to monolog service. Such configurator can be also a request event listener which can replace all handlers dynamically on request and bot detection and push empty handlers array to Logger which can be hold on configurator call.
In other words configurator added to DI by CompilerPass and added to EventDispatcher as Listener to Kernel events which onRequest check User-Agent header looking for bot and then clears Monolog\Logger (passed in configurator) all handlers (or putting an NullHandler if empty handlers array fails).
DI configurator is only way to change your services during runtime which can be applied as service definition level. Such definition can be attached or detached if not needed and it doesn't really change anything in your application.

Call a method from another class in another application with FOSTRestBundle in Symfony

I have the class file ex: Stats.php when i want to give an array with information from another method in another application and class file ex: Information.php.
File Stats.php
public function getStats()
{
$myInformations = // here i want to get information from Information.php
.
.
.
return $myInformations;
}
In different application.
File Informations.php
/**
* Get all locales
* #FOS\View()
* #FOS\Get("/locales")
* #param ParamFetcherInterface $paramFetcher
* #return mixed
*/
public function getLocales(ParamFetcherInterface $paramFetcher)
{
.
.
.
return $locales;
}
How I call function getLocales from url: http://myhost.com/api/locales in function getStatus()?
When you are using the GuzzleBundle as mentioned in your comment. You can just inject a client into the class containing getStats() or by making it ContainerAware and retrieving the guzzle-client by its service id from the container. If you don't have to set default options for your client, i.e. you just want to access a url you assume is always available for all environments from all places you could just create a new client with default values:
$guzzleClient = new GuzzleHttp\Client();
Making a request with the client is described in the guzzle docs in the Quickstart in section Sending Requests:
$response = $guzzleClient->get('http://myhost.com/api/locales')
When the request was successful you can retrieve the locales by calling:
$content = $response->getBody()->getContent();
Either casting getBody() to string or using getContent() is important here. Again refer to Guzzle's documentation particularly the section on using responses.
For example if you send a json-encoded string you can do something like:
$encodedLocales = $response->getBody()->getContent();
$decodedLocales = json_decode($encodedLocales, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception(json_last_error_msg());
}
There are many things that should be said about this approach such as it's fragile because it relies on a working network connection possibly to a different server, it's not easily testable, transfer exceptions are not handled gracefully, etc. pp. but for a simple proof of concept or asa starting point this should suffice.

Laravel Pull Queue

I have been using the Laravel framework and have just recently gotten into implementing queue's with Laravel's built in support for IronMQ.
From the Laravel documentation its easy enough to see how to push messages to a queue and then on Iron.io set subscribers and have the queue push to those subscribers. However I want to utilize IronMQ as a Pull queue. I do not see any indication of how to pull a message from a specified queue using Laravel's built in methods.
On the IronMQ site they have all the endpoints listed related to facilitate a Pull queue implementation.
Ex: /projects/{Project ID}/queues/{Queue Name}/messages
In the IronMQ package for Laravel I see the methods that seem to work with these endpoints:
/**
* Peek Messages on a Queue
* Peeking at a queue returns the next messages on the queue, but it does not reserve them.
*
* #param string $queue_name
* #return object|null message or null if queue is empty
*/
public function peekMessage($queue_name) {
$messages = $this->peekMessages($queue_name, 1);
if ($messages == null) {
return null;
} else {
return $messages[0];
}
}
However, I do not see any support for this through Laravel. I would expect to be able to do something along the lines of:
$message = Queue::peek();
Which would return the next message from a specified queue, etc.
Is there a way to do this with Laravel's built in support that is just not documented?
Thanks!
Edit:
I have seen the documentation on using Daemon Workers through Laravel, however I want to process the queue myself through a cron job.
You could try to use IronMQ class instead of laravel Queue class:
$ironmq = new \IronMQ(array(
'token' => Config::get('queue.connections.iron.token', 'xxx'),
'project_id' => Config::get('queue.connections.iron.project', 'xxx')
));
$ironmq->getMessage($queue_name);
IronMQ PHP lib

With PHPUnit, using a Data Provider, the last data set always shows "The test case was unexpectedly terminated"

So I'm using PHPUnit for testing. Trying to use a DataProvider with one of my tests.
/**
* Tests Events_Event->Events_Event()
* #dataProvider provider
*/
public function testEvents_Event($Name, $param, $time) {
//$this->assertInstanceOf("Events_Event", $this->Events_Event->Events_Event("test2", array()));
$this->assertTrue(true);
}
public static function provider()
{
return array(
array("test", array("Like a boss"), "Cheack the time"),
array("test2", array("Like a boss"), "9:00"),
array("test3", array("Time to go home"), "4:00"),
array("test3", array("Time to go home"), "4:00")
);
}
The results:
testEvents_Event with data set#0
testEvents_Event with data set#1
testEvents_Event with data set#2
testEvents_Event with data set#3: The test case was unexpectedly terminated
This happens on the last data set no matter how many there are and whether or not the last data set is valid of not. As you can see, we've simplified the test to a simple $this->assertTrue(true) and it's still giving us the error.
What do we need to do to get the Data Provider working?
In case it's important I'm working PHPUnit inside Zend Studio 9.0.3, I've checked for updates and it's telling me all is up to date.
I was going through
....
Time: 0 seconds, Memory: 12.75Mb
OK (4 tests, 0 assertions)
/**
* Tests Events_Event->Events_Event()
* #dataProvider provider
*/
public function testEvents_Event($Name, $param, $time)
{
}
public static function provider()
{
return array(
array("test", array("Like a boss"), "Cheack the time"),
array("test2", array("Like a boss"), "9:00"),
array("test3", array("Time to go home"), "4:00"),
array("test3", array("Time to go home"), "4:00")
);
}
how to you run tests? there do not have any other dependencies?
tests runing via any IDE?
PHPUnit instantiates the test case for each data provider method. Due to the magic of PHP you can get away with using static data provider methods, but they are called using an instance and thus should be non-static.
If your test case has a constructor, it must accept three parameters (see the source for PHPUnit_Framework_TestCase) and pass them to the parent constructor. One of them is the data from a provider for that particular test.
I doubt these are the problem, however. My money's on ZendStudio and how it parses the output from PHPUnit as Gordon suggested. When you run this test case from the command line, do you see the same issue?

Categories