DragosTrif
Posted on January 31, 2022
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
Now we can mock an API using Mojolicious lite web-framework.
- Create a cpanfile:
touch cpanfile
- Add Mojolicious web-framework to the cpanfile:
requires 'Mojolicious';
- Install Mojolicious using carton:
carton install
- Generate a Mojolicious lite app:
carton exec -- mojo generate lite-app mock.pl
- 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;
- 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
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
- Add Cucumber and test modules to cpanfile file:
requires 'Test::BDD::Cucumber';
requires 'Test2::V0';
requires 'Test::BDD::Cucumber::Harness::Html';
- Install Cucumber and test modules:
carton install
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 |
To understand the above example you need some basic knowledge about the gherkin verbs:
- Given is used for a predefined action.
- When is used for an action done by the user (making a request).
- Then is used for evaluating the user action.
- And duplicates the verb above
- 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;
};
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:- reads the http verb and the url passed to him from the feature file and uses them to create
Mojo::URL->new();
- makes a request using the object
Mojo::UserAgent
object stored in S. - uses the Test2::V0 note() subroutine to document what happened.
- stores the request response in
S->{result};
- reads the http verb and the url passed to him from the feature file and uses them to create
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
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;
Then make sure you load the lib in test_api/features/step_definitions/test_api_steps.pl
.
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:
@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:
carton exec -- local/bin/pherkin --tags=@json_tag test_api/
- Run all scenarios, except the xml one:
carton exec -- local/bin/pherkin --tags=~@xml_tag test_api/
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
The result should look like this:
If you click open a step you will see the details reported by note()
Integration with prove
carton exec -- prove -rv --source Perl --ext=.t --source Feature --ext=.feature test_api/
Bibliography
Posted on January 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.