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.
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 the 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 are 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 execute our Cucumber tests:-
npx cucumber-js
We will see that all our test secaniors have passed: –
But what happens once our valid token expires? Would the test cases still pass? Let’s replace our valid token with an 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 scenarios where the token has expired or an invalid token; this is a common problem while writing e2e tests with cucumber; each time, we have to provide tokens for the scenario to be passed since, after a while, they will expire.
Introduction to Keploy
Keploy is an open source API testing platform that generates 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 & requests, 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 a 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 test cases 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 the 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":"[email protected]", "password":"1234" }'
Login User
curl --request POST
--url http://localhost:4040/login
--header 'Content-Type: application/json'
--data '{
"email":"[email protected]",
"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 the fact 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":"[email protected]", "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":"[email protected]", "password":"1234" }'
And under noise, add body.token: [] :-
assertions: noise: header.Date: [] body.token: []
Now let’s run our test cases again, we can see that our testcases have passed except one which is failing due to 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, additional tools may be required for specific performance testing needs.
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.