In our previous blog, we explored how to build a modern web server using BunJs and Prisma, integrating robust authentication mechanisms with JWT tokens. Now, it's time to ensure our application is reliable and error-free through thorough testing.
In this blog, we'll dive into testing methodologies using Cucumber JS and Keploy, both are a powerful tools that streamline the testing process and enhance the quality of our application.
Understanding the Importance of Testing
By systematically executing test cases, developers can uncover bugs and errors early in the development process, preventing costly issues in later stages. For instance, in our BunJs web application, thorough testing would involve scenarios such as user authentication, post creation, and database interactions. By testing each feature extensively, developers can verify that user authentication works as expected, posts are created and retrieved accurately, and database queries return the correct results.
And testing our BunJs application's authentication mechanism ensures that users can securely sign up, log in, and access protected resources. Additionally, performance testing can validate that the application performs efficiently, even under high traffic loads. By prioritizing testing throughout the development lifecycle, teams can build robust, secure, and user-friendly applications that meet the needs and expectations of their users while minimizing the risk of critical failures and enhancing overall quality.
Testing BunJs with Cucumber JS
First, let's create our work directory and install necessary dependencies via npm:
mkdir features && mkdir features/support
npm i @cucumber/cucumber chai
Let's move towards writing the tests and setting up folder structures for cucumber-js; this is how the folder structure should look like
├── features
│ ├── support
│ │ ├── steps.mjs
│ ├── post.feature
We have already prepared our folder structure similar to above while installing dependencies,so now let's start with steps.mjs
file: -
import { Given, When, Then } from '@cucumber/cucumber';
import { expect } from 'chai';
import axios from 'axios';
let createdRecipeId;
Given('I have a valid token', async function () {
// Signin to get the valid token
this.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Ijk3Y2E1MWUzLWFiMWYtNDdkZC04ODM3LWFmMjkwZGQ1ZDAzYiIsImVtYWlsIjoiaXNzc2FsY3VwbmFtZGVAYWJjLmNvbSIsImlhdCI6MTcxNTU4MjEyNCwiZXhwIjoxNzE1NjY4NTI0fQ.T3WR8TO1uiV9t_ZIPm93JbdgWpYeXiUkRW3CUeyyOVQ';
});
When('I create a new recipe with title {string} and body {string}', async function (title, body) {
try {
const response = await axios.post('http://localhost:4040/create-post', {
title: title,
body: body,
}, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
expect(response.status).to.equal(200);
if (response.data && response.data.recipe && response.data.recipe.id) {
createdRecipeId = response.data.recipe.id;
} else {
throw new Error('Failed to retrieve recipe ID from response');
}
} catch (error) {
console.error('Error:', error); // Log any errors
this.error = error.response.data.error;
}
});
Then('I should receive a successful response with the created recipe', function () {
expect(this.error).to.be.undefined;
expect(createdRecipeId).to.not.be.undefined;
});
Given('I have an invalid token', function () {
this.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJ4eXpAYWJjLmNvbSIsImlhdCI6MTcxMzI1NTk2NSwiZXhwIjoxNzEzMzQyMzY1fQ.0RvrxVrkG6pQNGw0tjcDFqQuhmbVCmUcXuEy1brMED0';
// Create an Expired Token
});
When('I attempt to create a new recipe with title {string} and body {string}', async function (title, body) {
try {
const response = await axios.post('http://localhost:4040/create-post', {
title: title,
body: body,
}, {
headers: {
Authorization: `Bearer ${this.token}`,
},
});
if (response.data && response.data.recipe && response.data.recipe.id) {
createdRecipeId = response.data.recipe.id;
}
} catch (error) {
console.error('Error:', error); // Log any errors
this.error = error.response.data.error;
this.responseStatus = error.response.status;
}
});
Then('I should receive an ok response with status code {int}', function (statusCode) {
expect(this.error).to.be.undefined;
expect(createdRecipeId).to.not.be.undefined;
});
Writing Feature Files with Gherkin Syntax
Now that we have our step file ready, it's time to create our post.feature
file based on our steps.
Feature: Creating a new recipe post
Scenario: Create a new recipe post with a valid token
Given I have a valid token
When I create a new recipe with title "Delicious Pasta" and body "A simple pasta recipe"
Then I should receive a successful response with the created recipe
Scenario: Attempt to create a new recipe post with an invalid token
Given I have an invalid token
When I attempt to create a new recipe with title "Delicious Pasta" and body "A simple pasta recipe"
Then I should receive an ok response with status code 200
Executing Tests with Cucumber JS
Now that we have our post.features
and steps.mjs
ready to be executed, let's start our application first before running our test.
# Start our database instance
sudo docker-compose up -d
# Start our application in dev mode
bun --watch index.ts
Once we have our application and postgres database instance up and running, we can executed our Cucumber tests:-
npx cucumber-js
We will see that all our test secaniors has passed: -
But what happens once our valid token expire would the test cases still pass?
Let's replace our valid token with invalid token and see if our test cases are still passing or not
// existing steps
...
Given('I have a valid token', async function () {
// Replaced the token with invalid token
this.token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJ4eXpAYWJjLmNvbSIsImlhdCI6MTcxMzI1NTk2NSwiZXhwIjoxNzEzMzQyMzY1fQ.0RvrxVrkG6pQNGw0tjcDFqQuhmbVCmUcXuEy1brMED0';
});
...
//existing steps
Now when we run our tests again we get:-
Our first scenario failed as the token we provided was not correct. This is one of the scenario where token has been expired or invalid token, this is a common problem while writting e2e tests with cucumber, each time we have to provide token for the scenario to be passed since after a while they will get expried.
Introduction to Keploy
Keploy is an Open Source API testing platform to generate Test cases out of data calls along with data mocks.
In simple words, with keploy we don’t have to create or maintain manual test cases. As it records all network interactions from the application and based on expected responses & request it generates test cases and data mocks to make our work easy and efficient.
Testing BunJs application with Keploy
Let's get started by installing the Keploy with one-click command:-
curl -O https://raw.githubusercontent.com/keploy/keploy/main/keploy.sh && source keploy.sh
You should see something like this:
▓██▓▄
▓▓▓▓██▓█▓▄
████████▓▒
▀▓▓███▄ ▄▄ ▄ ▌
▄▌▌▓▓████▄ ██ ▓█▀ ▄▌▀▄ ▓▓▌▄ ▓█ ▄▌▓▓▌▄ ▌▌ ▓
▓█████████▌▓▓ ██▓█▄ ▓█▄▓▓ ▐█▌ ██ ▓█ █▌ ██ █▌ █▓
▓▓▓▓▀▀▀▀▓▓▓▓▓▓▌ ██ █▓ ▓▌▄▄ ▐█▓▄▓█▀ █▓█ ▀█▄▄█▀ █▓█
▓▌ ▐█▌ █▌
▓
Keploy CLI
Available Commands:
example Example to record and test via keploy
generate-config generate the keploy configuration file
record record the keploy testcases from the API calls
test run the recorded testcases and execute assertions
update Update Keploy
Flags:
--debug Run in debug mode
-h, --help help for keploy
-v, --version version for keploy
Use "keploy [command] --help" for more information about a command.
Now we are all set to create testcases with keploy.
Create Tests for our BunJs application
Since Keploy uses eBPF to instrument applications without code changes, all we need to do is pass application running command to keploy:-
keploy record -c "bun --watch index.ts"
Let's make API calls to record the interaction between our BunJs application and Prisma-ORM based Postgres
## Create User
curl --request POST \
--url http://localhost:4040/signup \
--header 'Content-Type: application/json' \
--data '{
"name":"Yash",
"email":"isssalcupnamde@rbc.com",
"password":"1234"
}'
## Login User
curl --request POST \
--url http://localhost:4040/login \
--header 'Content-Type: application/json' \
--data '{
"email":"isssalcupnamde@rbc.com",
"password":"1234"
}'
## Create a Post
curl --request POST \
--url http://localhost:4040/create-post \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imlzc3NhbGN1cG5hbWRlQHJiYy5jb20iLCJpYXQiOjE3MTU1ODQzMDYsImV4cCI6MTcxNTY3MDcwNn0.G7llqJc27ZQFunDiNjduMt9FZ2B4MbB0Xbhk_y7pzT0' \
--data '{
"title":"Anime",
"body":"AOT from quantum universe"
}'
We should be able to see something like this:-
Once we have our test cases ready, let's execute them in keploy test mode:-
keploy test -c "bun --watch index.ts"
We will notice that one of our test cases failed due to fact the each time the user.token would be different
So to avoid this keploy provides two feature, time-freezing and denoising, for this blog we are using denoising feature. We know that our test-2
is failing, so let's open the test-2.yaml
file : -
version: api.keploy.io/v1beta1
kind: Http
name: test-2
spec:
metadata: {}
req:
method: POST
proto_major: 1
proto_minor: 1
url: http://localhost:4040/login
header:
Accept: '*/*'
Accept-Encoding: gzip, deflate, br
Cache-Control: no-cache
Connection: keep-alive
Content-Length: "63"
Content-Type: application/json
Host: localhost:4040
Postman-Token: 8568266d-8c8e-41c3-95cc-5782f7f2c717
User-Agent: PostmanRuntime/7.32.1
body: |-
{
"email":"isssalcupnamde@rbc.com",
"password":"1234"
}
timestamp: 2024-05-13T12:41:46.83176148+05:30
resp:
status_code: 200
header:
Content-Length: "224"
Content-Type: application/json
Date: Mon, 13 May 2024 07:11:46 GMT
body: '{"message":"User logged in successfully","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imlzc3NhbGN1cG5hbWRlQHJiYy5jb20iLCJpYXQiOjE3MTU1ODQzMDYsImV4cCI6MTcxNTY3MDcwNn0.G7llqJc27ZQFunDiNjduMt9FZ2B4MbB0Xbhk_y7pzT0"}'
status_message: OK
proto_major: 0
proto_minor: 0
timestamp: 2024-05-13T12:41:49.018139032+05:30
objects: []
assertions:
noise:
header.Date: []
body.token: []
created: 1715584309
curl: |-
curl --request POST \
--url http://localhost:4040/login \
--header 'Host: localhost:4040' \
--header 'User-Agent: PostmanRuntime/7.32.1' \
--header 'Cache-Control: no-cache' \
--header 'Postman-Token: 8568266d-8c8e-41c3-95cc-5782f7f2c717' \
--header 'Content-Type: application/json' \
--header 'Accept: */*' \
--header 'Connection: keep-alive' \
--header 'Accept-Encoding: gzip, deflate, br' \
--data '{
"email":"isssalcupnamde@rbc.com",
"password":"1234"
}'
And under noise add body.token: []
:-
assertions:
noise:
header.Date: []
body.token: []
Now let's run our testcases again, we can see that our testcases have passed except one which is failing due the error of prisma which as been reported to prisma here:- Could not figure out an ID in create · Issue #23907 · prisma/prisma (github.com)
Conclusion
In this comprehensive guide, we've explored the process of testing a BunJs web application using Cucumber JS and Keploy. By leveraging these powerful tools, developers can ensure the reliability, security, and functionality of their applications. With a focus on Behavior-Driven Development (BDD) and automated testing, teams can streamline their testing process and deliver high-quality software with confidence.
As testing continues to evolve, embracing new tools and methodologies is essential for staying ahead in the ever-changing landscape of web development.
FAQ's
What are the benefits of testing a BunJs application with Cucumber JS and Keploy?
Testing with Cucumber JS allows for behavior-driven development (BDD), ensuring that the application behaves as expected from the user's perspective. Keploy simplifies testing by automatically generating test cases based on recorded interactions, reducing manual effort and improving efficiency.
How does Cucumber JS help in testing BunJs applications?
Cucumber JS allows developers to write test scenarios in plain language using the Gherkin syntax, making it easy to understand and collaborate on tests. These scenarios can then be executed against the application to validate its behavior.
What is the advantage of using Keploy for testing BunJs applications?
Keploy automates the process of generating test cases by recording interactions between the application and external services. This eliminates the need for manual test case creation and ensures thorough test coverage.
How does Keploy handle authentication tokens in BunJs application testing
Keploy provides features like time-freezing and denoising to handle dynamic data like authentication tokens. These features ensure that tests remain stable even when data changes, such as token expiration, by allowing developers to specify which parts of the response to ignore during comparison.
Can Keploy be used for performance testing of BunJs applications?
While Keploy primarily focuses on functional testing by generating test cases from recorded interactions, it can indirectly contribute to performance testing by ensuring that the application functions correctly under various scenarios and load conditions. However, for specific performance testing needs, additional tools may be required.
How does testing with Cucumber JS and Keploy enhance the reliability of BunJs applications?
By systematically executing test cases and automating test generation, Cucumber JS and Keploy help identify bugs and errors early in the development process. This results in more reliable and robust BunJs applications that meet user expectations and minimize the risk of critical failures.