Test web API's with Perl and Cucumber

dragostrif

DragosTrif

Posted on January 31, 2022

Test web API's with Perl and Cucumber

Behavior-Driven Development is an agile software development process that encourages teams to use conversation and concrete examples to formalize a shared understanding of how the application should behave.
Cucumber Open is the world's leading tool for BDD.

In this tutorial I want to show to you how to test web API using
Test::BDD::Cucumber, Mojo::UserAgent and Test2::V0 modules.

First we need to install Carton the Perl module dependency manager.

cpanm Carton
Enter fullscreen mode Exit fullscreen mode

Now we can mock an API using Mojolicious lite web-framework.

  • Create a cpanfile:
touch cpanfile
Enter fullscreen mode Exit fullscreen mode
  • Add Mojolicious web-framework to the cpanfile:
requires 'Mojolicious';
Enter fullscreen mode Exit fullscreen mode
  • Install Mojolicious using carton:
carton install
Enter fullscreen mode Exit fullscreen mode
  • Generate a Mojolicious lite app:
carton exec -- mojo generate lite-app mock.pl
Enter fullscreen mode Exit fullscreen mode
  • Edit the mock.pl to return a json:
#!/usr/bin/env perl
use Mojolicious::Lite -signatures;

any '/json' => sub ($c) {
  my $dc_name = $c->param('dc');

  my $api_data_centers = {
    ro => { dc => 'ro', machine => '10.0.0.1', os => 'linux mint'},
    uk => { dc => 'uk', machine => '10.0.0.2', os => 'ubuntu' }
  };

  $c->render( json => $api_data_centers->{lc $dc_name} );
};

app->start;


Enter fullscreen mode Exit fullscreen mode
  • Start the server:
carton exec -- morbo mock.pl
# you can test the API at this url:
# http://127.0.0.1:3000/json?dc=ro
Enter fullscreen mode Exit fullscreen mode

After the server is up and running we can move to the Cucumber part.

  • Create the Test::BDD::Cucumber file structure:
test_api/
└── features
    ├── step_definitions
    │   └── test_api_steps.pl
    └── test_api.feature
Enter fullscreen mode Exit fullscreen mode
  • Add Cucumber and test modules to cpanfile file:
requires 'Test::BDD::Cucumber';
requires 'Test2::V0';
requires 'Test::BDD::Cucumber::Harness::Html';
Enter fullscreen mode Exit fullscreen mode
  • Install Cucumber and test modules:
carton install
Enter fullscreen mode Exit fullscreen mode

Edit the feature file

Open the test_api/features/test_api.feature and add in this snippet:

Feature: Test a basic api with Test::BDD::Cucumber
  Test a basic api with Test::BDD::Cucumber

  Background:
   I get the request object

    Scenario: test api request
      Given I query for the RO data center
      When I make a GET request to the url /json
      Then http code is <code>

      Examples:
        | code  |
        | 200   |
Enter fullscreen mode Exit fullscreen mode

To understand the above example you need some basic knowledge about the gherkin verbs:

  1. Given is used for a predefined action.
  2. When is used for an action done by the user (making a request).
  3. Then is used for evaluating the user action.
  4. And duplicates the verb above
  5. But duplicates the verb above

Also as you can see that variables can be added to the feature file using the Examples table.
Running carton exec -- local/bin/pherkin test_api/ will display all the scenario steps in orange. This means that code to implement them was not written yet.

Writing step definitions

Open the test_api/features/step_definitions/test_api_steps.pl and add in this code snippet:


use strict;
use warnings;

use Test::BDD::Cucumber::StepFile;
use Mojo::UserAgent;
use Mojo::URL;
use Test2::V0;

Before
  sub {
    my $ua = Mojo::UserAgent->new();
    S->{ua} = $ua;
    note("we have a Mojo::UserAgent obj in S");
    return 1;
  };

Given qr/I use the (\w+) data center/ => sub {
  S->{query} = { dc => C->matches()->[0] },
  return 1;
};

When qr/I make a (\w+) request to the url ([\w\W]+)/ =>  sub {
  my $ua = S->{ua};
  my $endpoint = C->matches()->[1];
  my $http_verb = lc C->matches()->[0];

  my $url = Mojo::URL->new();
  $url->scheme('http');
  $url->host('127.0.0.1');
  $url->port('3000');
  $url->query( S->{query} );
  $url->path(lc $endpoint);

  my $tx = $ua->insecure(1)->$http_verb( $url => { Accept => 'application/json'} );

  note (  "Request:\n", '-' x 50, "\n",
    join( ' ',
    $tx->req->method,
    $tx->req->url->path,
    'HTTP/' . $tx->req->version ),
    "\n", $tx->req->headers->to_string );


  my $res = $tx->result();

  note("Reponse:\n", '-' x 50, "\n");
  note($res->body());
  note("Reponse code:\n", '-' x 50, "\n");
  note($res->code);

  S->{result} = $res;

  return 1;
};

Then qr/http code is (\d+)/ => sub {
  my $expected = C->matches()->[0];
  my $got = S->{result}->code();

  is($got, $expected, 'status code is ok');

  return 1;
};

Enter fullscreen mode Exit fullscreen mode

Running carton exec -- local/bin/pherkin test_api/ will display all the scenario steps in green, showing you that everything is ok.

Understanding "S" and "C" objects
The "S" and "C" objects are automatically created. "S" stores data for the duration of a scenario while "C" stores data for the duration of a step. Calling C->matches() will get you an array with all the matches the regex did in the current step. You can use instead regex capture groups like $1 if you prefer it.

How everything worked together:

  • The Background code in the feature file executed the Before block in the step file which adds Mojo::UserAgent object and stores it in S.
  • Given qr/I query for the (\w+) data center/ This step adds the {dc => 'ro' } hash on S. Later this is used to add this ?dc=ro part to the request query.
  • When I make a GET request to the url /json
    This step does a bunch of things:

    1. reads the http verb and the url passed to him from the feature file and uses them to create Mojo::URL->new();
    2. makes a request using the object Mojo::UserAgent object stored in S.
    3. uses the Test2::V0 note() subroutine to document what happened.
    4. stores the request response in S->{result};
  • Then http code is 200
    This step reads the expect status code sent in from the feature file using a regex and compares it with whatever value we have stored in the response object.

Creating a library for common steps

If your team manages more then one API you don't want to rewrite the same tests over and over again for obvious reasons.
Cucumber allows you to have such steps libraries:

mkdir lib
vim lib/Steps.pm
Enter fullscreen mode Exit fullscreen mode

Now move this step Then qr/http code is (\d+)/to a new lib.

package Steps;
use strict;
use warnings;

use Test::BDD::Cucumber::StepFile;
use Test2::V0;

Then qr/http code is (\d+)/ => sub {
  my $expected = C->matches()->[0];
  my $got = S->{result}->code();

  is($got, $expected, 'status code is ok');

  return 1;
};

1;

Enter fullscreen mode Exit fullscreen mode

Then make sure you load the lib in test_api/features/step_definitions/test_api_steps.pl.

use lib 'lib';
use Steps;
Enter fullscreen mode Exit fullscreen mode

Running the code again should result in a pass.

Using tags

If a feature has more than one scenario and you don't want to run them all at once you can use tags:

    @json_tag @all
    Scenario: test api json request
      Given I query for the RO data center
      When I make a GET request to the url /json
      Then http code is <code>

      Examples:
        | code  |
        | 200   |

   @xml_tag @all
    Scenario: test api xml request
      Given I query for the RO data center
      When I make a GET request to the url /xml
      Then http code is <code>

      Examples:
        | code  |
        | 200   |


Enter fullscreen mode Exit fullscreen mode
  • Call the json scenario:
carton exec -- local/bin/pherkin --tags=@json_tag test_api/
Enter fullscreen mode Exit fullscreen mode
  • Run all scenarios, except the xml one:
carton exec -- local/bin/pherkin --tags=~@xml_tag test_api/
Enter fullscreen mode Exit fullscreen mode

Generating an html report

To generate an html report use this:

carton exec -- local/bin/pherkin -o Html test_api/ > test.html
#  -o Html flag calls Test::BDD::Cucumber::Harness::Html harness 
Enter fullscreen mode Exit fullscreen mode

The result should look like this:

html report
If you click open a step you will see the details reported by note()
details reported by note

Integration with prove

carton exec -- prove -rv --source Perl --ext=.t --source Feature --ext=.feature test_api/
Enter fullscreen mode Exit fullscreen mode

Bibliography

💖 💪 🙅 🚩
dragostrif
DragosTrif

Posted on January 31, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related