Integration and HTTP Testing
Learning Objectives
- You know of integration testing and know how to test the HTTP interface of a Hono application.
While unit testing focuses on testing individual components, integration testing focuses on combining components and testing them as a group.
One approach for integration testing in web applications is testing the HTTP interfaces that the applications provide. This involves sending requests to the application and verifying that the responses match expectations.
Here, we briefly look into testing the HTTP interfaces of a Hono application.
First integration test
Let’s look into testing Hono applications. In the following, we assume that we have an application that has a single route, GET /
, and that the route returns an object with the message “Hello World!"" as a response to all requests. The application is in src/app.js
, which exports it for use.
import { Hono } from "jsr:@hono/hono@4.6.5";
const app = new Hono();
app.get("/", (c) => c.json({ message: "Hello World!" }));
export default app;
To test the above application, create a test file tests/app_test.js
with the following content.
import { assertEquals } from "jsr:@std/assert@1.0.8";
import app from "../src/app.js";
Deno.test("GET / returns { message: 'Hello World!' }", async () => {
let res = await app.request("/");
let body = await res.json();
assertEquals(body, { message: "Hello World!" });
});
Then, run the test using deno test
in the folder that contains the folders src
and tests
. The output is as follows.
running 1 test from ./tests/app_test.js
GET / returns { message: 'Hello World!' } ... ok (2ms)
ok | 1 passed | 0 failed (4ms)
Above, we took the Hono application and made a request to it, using Hono’s request
-method. The request
method accepts either a path for GET requests or a Request object, when making other types of requests. The request
-method returns a Response object.
The Request and Response objects are standard objects in the Fetch API. The request object provides methods for working with the request, and the response object provides methods for working with the response.
Hono’s method request
effectively allows us to simulate a request to the application, and then assessing whether the response is as expected.
Note that in app.js
, we export the application. If we would start the server in app.js
using Deno.serve
instead of exporting the application, the tests would not have access to the application.
Testing with requests
In the above example, we used the request
-method to make a GET request to the application. For other types of requests, and for requests with more information, we create a Request object and pass it to the request
-method. When creating a request for testing, the request is made to localhost
.
The example below demonstrates writing a test that makes a GET request to the root of the application, similar to the earlier test, but this time by explicitly creating a Request object.
import { assertEquals } from "jsr:@std/assert@1.0.8";
import app from "../src/app.js";
Deno.test("GET / returns { message: 'Hello World!' }", async () => {
let request = new Request("http://localhost/");
let res = await app.request(request);
let body = await res.json();
assertEquals(body, { message: "Hello World!" });
});
After the change, the tests pass successfully.
running 1 test from ./tests/app_test.js
GET / returns { message: 'Hello World!' } ... ok (2ms)
ok | 1 passed | 0 failed (3ms)
Sending JSON data to server
When working with the Request object, we can also send JSON-formatted data to the server.
Let’s add a new endpoint to our application and test it. The endpoint handles POST requests to the root path of the application, parsing JSON data from the request body, and returning it with an additional message
property set to “Hello World!”.
import { Hono } from "jsr:@hono/hono@4.6.5";
const app = new Hono();
app.get("/", (c) => c.json({ message: "Hello World!" }));
app.post("/", async (c) => {
const body = await c.req.json();
body.message = "Hello World!";
return c.json(body);
});
export default app;
To test the new endpoint, we create a new test in app_test.js
. The test sends a POST request to the application with a JSON object in the request body, and verifies that the response contains the expected data.
import { assertEquals } from "jsr:@std/assert@1.0.8";
import app from "../src/app.js";
Deno.test("GET / returns { message: 'Hello World!' }", async () => {
let request = new Request("http://localhost/");
let res = await app.request(request);
let body = await res.json();
assertEquals(body, { message: "Hello World!" });
});
Deno.test("POST / returns JSON object with additional message property", async () => {
let request = new Request("http://localhost/", {
method: "POST",
body: JSON.stringify({ data: "Hello!" }),
});
let res = await app.request(request);
let body = await res.json();
assertEquals(body, { message: "Hello World!", data: "Hello!" });
});
Now, both tests pass.
GET / returns { message: 'Hello World!' } ... ok (2ms)
POST / returns JSON object with additional message property ... ok (0ms)
ok | 2 passed | 0 failed (4ms)
Mocking and stubbing
A part of integration testing is mocking and stubbing. These techniques are used to simulate the behavior of real objects or functions, often used to test how a function interacts with its dependencies. Mocking involves creating fake objects or functions that simulate the behavior of real objects, while stubbing is a specific type of mocking where you define the behavior of a function or dependency, such as returning a predefined value when called.
At the moment, when using Deno, we’re using a lot of ES6 modules. This can make mocking and stubbing a bit more challenging. For example, although Deno provides stub functions, stubbing a full module such as the following is not currently trivial.
import * as helloService from "./services/helloService.js";
We omit this part from the course material for now, favoring end-to-end testing with Playwright, which provides a more comprehensive testing approach, and which we will look into in a moment.