Integration and HTTP Testing
Learning objectives
- You have an idea of what integration tests are and how one could test the HTTP interface of a Hono application.
While unit testing focuses on testing individual components, integration testing focuses on combining these components and testing them as a group. In essence, integration testing is used to verify that the components work together as expected.
Integration tests seek to verify that components work together as expected.
One approach for integration testing in web applications is testing the HTTP interfaces that the applications provide. Effectively, this means making queries to the application, and verifying that the responses are as expected. Here, we briefly look into testing the HTTP interfaces of a Hono application.
Testing Hono applications
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 the string Hello World!
as a response to all requests. The application is in a file called app.js
from where the concrete application is exported for use (similarly to the course exercises).
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.get("/", (c) => c.text("Hello World!"));
export default app;
To test the above application, we can create a test file app_test.js
. To test the application from app.js
, we need to import it -- note the import without the curly brackets -- and to add a test that verifies that the application returns the expected response.
This is outlined in the example below.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
import app from "./app.js";
Deno.test("GET / returns 'Hello World!'", async () => {
let res = await app.request("/");
let body = await res.text();
assertEquals(body, "Hello World!");
});
When we try out the above test, we notice that it passes. The output from running the test is as follows.
deno test
Check file:///path-to-project/app_test.js
running 1 test from ./app_test.js
GET / returns 'Hello World!' ... ok (6ms)
ok | 1 passed | 0 failed (69ms)
In effect, we can make a GET request to the application using await app.request(path);
, where path
corresponds to the path that we make the query to. The response from the application is a Fetch API Response, which provides a handful of methods for working with the response.
As an example, the asynchronous method text of the Response reads the response and returns it as a string. In the example above, the response is "Hello World!", which is then compared to the expected value using Deno's assertEquals
method.
Not launching the application
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 of the app that is used for GET requests. For other types of requests, and for requests with more information, we can create a Request object and pass it to the request
-method -- by default, the application is available at localhost
. The example below demonstrates writing a test that is similar to the example above, but this time using a request object.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
import app from "./app.js";
Deno.test("GET / returns 'Hello World!'", async () => {
let request = new Request("http://localhost/");
let res = await app.request(request);
let body = await res.text();
assertEquals(body, "Hello World!");
});
With the above change, the tests continue working as expected.
deno test
Check file:///path-to-project/app_test.js
running 1 test from ./app_test.js
GET / returns 'Hello World!' ... ok (6ms)
ok | 1 passed | 0 failed (69ms)
Sending form data to server
Sending form data to the server is also possible. In the following example, we test that the server responds with "Hello Jane!" to a POST request with form data. Form data is represented using a FormData object that can be added to the request. The FormData object has a method append that can be used to add data to the form.
Once the form data object is ready, we pass it to the body
of the request. The example below demonstrates this.
import { assertEquals } from "https://deno.land/std@0.222.1/testing/asserts.ts";
import app from "./app.js";
Deno.test("POST / with 'Jane' as name returns 'Hello Jane!'", async () => {
let formData = new FormData();
formData.append("name", "Jane");
let request = new Request("http://localhost/", {
method: "POST",
body: formData,
});
let res = await app.request(request);
let body = await res.text();
assertEquals(body, "Hello Jane!");
});
With the above, the tests do not pass, so we need to adjust the application. To handle form data, we use the parseBody
method of the request from the context. The code below demonstrates an application that handles form data, returning a response that it partially formed based on the form data.
import { Hono } from "https://deno.land/x/hono@v3.12.11/mod.ts";
const app = new Hono();
app.post("/", async (c) => {
const body = await c.req.parseBody();
return c.text(`Hello ${body.name}!`);
});
export default app;