Codeception, don't print a specific action to report - php

In codeception, I want check if an element exist in the page and do another test if the first element exist. I can do that simply :
// $I is a AcceptanceTester Object and extends \Codeception\Actor class
try{
$I->see('.firstElement');
}catch(ElementNotFound $e){
// do some actions
}
// do some anothers actions
But If I do that, in the report file I can see the line "I see '.firstElement'". I don't want see this test in this report.
My question : How can I call a \Codeception\Actor method quietly ? I just want do a simple DOM element html check and not print this action into the generated report

You can create a simple helper module to check elements existence. It can use WebDriver module or PhpBrowser module to elements finding. For example:
class ElementChecker extends \Codeception\Module
{
public function checkExistence($locator)
{
$elements = $this->getModule('WebDriver')->_findElements($locator);
return !empty($elements);
}
},
After it, you should add this helper to your codeception configuration. For example:
actor: SomeTester
modules:
enabled:
# some modules
- ElementChecker
And new methods will be included in the tester class. You can use them:
if ($I->checkExistence('.firstElement')) {
// some code
}
Also, you can read more about helpers in the official documentation

Related

Functional tests on codeception do not work

I have the following code:
class FirstCest
{
public function _before(FunctionalTester $I)
{
}
public function _after(FunctionalTester $I)
{
}
public function tryToTest(FunctionalTester $I)
{
$I->amOnPage('/about');
}
}
This is my config:
actor: FunctionalTester
modules:
enabled:
- \Helper\Functional
error_level: "E_ALL"
When I try to run: tests/functional/FirstCest.php:tryToTest I get:
[RuntimeException] Call to undefined method
FunctionalTester::amOnPage
Write your own module or use PhpBrowser.
I see that you already started work on https://github.com/visavi/codeception-phpixie/ .
An obvious thing which is missing is Connector class.
Example: https://github.com/Codeception/Codeception/blob/2.4.5/src/Codeception/Lib/Connector/Guzzle6.php#L183-L217
Connector class must extend BrowserKit\Client class (unless your framework is based on Symfony Http component, in that case look at the code of Symfony module and connector).
and implement doRequest method, which transforms BrowserKit\Request object to an object expected by your framework, invokes framework code and transforms framework response to BrowserKit\Response.
The middle part of doRequest method must be based on the code that you put to index.php of your site.
Ask questions in comments.

How to do advanced filtering of Monolog messages in Symfony?

I am using the MonologBundle in my Symfony 2.8 project to manage log messages. Using different Handlers it is no problem to write logs to file and to send them by e-mail at the same time.
I would like to reduce the number of messages I receive by mail. I already use the DeduplicationHandler and the FingersCrossed handler to filter by error level and to avoid duplicate messages. This works fine but is not enough.
For example I would like to reduce the number of mail about PageNotFound errors. Of course I want to be notified if /existingPage is not found, but I am not interested in messages about /.well-known/... files.
Another example are messages about errors in a third party CSV parser component. There are several known and harmless errors I am not interested in, but of course other errors are important.
This these errors/messages are generated by third party code, I cannot influence the source. I could only ignore these messages completely but this is not what I want.
I am looking for a solution to filter the messages by content. How can this be done in Monolog?
I already tried to solve this using a HandlerWrapper and discussed this issue in another question: The idea was, that the HandlerWrapper acts as filter. The HandlerWrapper is called by Monolog, it checks the message content and decides wether it should be processed or not (e.g. discard all messages including the text "./well-known/"). If a messages passes, the HandlerWrapper should simple hand it over to its nested/wrapped handler. Otherwise the message is skipped without further processing.
However this idea did not work, and the answers to the other question indicate, that a HandlerWrapper is not the right approach for this problem.
So the new/actual question is: How to create a filter for Monolog messages, that let me control wether a specific message should be process or not?
I'm not sure why using a HandlerWrapper is the wrong way to do it.
I had the same issue and figured a way how to wrap a handler in order to filter certain records.
In this answer I describe two ways to solve this, a more complex and an easy one.
(More or less) complex way
First thing I did, was to create a new class wich extends the HandlerWrapper and added some logic where I can filter records:
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper
{
public function isHandling(array $record)
{
if ($this->shouldFilter($record)) {
return false;
}
return $this->handler->isHandling($record);
}
public function handle(array $record)
{
if (!$this->isHandling($record)) {
return false;
}
return $this->handler->handle($record);
}
public function handleBatch(array $records)
{
foreach ($records as $record) {
$this->handle($record);
}
}
private function shouldFilter(array $record)
{
return mt_rand(0, 1) === 1;; // add logic here
}
}
Then I created a service definition and a CompilerPass where I can wrap the GroupHandler
services.yml
CustomHandler:
class: CustomHandler
abstract: true
arguments: ['']
use Monolog\Handler\GroupHandler;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class CustomMonologHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition(CustomHandler::class)) {
return;
}
$definitions = $container->getDefinitions();
foreach ($definitions as $serviceId => $definition) {
if (!$this->isValidDefinition($definition)) {
continue;
}
$cacheId = $serviceId . '.wrapper';
$container
->setDefinition($cacheId, new ChildDefinition(CustomHandler::class))
->replaceArgument(0, new Reference($cacheId . '.inner'))
->setDecoratedService($serviceId);
}
}
private function isValidDefinition(Definition $definition): bool
{
return GroupHandler::class === $definition->getClass();
}
}
As you can see I go over all definitions here and find the ones which have the GroupHandler set as their class. If this is the case, I add a new definition to the container which decorates the original handler with my CustomHandler.
Side note: At first I tried to wrap all handlers (except the CustomHandler of course :)) but due to some handlers implementing other interfaces (like the ConsoleHandler using the EventSubscriberInterface) this did not work and lead to issues I didn't want to solve in some hacky way.
Don't forget to add this compiler pass to the container in your AppBundle class
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CustomMonologHandlerPass());
}
}
Now that everything is in place you have to group your handlers in order to make this work:
app/config(_prod|_dev).yml
monolog:
handlers:
my_group:
type: group
members: [ 'graylog' ]
graylog:
type: gelf
publisher:
id: my.publisher
level: debug
formatter: my.formatter
Easy way
We use the same CustomHandler as we did in the complex way, then we define our handlers in the config:
app/config(_prod|_dev).yml
monolog:
handlers:
graylog:
type: gelf
publisher:
id: my.publisher
level: debug
formatter: my.formatter
Decorate the handler in your services.yml with your own CustomHandler
services.yml
CustomHandler:
class: CustomHandler
decorates: monolog.handler.graylog
arguments: ['#CustomHandler.inner']
For the decorates property you have to use the format monolog.handler.$NAME_SPECIFIED_AS_KEY_IN_CONFIG, in this case it was graylog.
... and thats it
Summary
While both ways work, I used the first one, as we have several symfony projects where I need this and decorating all handlers
manually is just not what I wanted.
I hope this helps (even though I'm quite late for an answer :))
In my humble opinion, there is a simpler and clean way (maybe it was not available when the first answer was done).
I write it here because I think it can help a lot of people who need to filter their logs with a simple answer, understanding what they do:
Just implements a simple handler from AbstractHandler :
use Monolog\Handler\AbstractHandler;
/** Simple handler wrapper that filters records based on content. */
class FilterHandler extends AbstractHandler
{
/** For Monolog 3+, typehint with LogRecord instead of array */
public function isHandling(array $record): bool
{
// This function is called only with level info, so always return true if you don't want to filter on level (cf AbstractHandler phpdoc)
return true;
}
/** Handles a record. Idem, for Monolog 3+, typehint with LogRecord */
public function handle(array $record): bool
{
return $this->shouldFilter($record);
}
// Just a simple exemple (idem, Logrecord for Monolog 3+)
private function shouldFilter(array $record): bool
{
return
array_key_exists('context', $record) &&
array_key_exists('route', $record['context']) &&
'app_logout' === $record['context']['route']
;
}
}
Then in services.yaml, declare your Handler (as ordinary service) :
services:
my_filter_handler:
class: App\MyFilterNamespace\FilterHandler
public: false
And in your monolog.yaml conf, just add your handler on the top of the handlers with the type service:
monolog:
handlers:
main:
type: service
id: my_filter_handler
handler: rotating # next handler
rotating:
type: rotating_file # or whatever you need
[...]

How to use custom HandlerWrapper for Monolog in Symfony config.yml?

I would like to create a custom HandlerWrapper to use it with Monolog in my Symfony 2.8 project.
The goal is to use this CustomHandler as a filter that decides wether the wrapped/nested handler is called or not.
==============
UPDATE: The following question is already solved thanks to the answer of #Yonel. However this brought ab a new problem described below.
Extending Monolog\Handler\HandlerWrapper is no problem of course. But I am struggling to us the custom class in the Monolog config within config_prod.yml:
namespace AppBundle\Log;
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper {
public function handle(array $record) {
// some custom handling/processing...
if ($this->doSomeCheck())
return $this->handler->handle($record);
else
return false; // Do not call nested handler
}
}
Config:
services:
monolog.custom_handler.service:
class: AppBundle\Log\CustomHandler
monolog:
main:
type: service
id: monolog.custom_handler.service
level: error
handler: someHandler
someHandler:
...
Problem: When running app/console cache:warmup on this config, I get the following error:
[Symfony\Component\Debug\Exception\ContextErrorException]
Catchable Fatal Error: Argument 1 passed to
Monolog\Handler\GroupHandler::__construct() must be of the type array,
none given
Well, I think the source of the problem is obvious: The service definition of monolog.custom_handler.service does not pass any argument to the class. But how can I pass someHandler as argument, as defined in the Monolog config?
==============
EDIT: New Problem
As Yonel describes in his answer, on can use the following config to pass someHandler to custom_handler:
services:
monolog.custom_handler.service:
class: AppBundle\Log\CustomHandler
arguments:
- '#monolog.handler.testHandler'
monolog.test_handler.service:
class: AppBundle\Log\TestHandler
monolog:
# Skip level checking (can be solved by adding a FingersCrossed handler)
main:
type: service
id: monolog.custom_handler.service
testHandler:
type: service
id: monolog.test_handler.service
nested: true
This works: TestHandler is now correctly passed to CustomHandler
However the goal was to let CustomHandler decide, wether TestHandler is called or not. This does not work with this config: TestHandler is called anyway (I assume directly by Monolog, as any other handler). It does not make any difference if TestHandler is passed to CustomHandler, marked as nested or not.
I implemented CustomHandler as HandlerWrapper and TestHandler as AbstractHandler. Both classes directly to a log file (not using Monolog) when their handle method is called. This way I can check if the handlers work as expected (TestHandler should only be called if CustomHandler allows it). This is not the case.
No matter if TestHandler is passed to CustomHandler, if it is marked as nested, if CustomHandler returns true or false in its handle method, etc., the result is always the same: TestHandler is called, no matter what CustomHandler decides.
How to solve this?
Handler implementations:
namespace AppBundle\Log;
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper {
public function handle(array $record) {
// some custom handling/processing...
if ($this->doSomeCheck()) {
$this->directlyWriteToFileNotUsingMonolog('CustomHandler: Call TestHandler');
return $this->handler->handle($record);
else {
$this->directlyWriteToFileNotUsingMonolog('CustomHandler: Do NOT call TestHandler');
return false; // Do not call nested handler
}
}
class TestHandler extends AbstractHandler {
public function handle(array $record) {
$this->directlyWriteToFileNotUsingMonolog('TestHandler');
return false;
}
}

Run custom code after Codeception suite has finished

I am aware of the _bootstrap.php file that's used to set up the testing enviroment, etc., but I'm looking for a way to run some code after the entire test suite has finished.
Note that I'm not looking for a way to run code after a single class, i.e. something like _after, but after all classes.
Is there a way to achieve this?
Actually managed to solve this myself, here's how, if anyone is interested.
I created a new helper class inside _support.
<?php
class DataHelper extends \Codeception\Module
{
public function _beforeSuite()
{
// Set up before test suite
}
public function _afterSuite()
{
// Tear down after test suite
}
}
You can then enable this as a module in any suite configuration (the .yml files), like this:
modules:
enabled:
- DataHelper
#Sacha's solution is specially useful if you want to share the same methods accross all suites.
If you're looking for a way to define the methods for a specific suite (or if you want a different method per suite), you can define those methods directly in the suite Helper class.
For instance, if you want to define a _afterSuite method for the Acceptance Suite, just go to support/AcceptanceHelper.php and define those methods there. Eg:
<?php
namespace Codeception\Module;
// here you can define custom actions
// all public methods declared in helper class will be available in $I
class AcceptanceHelper extends \Codeception\Module
{
public function _afterSuite() {
die('everything done');
}
}

How to prevent Symfony profiler from accessing or executing a listener

My user has countTasks property, with corresponding setter and getter:
class User implements UserInterface, \Serializable
{
/**
* #var integer
*/
private $countTasks;
}
I want this property to be always shown in the application's navigation bar (the "14" number in red):
Obviously, this property should be set for every controller. (Actually, only for every that deals with rendering the navigation bar, but that's not the case here). So the application should count tasks for the currently logged-in user for every controller.
I found a relevant topic in the Symfony cookbook: How to Setup before and after Filters, and I managed to implement it:
Acme\TestBundle\EventListener\UserListener.php:
namespace Acme\TestBundle\EventListener;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class UserListener
{
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if ( ! is_array($controller)) {
return;
}
$securityContext = $controller[0]->get('security.context');
// now count tasks, but only if a user logged-in
if ($securityContext->isGranted('IS_AUTHENTICATED_REMEMBERED') or $securityContext->isGranted('IS_AUTHENTICATED_FULLY'))
{
$user = $securityContext->getToken()->getUser();
// ...
// countig tasks and setting $countTasks var
// ...
$user->setCountTasks($countTasks);
}
}
}
services.yml:
services:
acme.user.before_controller:
class: Acme\TestBundle\EventListener\UserListener
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
It works as expected and I'm able to pull the property in a Twig template like this:
{{ app.user.countTasks }}
It works as expected in prod env.
In dev however, profiler throws UndefinedMethodException:
UndefinedMethodException: Attempted to call method "get" on class "Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController" in ...\src\Acme\TestBundle\EventListener\UserListener.php line 18.
where line 18 is this one:
$securityContext = $controller[0]->get('security.context');
As a quick patch I added additional check (before line 18) to prevent profiler from executing the further logic:
if (is_a($controller[0], '\Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController'))
{
return;
}
$securityContext = $controller[0]->get('security.context');
and it has made the trick. But I'm afraid it's not the right way. I'm also afraid that I'm loosing some part of debug information in profiler.
Am I right with my concerns? Can you point me to a better way to prevent profiler from executing this listener? In config somehow?
Even in Symfony's documentation How to Setup before and after Filters, an instanceof condition is being evaluated in line 29.
I'd go about saying that if the doc's doing it, you're pretty safe doing it yourself (unless stated otherwise, which is not the case).
In the beginning I was trying to fix the issue conversely than I should. During testing it turned out that I have to exclude some other core controllers too. Of course, rather than block unwanted controllers I should have allowed the required ones only.
I ended up creating an empty interface UserTasksAwareController:
namespace Acme\TestBundle\Controller;
interface UserTasksAwareController
{}
fixing that validity check in UserListener.php:
if ( ! $controllers[0] instanceof UserTasksAwareController) {
return;
}
and implementing it in every other controller which deals with displaying countTasks property, like this one:
class UserController extends Controller implements UserTasksAwareController
So, the problem I had was just another one when you forget to program to an interface, not an implementation.

Categories