Applying test-driven development to your database

Applying test-driven development to your database

·

8 min read

Test-driven development (TDD) is a popular software development methodology that aims to reduce defects and improve the ease of maintaining code. In this post, you learn how to apply TDD to write queries and expressions with the Fauna Query Language (FQL). You learn a general pattern for TDD with FQL, then apply that pattern to implement a Split() function that accepts string and delimiter parameters and returns an array of substrings “split” at each occurrence of the delimiter.

Pre-requisites

To follow along with this post you must have access to a Fauna account. You can register for a free Fauna account and benefit from Fauna’s free tier while you learn and build, and you won’t need to provide payment information until you are ready to upgrade.

This post assumes you are familiar enough with Fauna to create a database and client key. If not, please complete the client application quick start and return to this post.

This post also assumes you have Node.js v12 or later in your development environment. This post uses Jest and JavaScript, but you can apply the principles to your preferred test tooling in any supported language.

The source code used in this post is available in Fauna Labs.

TDD overview

TDD begins with writing a single failing test. For example, if you are implementing a Split() function that accepts a string and a delimiter and returns an array of strings “split” at each occurrence of the delimiter, you would test that Split("127.0.0.1", ".") is equal to ["127", "0", "0", "1"]. When you first run your test suite, the test fails, because you have not yet implemented the Split() function.

Next, you implement the functionality of a single “unit” of code, in this case, the Split() function. You run your tests as you make changes, and when the tests pass, you have implemented the code to cover the specific use case that the test specifies.

With passing tests, you can add more tests to cover additional use cases, or you can refactor your existing code for performance or readability improvements. Because your tests are in place and passing, you can make changes with more confidence that your code is correct.

Setting up your environment

Setting up your database

Create a new database in your Fauna account and create a key for that database with the Server role. Save the key to add to your application.

Configuring Jest

  1. Create a new Node.js application in an empty directory and install the Fauna JavaScript driver.

     npm init --yes
     npm install faunadb
    
  2. Add Jest to your application.

     npm install --save-dev jest babel-jest @babel/core @babel/preset-env
    
  3. Edit package.json to add the following “test” and “test:watch” scripts.

     {
       "scripts": {
         "test": "jest",
         "test:watch": "jest --watchAll"
       }
     }
    
  4. Create a file babel.config.js in the root directory of your project with the following content.

     module.exports = {
       presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
     };
    
  5. Create a file jest.config.mjs in the root directory of your project with the following content.

     export default {
       collectCoverage: true,
       coverageDirectory: "coverage",
       coverageProvider: "v8",
       setupFiles: ["<rootDir>/.jest/setEnvVars.js"],
     };
    
  6. Create a file .jest/setEnvVars.js and paste the following code, replacing with the value of your admin key and db.fauna.com with your Region Group endpoint. Add this file to your .gitignore to avoid committing secrets to your repository.

     process.env.FAUNADB_ADMIN_KEY = '<secret_key>';
     process.env.FAUNADB_DOMAIN = 'db.fauna.com';
    
  7. Run npm run test -- --passWithNoTests to check that you have configured your application to use Jest. You should receive a response “No tests found, exiting with code 0.”

npm run test passWithNoTests

Testing partial FQL expressions

You test partial FQL expressions from JavaScript by writing JavaScript functions that generate FQL expressions. You then export these functions so that both your code and your tests can import and use them. This supports the “don’t repeat yourself” or “DRY” principle of software development in your FQL queries.

General pattern

The following five steps form the general pattern for testing partial FQL expressions:

  1. Define the input to your expression as one or more constants.
  2. Define the expected output of your expression as a constant.
  3. Construct your FQL expression.
  4. Evaluate your FQL expression in Fauna and save the actual output.
  5. Compare the actual output to the expected output.

Your test passes when the comparison in the final step is successful. This comparison may be equality, inequality, or any other comparison allowed by your testing framework. In Jest, these comparisons are called matchers; your testing framework may have different terminology.

Preparing your test harness

Make a stub for your implementation by creating a file named fauna/lib/split.js and pasting the following code.

export function Split(str, sep) {
  return undefined;
};

This gives your test an undefined implementation to call, guaranteeing your test fails.

Next, create a file fauna/test/split.test.js in your project. Since you run your tests against an actual Fauna environment, you must import the Fauna JavaScript driver and create a new Fauna client by adding the following code.

import faunadb from 'faunadb';

const faunaClient = new faunadb.Client({
  secret: process.env.FAUNADB_ADMIN_KEY,
  domain: process.env.FAUNADB_DOMAIN || "db.fauna.com",
  port: parseInt(process.env.FAUNADB_PORT) || 443,
  scheme: process.env.FAUNADB_SCHEME || "https",
  checkNewVersion: false,
});

Next, import the Split() function stub that you want to test.

import { Split } from '../lib/split';

Copy and paste the following general framework code into split.test.js.

test('<description>', async () => {
  // 1. Define the input to your expression as one or more constants.
  const input = ...;

  // 2. Define the expected output of your expression as a constant.
  const expected = ...;

  // 3. Construct your FQL expression.
  const expr = ...;

  // 4. Evaluate your FQL expression in Fauna and save the actual output.
  const actual = await faunaClient.query(expr);

  // 5. Compare the actual output to the expected output.
  expect(actual).toEqual(expected);
});

At this point, you have a complete test harness, but your tests will not yet run because of the placeholder values. In the next section, you replace these with real values based on the specified behavior of your code.

Create a single failing test

The following examples specify the expected behavior of your Split() implementation.

  1. Split(ip_address, '.') correctly separates an IPv4 address into four octets.
  2. When delimiter is not found, Split(string, delimiter) returns an array with one element, string.
  3. When the string contains only delimiter, Split(string, delimiter) returns an empty array.
TeststringdelimiterExpected result
1'127.0.0.1''.'['127', '0', '0', '1']
2'Hello, world!''.'['Hello, world!']
3'................''.'[]

To implement the first test, modify the general framework code in fauna/test/split.test.js with the properties from your first specification. Your implementation may vary but should be similar to the following code.

test('Split correctly separates an IPv4 address into four octets', async () => {
  // 1. Define the input to your expression as one or more constants.
  const input = {
    string: '127.0.0.1',
    delimiter: '.',
  };

  // 2. Define the expected output of your expression as a constant.
  const expected = ['127', '0', '0', '1'];

  // 3. Construct your FQL expression.
  const expr = Split(input.string, input.delimiter);

  // 4. Evaluate your FQL expression in Fauna and save the actual output.
  const actual = await faunaClient.query(expr);

  // 5. Compare the actual output to the expected output.
  expect(actual).toEqual(expected);
});

The previous code expects Split() to split the string ‘127.0.0.1' into an array of four strings at each decimal point/period, yielding ['127', ‘0', '0', '1'].

Before moving on to implement Split(), check that your test runs using npm run test. It’s okay if it fails. This is the expected behavior since you haven’t implemented the functionality of Split()!

If your tests do not yet run, check that you have included all parts of the test harness and the test itself. Your complete fauna/test/split.test.js should look as follows.

import faunadb from 'faunadb';

import { Split } from '../lib/split';

const faunaClient = new faunadb.Client({
  secret: process.env.FAUNADB_ADMIN_KEY,
  domain: process.env.FAUNADB_DOMAIN || "db.fauna.com",
  port: parseInt(process.env.FAUNADB_PORT) || 443,
  scheme: process.env.FAUNADB_SCHEME || "https",
  checkNewVersion: false,
});

test('Split correctly separates an IPv4 address into four octets', async () => {
  // 1. Define the input to your expression as one or more constants.
  const input = {
    string: '127.0.0.1',
    delimiter: '.',
  };

  // 2. Define the expected output of your expression as a constant.
  const expected = ['127', '0', '0', '1'];

  // 3. Construct your FQL expression.
  const expr = Split(input.string, input.delimiter);

  // 4. Evaluate your FQL expression in Fauna and save the actual output.
  const actual = await faunaClient.query(expr);

  // 5. Compare the actual output to the expected output.
  expect(actual).toEqual(expected);
});

Implementation

Now that your test harness is set up, run your tests in watch mode.

npm run test:watch

npm run test watch

In watch mode, test tooling watches your source and test files and runs the relevant test suite when changes are detected. You immediately see the results of running the test suite whenever you save your work, which tightens the developer feedback loop and improves your productivity.

With your tests running in watch mode, it’s time to implement the actual Split() function so that your tests pass. Replace the stub you created earlier in fauna/lib/split.js with the following code. This implementation uses the built-in FQL action FindStrRegex to split the parameter str at each occurrence of sep.

import faunadb from 'faunadb';

const q = faunadb.query;
const {
  Concat,
  FindStrRegex,
  Lambda,
  Map,
  Select,
  Var
} = q;

export function Split(str, sep) {
  return Map(
    FindStrRegex(str, Concat(["[^\\", sep, "]+"])),
    Lambda("res", Select(["data"], Var("res")))
  );
};

When you save your file, Jest automatically re-runs the test suite, and this time, it passes. Congratulations, you’ve written your first TDD with FQL!

npm run test watch pass

Review and next steps

In this post, you learned how to apply TDD to write and test FQL expressions. You wrote failing tests for a Split() function that accepts string and delimiter parameters and returns an array of substrings “split” at each occurrence of the delimiter. Finally, you implemented the Split() function while running unit tests in watch mode.

To test your knowledge, try implementing tests for the remaining two specifications. Once you have those tests in place and passing, try to refactor the implementation of Split(). Notice how TDD allows you to safely refactor your code without introducing regressions.

For more tooling and sample code, check out Fauna Labs on GitHub. We can’t wait to see what you test and build!