Development teams provisioning software services face a constant trade-off between speed and accuracy. New features should be made available in the least possible time with a high amount of accuracy, meaning no downtime. Unforeseen downtime due to human error is common for any manual integration processes your team uses to manage codebases. This kind of unexpected interruption can be one of the key drivers for a team to take on the challenge of automating their integration process.

Continuous Integration (CI) occupies a sweet spot between speed and accuracy where features are made available as soon as possible. If a problem with the new code causes the build to fail, the contaminated build is not made available to the customer. The result is that the customer experiences no downtime.

In this article, I will use CircleCI to demonstrate how CI can be applied to a Deno project. Deno is a simple, modern, and secure runtime for JavaScript and TypeScript. Using Deno gives you these advantages when you create a project:

  1. Deno is secure by default. There is no file, network, or environment access, unless you explicitly enable it.
  2. It supports TypeScript out of the box.
  3. It ships as a single executable file.

The sample project for this tutorial will be an API built with Oak. This API has one endpoint which returns a list of quiz questions. A test case will be written for the endpoint using SuperOak.

Prerequisites

Before you start, make sure these items are installed on your system:

  • An up-to-date installation of Deno

For repository management and continuous integration, you need:

  • A GitHub account. You can create one here.
  • A CircleCI account. You can create one here. To easily connect your GitHub projects, you can sign up with your GitHub account.

Getting started

Create a new directory to hold all the project files:

mkdir deno_circleci

cd deno_circleci

To keep things simple, you can import a hard-coded array of questions in our project. Create a file named questions.ts and add this:

export default [
  {
    id: 1,
    question: "The HTML5 standard was published in 2014.",
    correct_answer: "True",
  },
  {
    id: 2,
    question:
      "Which computer hardware device provides an interface for all other connected devices to communicate?",
    correct_answer: "Motherboard",
  },
  {
    id: 3,
    question: "On which day did the World Wide Web go online?",
    correct_answer: "December 20, 1990",
  },
  {
    id: 4,
    question: "What is the main CPU in the Sega Mega Drive / Sega Genesis?",
    correct_answer: "Motorola 68000",
  },
  {
    id: 5,
    question: "Android versions are named in alphabetical order.",
    correct_answer: "True",
  },
  {
    id: 6,
    question:
      "What was the first Android version specifically optimized for tablets?",
    correct_answer: "Honeycomb",
  },
  {
    id: 7,
    question:
      "Which programming language shares its name with an island in Indonesia?",
    correct_answer: "Java",
  },
  {
    id: 8,
    question: "What does RAID stand for?",
    correct_answer: "Redundant Array of Independent Disks",
  },
  {
    id: 9,
    question:
      "Which of the following computer components can be built using only NAND gates?",
    correct_answer: "ALU",
  },
  {
    id: 10,
    question:
      "What was the name of the security vulnerability found in Bash in 2014?",
    correct_answer: "Shellshock",
  },
];

Setting up a Deno server

In this section, we will use the Oak middleware framework to set up our Deno server. It is a framework for Deno’s HTTP server. It is comparable to Koa and Express. To begin, create a file named server.ts and add this code to it:

import { Application, Router } from "https://deno.land/x/oak@v7.5.0/mod.ts";
import questions from "./questions.ts";

const app = new Application();
const port = 8000;

const router = new Router();

router.get("/", (context) => {
  context.response.type = "application/json";
  context.response.body = { questions };
});

app.addEventListener("error", (event) => {
  console.error(event.error);
});

app.use(router.routes());
app.use(router.allowedMethods());

app.listen({ port });
console.log(`Server is running on port ${port}`);

export default app;

In this example, we are using the Application and Router modules imported from the Oak framework to create a new application that listens to requests on port 8000. Then we declare a route that returns a JSON response containing the questions stored in questions.ts. We also add an event listener to be triggered every time an error occurs. This will be helpful if you need to debug when there is an error.

Running the application

We can run the application to see what has been done so far using this command:

deno run --allow-net server.ts

Navigate to http://localhost:8000/ to review the response.

API response

Writing tests for the application

Now that we have set up the server and run the app, we can write a test case for our API endpoint. Create a new file called server.test.ts and add this code to it:

import { superoak } from "https://deno.land/x/superoak@4.2.0/mod.ts";
import { delay } from "https://deno.land/x/delay@v0.2.0/mod.ts";
import app from "./server.ts";

Deno.test(
  "it should return a JSON response containing questions with status code 200",
  async () => {
    const request = await superoak(app);
    await request
      .get("/")
      .expect(200)
      .expect("Content-Type", /json/)
      .expect(/"questions":/);
  }
);

// Forcefully exit the Deno process once all tests are done.
Deno.test({
  name: "exit the process forcefully after all the tests are done\n",
  async fn() {
    await delay(3000);
    Deno.exit(0);
  },
  sanitizeExit: false,
});

In this example, we import the superoak module and the application we created in server.ts. Then we declare a test case where we create a request using SuperOak and our application. We then make a GET request to the application index route and make the following assertions:

  1. A HTTP:OK response (200) is returned
  2. The response received is a JSON response
  3. The JSON response received has a node named questions

Running the test locally

Navigate to the terminal. From the root of the application, stop the server from running using CTRL + C. Then issue this command to run the test:

deno test --allow-net server.test.ts

This should be the response:

test it should return a JSON response containing questions with status code 200 ... ok (30ms)
test exit the process forcefully after all the tests are done
 ...%

With your test cases in place, you can add the CircleCI configuration.

Adding CircleCI configuration

In your project root directory, create a folder named .circleci. Add a file called config.yml to that directory.

mkdir .circleci

touch .circleci/config.yml

In .circleci/config.yml add:

# Use the latest 2.1 version of CircleCI pipeline process engine.
version: 2.1

jobs:
  build-and-test:
    docker:
      - image: denoland/deno:1.10.3
    steps:
      - checkout
      - run: |
          deno test --allow-net server.test.ts

workflows:
  sample:
    jobs:
      - build-and-test

In this example, the first thing we do is to specify the version of CircleCI pipeline process engine. Always specify the latest version (2.1 at the time this article was written).

After specifying the CircleCI version, we specify a job named build-and-test. This job has two key blocks: docker and steps. The docker block specifies the images we need for our build process to run successfully. In this case, we are using the official Deno Docker image.

The steps block does the following:

  1. Checks out the latest code from our GitHub repository
  2. Runs the tests in server.test.ts

The build-and-test job is executed as specified in the workflows block.

Next, we need to set up a repository on GitHub and link the project to CircleCI. For help, review this post: Pushing your project to GitHub.

Adding the project to CircleCI

Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be displayed on your project’s dashboard.

Next to your deno_circleci project, click Set Up Project.

CircleCI will detect the config.yml file within the project. Click Use Existing Config and then Start Building. Your first build process will start running and complete successfully.

Click build-and-test to review the job steps and the status of each job.

CI Result

Conclusion

In this tutorial, I have shown you how to set up a continuous integration pipeline for a Deno application using GitHub and CircleCI. While our application was a simple one, with just one endpoint and one test case, we covered the key areas of pipeline configuration and feature testing.

CI builds on software development best practices in testing and version control to automate the process of adding new features to software. This removes the risk of human error causing downtime in the production environment. It also adds an additional level of quality control and assurance to the software being maintained. Give continuous integration a try and make code base bottlenecks a thing of the past for your team!

The entire codebase for this tutorial is available on GitHub.


Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.