Functional tests with puppeteer and gherkin scenarios
Tags: gherkin, dev, testing, python
Published: 2020-02-14 08:52:04

Read the original article on [GKDV](https://gkdv.net/functional-tests-using-puppeteer-and-gherkin/)

<p>For my custom needs, I was looking for puppeteer functional testing suite. As a php developer, I ordinary use behat to write and play my functional tests. But the project I want to test is python written, and I don’t want to install and configure PHP just to run tests. And what about puppeteer and chrome headless engine? Hey, I had an idea: implement the cool gherkin syntax with puppeteer in pyhton.</p>

<h2 id="about-gherkin">About Gherkin</h2>

<p>Gherkin is a syntax which looks like:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Feature: Use the rest test suite

	Scenario: We can fetch data from an api
		Given I add these headers:
			| accept | application/json |
		Given I prepare a "GET" request to "/todos/1"
		When I send the request
		Then print the last json response
		And the json node "anythng" should not exist
		And the json node "userId" should exist
		And the json node "id" should be equal to "1"
</code></pre></div></div>

<p>It’s readable by human, (I mean, by non-developer human :)) and it’s really easy to write.</p>

<h2 id="about-puppeteer">About Puppeteer</h2>

<p><a href="https://github.com/GoogleChrome/puppeteer">Puppeteer</a> is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol, and allows to execute actions in a headless (without visible UI) version of chrome.
In nodejs, these tests look like:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">puppeteer</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">puppeteer</span><span class="dl">'</span><span class="p">);</span>
<span class="p">(</span><span class="k">async</span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">browser</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">puppeteer</span><span class="p">.</span><span class="nx">launch</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">newPage</span><span class="p">();</span>
  <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">setRequestInterception</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
  <span class="nx">page</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="dl">'</span><span class="s1">request</span><span class="dl">'</span><span class="p">,</span> <span class="nx">request</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">resourceType</span><span class="p">()</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">image</span><span class="dl">'</span><span class="p">)</span>
      <span class="nx">request</span><span class="p">.</span><span class="nx">abort</span><span class="p">();</span>
    <span class="k">else</span>
      <span class="nx">request</span><span class="p">.</span><span class="k">continue</span><span class="p">();</span>
  <span class="p">});</span>
  <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">goto</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://news.google.com/news/</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">await</span> <span class="nx">page</span><span class="p">.</span><span class="nx">screenshot</span><span class="p">({</span><span class="na">path</span><span class="p">:</span> <span class="dl">'</span><span class="s1">news.png</span><span class="dl">'</span><span class="p">,</span> <span class="na">fullPage</span><span class="p">:</span> <span class="kc">true</span><span class="p">});</span>

  <span class="k">await</span> <span class="nx">browser</span><span class="p">.</span><span class="nx">close</span><span class="p">();</span>
<span class="p">})();</span>
</code></pre></div></div>

<p>Understandable by developers, but a bit ugly (and boring to write)…
However, using a real browser as chrome to play functional tests has a lot of advantages:</p>
<ul>
  <li>javascript testing</li>
  <li>speed</li>
  <li>easy to use</li>
</ul>

<h2 id="lets-play-with-pyppettheater">Let’s play with pyppettheater</h2>

<p>Pyppettheater is one of my amaziiing idea (and it’s what this article is about).
In python, the puppether engine can be used thanks to <a href="https://github.com/miyakogi/pyppeteer">Pyppeteer</a>. And to parse the gherkin features files, we’ll use gherkin-parser.</p>

<h3 id="install-pyppetheater">Install pyppetheater</h3>

<p>First, you have to install the chrome headless engine (under ubuntu):</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>wget <span class="nt">-q</span> <span class="nt">-O</span> - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
<span class="nv">$ </span>sh <span class="nt">-c</span> <span class="s1">'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" &gt;&gt; /etc/apt/sources.list.d/google-chrome.list'</span>
<span class="nv">$ </span>apt-get <span class="nt">-y</span> update
<span class="nv">$ </span>apt-get <span class="nb">install</span> <span class="nt">-yqq</span> unzip <span class="nb">sudo </span>python3-pip mysql-client google-chrome-stable
<span class="nv">$ </span>wget <span class="nt">-O</span> /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/<span class="sb">`</span>curl <span class="nt">-sS</span> chromedriver.storage.googleapis.com/LATEST_RELEASE<span class="sb">`</span>/chromedriver_linux64.zip
<span class="nv">$ </span>unzip /tmp/chromedriver.zip chromedriver <span class="nt">-d</span> /usr/local/bin/
<span class="nv">$ </span>pyppeteer-install
</code></pre></div></div>

<p>And install pyppettheater:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pip3 <span class="nb">install </span>Pyppetheater
</code></pre></div></div>

<p>You can now run the <code class="highlighter-rouge">pyppet_theater</code> binary:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pyppet_theater /path/to/your/yml/or/feature/file
</code></pre></div></div>

<h3 id="some-example-of-test">Some example of test</h3>

<p>Each test suite is a yml file which contains some parameters (if needed):</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
    <span class="na">rest</span><span class="pi">:</span>
        <span class="na">base_endpoint</span><span class="pi">:</span> <span class="s">https://jsonplaceholder.typicode.com</span>

<span class="na">scenarios</span><span class="pi">:</span>
    <span class="na">0</span><span class="pi">:</span> <span class="s">rest/rest-suite.feature</span>
</code></pre></div></div>

<h4 id="for-a-rest-api">For a REST API:</h4>

<p>For example in the rest/rest-suite.feature:</p>

<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Feature: Use the rest test suite

	Scenario: We can fetch data from an api
		Given I add these headers:
			| accept | application/json |
		Given I prepare a "GET" request to "/todos/1"
		When I send the request
		Then print the last json response
		And the json node "anythng" should not exist
		And the json node "userId" should exist
		And the json node "id" should be equal to "1"
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pyppet_theater rest.yml
</code></pre></div></div>

<h4 id="for-a-frontend-test">For a frontend test:</h4>

<p>For example, in a game/registration.feature file:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Feature: Create an account on a website

	Scenario: As a visitor, I register on the website
		Given I go on "http://my-website.com"
		And I click on "\#signup-link"
		And I type "player6" in field "\#id_username"
		And I type "somepassword" in field "\#id_password1"
		And I type "somepassword" in field "\#id_password2"
		When I click on "\#register"
		Then I should be on "http://my-website.com"
</code></pre></div></div>

<p>And run it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pyppet_theater game/registration.feature
</code></pre></div></div>

<h4 id="for-mysql-tests">For mysql tests:</h4>
<p>With a <code class="highlighter-rouge">mysql.yml</code> file</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
    <span class="na">mysql</span><span class="pi">:</span>
        <span class="na">db_host</span><span class="pi">:</span> <span class="s">127.0.0.1</span>
        <span class="na">db_user</span><span class="pi">:</span> <span class="s">root</span>
        <span class="na">db_password</span><span class="pi">:</span> <span class="s">root</span>
        <span class="na">db_name</span><span class="pi">:</span> <span class="s">test</span>

<span class="na">scenarios</span><span class="pi">:</span>
    <span class="na">0</span><span class="pi">:</span> <span class="s2">"</span><span class="s">mysql/mysql-suite.feature"</span>

</code></pre></div></div>
<p>And, in a mysql/mysql-suite.feature:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Feature: Use the mysql test suite

	Scenario: We can select, update and check existence
		Given the row with "id" equal to "1" in table "some_table" should exist
		And the row with "id" equal to "1" in table "some_table" has "some_key" equal to "some_value"
		Then the row with "id" equal to "1" in table "some_table" should have "some_key" equal to "some_value"
		And the row with "some_key" equal to "some_value" in table "some_table" should exist

	Scenario: We can delete and check existence
		Given the row with "id" equal to "1" in table "some_table" does not exist
		Then the row with "id" equal to "1" in table "some_table" should not exist

</code></pre></div></div>

<p>And run it:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>pyppet_theater mysql.yml
</code></pre></div></div>

<p>You can also use the <a href="https://cloud.docker.com/u/gpenverne/repository/docker/gpenverne/pyppettheater">docker image</a>.</p>

<p>To know more about Pyppettheater, <a href="https://github.com/gpenverne/pyppettheater">go to the repo!</a></p>