Skip to main content

Testing

Depot provides a test framework with the capability of verifying that data transformation and data flow are correct. Tests are written as part of the package repository, runnable under any standard test runner (Jest, Vitest, Node test).

Setup

Add @stage-tech/depot-test to your npm dependencies and configure depot.properties with Snowflake credentials. For in-depth documentation go to the README.MD file in the packages/depot-test repository folder of stage-depot.

tip

To run tests in GitHub Actions or AWS CodeBuild you will need to set up secrets if it’s not done yet.

info

Check you have created an empty Snowflake Database that matches the name of the sql.database value in your depot.properties file before you run your tests.

The DepotTest object

The main entry point of the framework is the DepotTest class. It provides a single entry point, the in(namespace) method.

You can then access a set of chainable methods to set up your test scenario, move data around, and check results:

setupdata motioncontrol
in setContent withAdditionalSchemas named optionsmerge replace upsert insert patch delete refresh call transaction legacySteps exportcheck checkExports run

These methods only accumulate work items for deferred execution, with the exception of run() which takes all accumulated work items and executes them in a single back-end operation.

Once you've set up your test scenario, you can execute in one of three ways:

  1. Execute the test in a test runner (Jest, Vitest, Node test). Call .run() to await the result; failures throw and surface as test failures.
example.test.ts
import * as path from "node:path";

import { DepotTest } from "@stage-tech/depot-test";

import * as pkg from "..";

describe("some clever test name", () => {
it("some clever scenario name", async () => {
await DepotTest.in(pkg.mergedSchemas)
.setContent(path.join(__dirname, 'data'))
.check({
expectedUri: path.join(__dirname, 'expected'),
actual: ['my.Result']
})
.run();
});
});
  1. Execute the test asynchronously at your own pace
example-anywhere.ts
import * as path from "node:path";

import { MergedSchemas } from "@stage-tech/depot-schema";

async function checkSomeScenario(namespace: MergedSchemas): Promise<boolean> {

try {
const scenario = DepotTest.in(namespace)
.setData(path.join(__dirname, 'data'))
.check({
expectedUri: path.join(__dirname, 'expected'),
actual: ['my.Result']
});

await scenario.run();
return true;
} catch (e) {
console.warn(e);
return false;
}
}
  1. Set up the test as multiple given/when/then bits
example-anywhere.ts
import * as path from "node:path";

import { MergedSchemas } from "@stage-tech/depot-schema";


const dt = DepotTest
.in(namespace)
.options({ sessionTimeoutMilliseconds: 90_000 });

function givenXXX() {
dt.setData(path.join(__dirname, 'data'));
}

function whenXXX() {
dt.transaction({
source: "my.Source",
target: "my.TransformTarget"
})
}

async function thenXXX() {
dt.check({
expectedUri: path.join(__dirname, 'expected'),
actual: ['my.Result']
});

await dt.run();
}

async function thenYYY() {
dt.check({
expectedUri: path.join(__dirname, 'expected'),
actual: ['my.OtherResult']
});

await dt.run();
}

In this example, we use the "long-running sessions" mode, where sessions linger within the back-end after a .run(). In each then...() step, we invoke .run() to execute the test steps declared so far, and immediately check the results. We chose here to not do this immediately in the .then() steps, in order to accumulate multiple checks before executing the test (but we could. It is a tradeoff between performance and precision of error reporting).

Seeded views

The framework supports loading data into non-materialized views. This is done by adding json file to dataURI for a step with qualified view name as file name. It will create View as a table and will load provided data into it.

caution

Use with caution in multistep tests

Programmatic data input/output

The normal operation of DepotTest is to rely on static files (CSV, JSON, etc.) to provide input data and to compare the results of a test with a set of static files containing the expected results.

In some circumstances, this isn't as convenient as having the ability to programmatically provide input data and to obtain output data in order to perform assertions outside DepotTest.

This mode of operation is supported, and described in advanced: programmatic data input/output.

Skipping expressions (emulating replication/rollup)

If your tests are to emulate e.g. views built from replicated schemas with expressions, it is recommended to pass skipExpressions: true to test steps, this is the behaviour used by the real replication process, and will avoid UnsupportedOperationExceptions being thrown when expressions are too complex to apply using SQL (and is generally better to match the real behaviour).

const scenario = DepotTest.in()
.setContent({
dataUri: "test/clever/name/data",
skipExpressions: true
})
.

In legacy mode, this can be passed in the long-hand form of TestSteps or the shorthand form as follows:

const longSteps: TestStep[] = [
{
name: "seed data",
operation: Operation.UPSERT,
dataUri: "test/clever/name/data",
skipExpressions: true
}
];

const shortSteps: TestStep[] = [
objectTestStep("test/clever/name/data", "my step", "my.schema.Name", { skipExpressions: true })
];