Test your Compute apps end-to-end with JavaScript

harmony7

Katsuyuki Omuro

Posted on January 29, 2024

Test your Compute apps end-to-end with JavaScript

Automated testing is a vital part of modern application development, and applications built for the edge on Fastly’s Compute platform are no exception. Today, developers can run tests for their Compute applications with custom shell scripts, but we wanted a more convenient and natural way for this to be done. Our new Compute Application Testing library allows you to use JavaScript to write your tests.

This new library makes writing a new test as easy as:

describe('/user.json', () => {
  it('returns 200 status and valid username', async () => {
    const resp = await app.fetch('/user.json');
    assert.strictEqual(resp.status, 200);
    const data = await resp.json();
    assert.ok(/^\w+$/.test(data.username));
  });
});
Enter fullscreen mode Exit fullscreen mode

In this post, we'll compare testing via shell script and the new Compute Application Testing library so you can choose the best way to test your code, regardless of the programming language used to write the Compute application.


Automated testing is a technique used in the software development process to prove that code works as it’s intended, and to ensure that changes made to the code will not break existing functionality. When combined with strategies such as continuous delivery, it helps developers build and deploy reliable software, enabling a team to always have a shippable, high-quality product ready.

There are a few types of testing: "Unit" testing refers to testing the smallest building blocks of your software in isolation, often at the function, class, or API level. "Integration" tests check the interactions between these various units that make up the software. Finally, "end-to-end" testing refers to a type of test that is applied against an entire application, checking that specified inputs produce an expected output.

Testing at all of these levels is important. In this post, we cover end-to-end testing, which is one effective way to ensure that your Fastly Compute application continues to produce an expected set of outputs.

How can we test Compute applications end-to-end?

Fastly Compute is a platform that executes WebAssembly modules across our global network of edge servers, compiled from a language of your choice, such as Rust, Go, or JavaScript. For development and testing, Fastly provides a local testing environment for Compute.

What does an end-to-end test look like for an application running on Fastly Compute? Because Compute applications are served as HTTP APIs, one way to accomplish this would be a GitHub actions workflow that starts the Compute application in the testing environment and then executes a shell script which runs cURL commands:

.github/workflows/tests.yaml (excerpt)

jobs:
  curl-test:
    name: Build Compute Edge service, and test with curl
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
      - name: Set up Fastly CLI
        uses: fastly/compute-actions/setup@main
      - name: Install Edge code dependencies
        run: npm install
      - name: Build and start Compute application
        run: fastly compute serve > ./out.txt & (tail -f ./out.txt &) | grep -qF 'Listening on http://127.0.0.1:7676'
      - name: Run tests
        run: ./curl-tests/test.sh
Enter fullscreen mode Exit fullscreen mode

./curl-tests/test.sh

#! /bin/bash

echo "test: returns 200 and the text \"Hello, World!\" for /"
OUTPUT=$(curl -vs "http://127.0.0.1:7676/" 2>&1)
grep -qF "200 OK" <<< "$OUTPUT" || { echo 'status not 200' ; exit 1; }
grep -qF "Hello, World!" <<< "$OUTPUT" || { echo 'unexpected response content' ; exit 1; }

echo "test: returns 200 and json including \"newField\": \"newValue\" for /json"
OUTPUT=$(curl -vs "http://127.0.0.1:7676/json" 2>&1)
grep -qF "200 OK" <<< "$OUTPUT" || { echo 'status not 200' ; exit 1; }
grep -qF "\"newField\":\"newValue\"" <<< "$OUTPUT" || { echo 'unexpected response content' ; exit 1; }
Enter fullscreen mode Exit fullscreen mode

Although we have to perform a little bit of text handling in shell scripts, these tests ensure that changes to the program will continue to emit the expected output.

Tests written in JavaScript

However, many programming languages, platforms, and communities offer better options, like utilizing frameworks designed specifically for writing and executing tests.

We wanted this same functionality for Fastly Compute too, with the ability to write simple tests for a Fastly Compute service like this:

  it('returns 200 and the text "Hello, World!" for /', async () => {
    const resp = await app.fetch('/');
    assert.strictEqual(resp.status, 200);
    assert.ok((await resp.text()).includes('Hello, World!'));
  });
  it('returns 200 and json including "newField": "newValue" for /json', async () => {
    const resp = await app.fetch('/json');
    assert.strictEqual(resp.status, 200);

    const responseJson = await resp.json();
    assert.strictEqual(responseJson['data']?.['newField'], 'newValue');
  });
Enter fullscreen mode Exit fullscreen mode

This is why we built Compute Application Testing for JavaScript, a library designed specifically for this purpose.

NOTE: End-to-end tests only work with the inputs and outputs of a program. Therefore, the tests and the Compute app that they apply to can be written in the same or different languages. This library allows you to specifically use JavaScript to write end-to-end tests against your Compute application.

JavaScript is everywhere and super popular. JavaScript offers great support for making web requests, natively parsing JSON, utilities such as regular expressions, access to data structures such as objects and arrays, and the ability to work with an extensive package library available on npm. Because of these capabilities, it just felt natural to write end-to-end tests using JavaScript. And since we’re just checking the inputs and outputs, we can do this regardless of the language of the Compute application.

Compute Application Testing for JavaScript is a JavaScript library, available on npm as @fastly/compute-testing. Your test application will be running in Node.js, so you’ll want to make a test application separate from your Compute application. There, you can add it as a development dependency:

npm install --save-dev @fastly/compute-testing
Enter fullscreen mode Exit fullscreen mode

Now, let’s take a look at what an individual test would look like.

  it('returns 200 and the text "Hello, World!" for /', async () => {
    const resp = await app.fetch('/');
    assert.strictEqual(resp.status, 200);
    assert.ok((await resp.text()).includes('Hello, World!'));
  });
Enter fullscreen mode Exit fullscreen mode

If you’re familiar with the standard Node.js test runner, or a BDD-style runner such as Jest, this should look familiar to you. The test case has the name returns 200 and the text "Hello, World!" for /. It uses the variable "app" to make a call to the '/' path of the Compute application. It then makes assertions on the status code and the text content of the response.

What is the variable "app", you may be asking? It’s an instance of ComputeApplication, a class that allows you to instantiate your Compute application in the local test environment. The variable represents an instance of the Compute application, and it’s used like this:

import { ComputeApplication } from '@fastly/compute-testing';

describe('Edge app', () => {

  const app = new ComputeApplication();

  before(async () => {
    await app.start({
      // Set 'appRoot' to the directory in which to start the app.  This is usually
      // the directory that contains the 'fastly.toml' file.
      appRoot: path.join(__dirname, '..'),
    });
  });

  after(async() => {
    await app.shutdown();
  });

  // ...tests here

});
Enter fullscreen mode Exit fullscreen mode

When the class is instantiated, it calls out to the Fastly CLI to spawn an instance of your Compute application at appRoot. Once it’s running, your tests will be able to make requests to it—simply call .fetch() on it. It takes the same parameters as the global fetch function, so you can pass a URL object, a string, or a Request object as its first parameter, and set headers as you need—and you will receive back a Response object, which you can test for its status, headers, and contents. And because it’s a Response, you can also use its other useful features, such as built-in JSON deserialization:

  it('returns 200 and json including "newField": "newValue" for /json', async () => {
    const resp = await app.fetch('/json');
    assert.strictEqual(resp.status, 200);

    const responseJson = await resp.json();
    assert.strictEqual(responseJson['data']?.['newField'], 'newValue');
  });
Enter fullscreen mode Exit fullscreen mode

There are plenty of other JavaScript features that are available to you to write expressive tests as well. For example, use Request to send JSON, form data (using FormData), and streaming blobs. And because you get back a Response object, you’re able to access native streaming and JSON parsing, which are useful for checking HTTP responses.

Of course, using Node.js also opens you to all of the packages available on npm: If you need to parse an HTML DOM, you can look to JSDOM. Do you need to work with a JSON Web Token (JWT)? Look no further than jsonwebtoken. In this post we’re using Node.js's test runner along with Node.js's assertions, but do you prefer testing with Jest, Mocha, or Chai? These are all available too. Whatever intent your test needs to express, writing your tests in JavaScript for Node.js will help you do that.

Now, running these tests can be done with a GitHub workflow like this:

.github/workflows/tests.yaml (excerpt)

jobs:
  compute-testing-test:
    name: Test Compute Edge service using compute-testing
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
      - name: Set up Fastly CLI
        uses: fastly/compute-actions/setup@main
      - name: Install Edge code dependencies
        run: npm install
      - name: Install Test code dependencies
        working-directory: ./test
        run: npm install
      - name: Run tests
        working-directory: ./test
        run: npm test
Enter fullscreen mode Exit fullscreen mode

Triggering this workflow results in this neat format that gives us access to test names and some stats:

Image description

If you’d like to take a closer look at the code and tests in this example, they are available on my GitHub at https://github.com/harmony7/compute-testing-demo.

Try it on your own code

Using JavaScript to write tests is now a great alternative to using scripts. Compute Application Testing for JavaScript is available on npm for you to try right away. See how easy it is to build and run tests against your Compute application, and to add them to your CI tests.

NOTE: @fastly/compute-testing is provided as a Fastly Labs product. Visit the Fastly Labs website for terms of use.

For more information and examples, be sure to check out our project page on GitHub.

At Fastly, we strive to provide the tools that give you the power to run more code at the Edge and develop for it with the tools you know and love. We love to hear about it when our users get the most out of these tools. Join us on the Fastly community forum and let us know what you’ve been building!

💖 💪 🙅 🚩
harmony7
Katsuyuki Omuro

Posted on January 29, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related