Applying test-driven development to your database
Tags: tdd, testing, database, fauna
Published: 2022-02-03 16:31:33

[Test-driven development](https://en.wikipedia.org/wiki/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](https://dashboard.fauna.com/accounts/register?utm_source=Devto&utm_medium=referral&utm_campaign=rob_tdd) and benefit from [Fauna’s free tier](https://fauna.com/pricing?utm_source=Devto&utm_medium=referral&utm_campaign=rob_tdd) 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](https://docs.fauna.com/fauna/current/learn/quick_start/client_quick_start?lang=javascript&utm_source=Devto&utm_medium=referral&utm_campaign=rob_tdd) and return to this post.

This post also assumes you have [Node.js](https://nodejs.org/) v12 or later in your development environment. This post uses [Jest](https://jestjs.io/) and JavaScript, but you can apply the principles to your preferred test tooling in any [supported language](https://docs.fauna.com/fauna/current/drivers/?utm_source=Devto&utm_medium=referral&utm_campaign=rob_tdd).

The source code used in this post is [available in Fauna Labs](https://github.com/fauna-labs/tdd-fql-jest).

## 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](https://www.npmjs.com/package/faunadb).

    ```
    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*](https://github.com/fauna-labs/tdd-fql-jest/blob/b956595b0f3ab6e09f66b6000fd878f88af69236/package.json#L6-L9) to add the following *“test”* and *“test:watch”* scripts.

    ```json
    {
      "scripts": {
        "test": "jest",
        "test:watch": "jest --watchAll"
      }
    }
    ```

4. Create a file [*babel.config.js*](https://github.com/fauna-labs/tdd-fql-jest/blob/main/babel.config.js) in the root directory of your project with the following content.

    ```javascript
    module.exports = {
      presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
    };
    ```

5. Create a file [*jest.config.mjs*](https://github.com/fauna-labs/tdd-fql-jest/blob/main/jest.config.mjs) in the root directory of your project with the following content.

    ```javascript
    export default {
      collectCoverage: true,
      coverageDirectory: "coverage",
      coverageProvider: "v8",
      setupFiles: ["<rootDir>/.jest/setEnvVars.js"],
    };
    ```

6. Create a file [*.jest/setEnvVars.js*](https://github.com/fauna-labs/tdd-fql-jest/blob/main/.jest/setEnvVars.example.js) and paste the following code, replacing *<secret_key>* with the value of your admin key and *db.fauna.com* with your [Region Group endpoint](https://docs.fauna.com/fauna/current/learn/understanding/region_groups). Add this file to your *.gitignore* to avoid committing secrets to your repository.

    ```javascript
    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**.”

![Output of npm run test -- --passWithNoTests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/es3zmbni19k5ng5ntdnr.png)

## Testing partial FQL expressions

You test partial FQL expressions from JavaScript by writing JavaScript functions that generate FQL expressions. You then [export](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export) these functions so that both your code and your tests can import and use them. This supports the “[don’t repeat yourself](https://en.wikipedia.org/wiki/Don%27t_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](https://archive.jestjs.io/docs/en/22.x/expect); 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.

```javascript
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](https://npmjs.com/package/faunadb) and create a new Fauna client by adding the following code.

```javascript
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.

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

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

```javascript
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.

| Test | string | delimiter | Expected 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.

```javascript
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.

```javascript
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
```

![Output of npm run test:watch with failing tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z0oq9utgo81tr1ffe310.png)

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`](https://docs.fauna.com/fauna/current/api/fql/functions/findstrregex?lang=javascript&utm_source=Devto&utm_medium=referral&utm_campaign=rob_tdd) to split the parameter `str` at each occurrence of `sep`.

```javascript
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!

![output of npm run test:watch with passing tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4u1yn2k8t8e8wcsf60bi.png)

## 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](https://github.com/fauna-labs) on GitHub. We can’t wait to see what you test and build!