Test web API's with Perl and Cucumber
Tags: perl, cucumber, testing, bdd
Published: 2022-01-31 16:24:15

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.

```perl
cpanm Carton
```

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

- 
 Create a _cpanfile_:
```perl
touch cpanfile
```
- 
 Add Mojolicious web-framework to the _cpanfile_:
```
requires 'Mojolicious';
```

- 
 Install Mojolicious using carton:
```perl
carton install
```

- 
 Generate a Mojolicious lite app:
```perl
carton exec -- mojo generate lite-app mock.pl
```

- 
 Edit the mock.pl to return a json:
```perl
#!/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;


```

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

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

- Create the Test::BDD::Cucumber file structure:
```perl
test_api/
└── features
    ├── step_definitions
    │   └── test_api_steps.pl
    └── test_api.feature
```  
 

- Add Cucumber and test modules to cpanfile file:
```perl
requires 'Test::BDD::Cucumber';
requires 'Test2::V0';
requires 'Test::BDD::Cucumber::Harness::Html';
```
 

- Install Cucumber and test modules:
```perl
carton install
```


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

```gherkin
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   |
```
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:
```perl

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;
};

```
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:

```perl
mkdir lib
vim lib/Steps.pm
```
Now move this step `Then qr/http code is (\d+)/ `to a new lib.

```perl
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;

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

```perl
use lib 'lib';
use Steps;
```
  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:

```gherkin
    @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   |


```

- Call the json scenario:

```perl
carton exec -- local/bin/pherkin --tags=@json_tag test_api/
```

- Run all scenarios, except the xml one:

```perl
carton exec -- local/bin/pherkin --tags=~@xml_tag test_api/
```
     
## Generating an html report
To generate an html report use this:

```perl
carton exec -- local/bin/pherkin -o Html test_api/ > test.html
#  -o Html flag calls Test::BDD::Cucumber::Harness::Html harness 
```
The result should look like this:

![html report](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9wwva6op4ipa4x8y4yxf.png)
If you click open a step you will see the details reported by `note()`
![details reported by note](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wkxzy363p0eidd8q18dn.png)

## Integration with prove
```perl
carton exec -- prove -rv --source Perl --ext=.t --source Feature --ext=.feature test_api/
```

##  Bibliography
- [Cucumber & Perl by John Davies](https://perlschool.com/books/cucumber/)

- [Mojolicious Web Clients By brian d foy](https://perlschool.com/books/mojolicious-web-clients/)

- [Mocking a REST API by Doug Bell](https://mojolicious.io/blog/2017/12/08/day-8-mocking-a-rest-api/)