📜 ⬆️ ⬇️

Manage your browser with PHP and Selenium

Intro


Hello! Today I will tell you about how PHP can work with Selenium .

Most often it is necessary when you are faced with the task of writing autotests for the web interface or your parser / crawler.

From wikipedia
“Selenium is a tool to automate web browser actions.
In most cases, it is used to test web applications, but this does not
is limited. In particular, the implementation of Selenium WebDriver for the phantomjs browser
often used as a web grabber. ”

We will consider the following nuances:
')

So let's go!

1. Cooking Behat and Mink


Behat is a php framework that was originally created for behavioral testing (BDD). It is the official PHP implementation of the more well-known Cucumber product, which is often used in other programming languages.

By itself, he does not know how to work with Selenium and is intended more for writing functional tests without using a browser.

Installed as a regular package:

$ composer require "behat/behat" 

To teach behat how to work with a browser, we need its Mink extension, as well as a bridge for working with a specific vendor (in our case, it is Selenium). A complete list of vendors can be found on the Mink page . Given the versions, your composer.json should look something like this:

  "require": { "behat/behat" : "^3.4", "behat/mink-extension" : "2.2", "behat/mink-selenium2-driver" : "^1.3" } 

After installation, you will have the vendor / bin / behat file responsible for running the tests. If vendor / bin / behat --version showed you the installed version, then with high probability the installation was successful :)

The final phase is the configuration

Create the main configuration file behat.yml in the project root
 default: #     «»  autoload: '': '%paths.base%/src/Context' suites: #    facebook_suite: # ()   ,   Gherkin language paths: - '%paths.base%/scenario/facebook' contexts: #   «»   . # API     - Dossier\Context\FacebookContext: #       FacebookContext base_url: 'https://www.facebook.com/' user: 'email@gmail.com' pass: 'password' vk_suite: paths: - '%paths.base%/scenario/vk' contexts: - Dossier\Context\VkContext: #       - "@lookup" services: #      lookup: 'Dossier\Context\AccessLimit\Lookup' extensions: #   ,  behat Behat\MinkExtension: browser_name: 'chrome' default_session: 'selenium2' selenium2: #  Selenium .     IP (     localhost   ) wd_host: 'http://172.17.0.1:4444/wd/hub' #     browser: chrome 


Script files or (* .feature files) - yml files written in the Gherkin pseudo-language contain, in fact, a set of step-by-step instructions that your browser will execute during the execution of a particular suite. You can learn more about the syntax by clicking on the link above.

Each such “instruction”, in turn, is matched to the methods of the “context” class using regular expressions specified in the class annotations. Behat \ MinkExtension \ Context \ MinkContext
The names of the methods themselves do not matter, although it will be good practice to stick to the similar naming annotations in CamelCase.

If you lack the default Gherkin constructs, you can extend the functionality in the classes inherited by MinkContext by correctly specifying annotations. This role is performed by the "context" classes.

2. Install and configure the environment


Those of you who have already worked with Selenium know that after starting the test, the browser will start on the machine and go through the steps specified in the .feature file.

Running Selenium in Docker is a little more difficult. First, you will need X in the container, and second, you will want to see what is happening inside the container.

The guys from Selenium have already taken care of everything and you don’t have to collect your container. A container with a Standalone server on board will be immediately available on port 5900, where you can knock on any VNC client (for example, from this ). Inside the container you will be greeted by a friendly Fluxbox interface with Chrome preinstalled. In my case, it looks like this:



To achieve success, you can launch the docker container, according to the instructions on the site:

 $ docker run -d -p 4444:4444 -p 5900:5900 -v /dev/shm:/dev/shm selenium/standalone-chrome-debug:3.11.0-californium 

An important point, without a shared volume / dev / shm, there is not enough memory for the chrome and it will not be able to start, so do not forget to specify it.

In my case, docker-compose is used , and the YAML file will look like this:

 version: '2' services: selenium: image: selenium/standalone-chrome-debug:3.11.0 ports: - "4444:4444" - "5900:5900" volumes: - /dev/shm:/dev/shm network_mode: "host" 

I want my tests to go to Facebook through a VPN that is enabled on the host machine, so it’s important to specify the network_mode .

To run the container using compose, run the following command:

 $ docker-compose up 

Now we try to connect via VNC to localhost: 5900 and open the browser inside the container. If you succeeded and you see something similar to the screenshot above - you have passed this level.

3. From theory to practice. Automating


In the example below, I’ll retrieve all Facebook users by the transferred last and first name. The script will look like this:

src / scenario / facebook / facebook.feature
 Feature: Facebook Parse In order parse fb @first-level Scenario: Find person in facebook Given I am on "https://facebook.com/" When I fill in "email" with "some@gmail.com" And I fill in "pass" with "somepass" #   Then I press tricky facebook login button Then I should see "" #   Then I am searching by input params Then I dump users 


And accordingly Context class (constructor and namespaces are omitted)

src / Context / FacebookContext.php
 class FacebookContext extends MainContext { /** * @Then /^I press tricky facebook login button$/ */ public function pressFacebookButton() { $this->getSession()->getPage()->find( 'css', 'input[data-testid="royal_login_button"]' )->click(); } /** *    . , , .  * @Then /^I dump users$/ */ public function dumpUsers() { $session = $this->getSession(); $users = $this->getSession()->getPage()->findAll( 'xpath', $session->getSelectorsHandler() ->selectorToXpath('css', 'div._4p2o') ); if (!$users) { throw new \InvalidArgumentException("The user with this name was not found"); } $collection = new UserCollection('facebook_suite'); foreach ($users as $user) { $img = $user->find('xpath', $session->getSelectorsHandler() ->selectorToXpath( 'xpath', $session->getSelectorsHandler()->selectorToXpath('css', 'img') )); $link = $user->find('xpath', $session->getSelectorsHandler() ->selectorToXpath( 'xpath', $session->getSelectorsHandler()->selectorToXpath('css','a._32mo') )); $outputInfo = new OutputUserInfo('facebook_suite'); $outputInfo->setName($link ? $link->getText(): '') ->addPublicLinks($link ? $link->getAttribute('href') : '') ->setPhoto($img ? $img->getAttribute('src') : ''); $collection->append($outputInfo); } $this->saveDump($collection); } /** *        URL * @Then /^I am searching by input params$/ */ public function search() { if (!Registry::has('query')) { throw new \BadMethodCallException('No search query received'); } $criteria = Registry::get('query'); $this->getSession()->visit("https://www.facebook.com/search/people/?q=" . urldecode($criteria->getQuery())); } } 


Often there is a need for custom methods like FacebookContext :: pressFacebookButton, because by default all selectors in mink can only search by name | value | id | alt | title.

If you need a sample for another attribute, you will have to write your method. The Login button for facebook has an id attribute, but changes its value periodically according to some logic of its own. Therefore, I had to re-connect to data-testid, which, so far, remains static.

Now, in order for all this to start, you need to make sure that Selenium is running and listening to the specified port.

Then execute:

 $ vendor/bin/behat 

The browser instance should start inside the container and go to follow the specified instructions.

4. behat customization. Extensions


The Behat framework has an excellent extension mechanism built in through behat.yml . Note that many framework classes are declared final to temper the temptation to simply inherit them.

The extension allows you to add functionality to behat, declare new console arguments and options, modify the behavior of other extensions, etc. It consists of the class implementing
Behat \ Testwork \ ServiceContainer \ Extension interface (also specified in behat.yml) and helper classes, if needed.

I want to teach behat to accept the full name of the person being sought through the new incoming argument - search-by-fullname , in order to use this data within the suite.

Below is the code that performs the necessary operations:

SearchExtension
 use Behat\Behat\Gherkin\ServiceContainer\GherkinExtension; use Behat\Testwork\Cli\ServiceContainer\CliExtension; use Behat\Testwork\ServiceContainer\Extension; use Behat\Testwork\ServiceContainer\ExtensionManager; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; class SearchExtension implements Extension { /** *       Extensions   */ public function process(ContainerBuilder $container) { } /** *       behat.yml * @return string */ public function getConfigKey() { return 'search'; } /** *        ,  *    configure().    *      * @param ExtensionManager $extensionManager */ public function initialize(ExtensionManager $extensionManager){} /** *     * @param ArrayNodeDefinition $builder */ public function configure(ArrayNodeDefinition $builder){ } /** *      * @param ContainerBuilder $container * @param array $config */ public function load(ContainerBuilder $container, array $config) { $definition = new Definition('Dossier\BehatSearch\SearchController', array( new Reference(GherkinExtension::MANAGER_ID) )); $definition->addTag(CliExtension::CONTROLLER_TAG, array('priority' => 1)); $container->setDefinition( CliExtension::CONTROLLER_TAG .' . search', $definition ); } } 


In the SearchExntesion :: load method, the SearchController Service is forwarded , which is directly responsible for the declaration of parameters and their reception / processing.

SearchController
 use Behat\Testwork\Cli\Controller; use Dossier\Registry; use Dossier\User\Criteria\FullnameCriteria; use Symfony\Component\Console\Command\Command as SymfonyCommand; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class SearchController implements Controller { const SEARCH_BY_FULLNAME = 'search-by-fullname'; /** * Configures command to be executable by the controller. * @param SymfonyCommand $command */ public function configure(SymfonyCommand $command) { $command->addOption( '--' . self::SEARCH_BY_FULLNAME, null, InputOption::VALUE_OPTIONAL, "Specify the search query based on fullname of the user. Must be started from surname" ); } /** * Executes controller. * * @param InputInterface $input * @param OutputInterface $output * * @return null|integer */ public function execute(InputInterface $input, OutputInterface $output) { $reflect = new \ReflectionClass(__CLASS__); foreach ($reflect->getConstants() as $constName => $option) { if ($input->hasOption($option) && ($optValue = $input->getOption($option))) { $queryArgs = explode(',', $optValue); Registry::set('query', new FullnameCriteria( $queryArgs[0], $queryArgs[1] ?? null, $queryArgs[2] ?? null) ); return null; } } throw new \InvalidOptionException("You must specify one of the following options to proceed: " . implode(', ', $reflect->getConstants())); } } 


If everything is declared correctly, then the list of available commands behat will be supplemented by a new argument - search-by-fullname :

 $ vendor/bin/behat --help 

 [mkardakov@mkardakov-local dossier.io]$ vendor/bin/behat --help | grep search-by-fullname --search-by-fullname[=SEARCH-BY-FULLNAME] Specify the search query based on fullname of the user. Must be started from surname [mkardakov@mkardakov-local dossier.io]$ 

Having received the input data inside SearchController, they can be transferred to the Context classes directly, or stored in a database, etc. In the example above, I use the Registry pattern for this. The approach is quite working, but if you know how to do it differently, please tell us in the comments.

That's all. Thanks for attention!

Source: https://habr.com/ru/post/353612/


All Articles