Skip to main content

Testing against fakesnow

The FAKESNOW location type runs a Snowflake test against fakesnow — a local Snowflake emulator built on DuckDB and SQLGlot — instead of a real Snowflake account. The Depot Test backend starts the container, resolves its connection details, and hands them to the ordinary Snowflake execution path. Your test does not change, no Snowflake account is contacted, and no credentials of any kind are needed: no depot.properties, no private key, no AWS profile.

The obvious use is speed and cost. The less obvious one is that a suite which needs no secrets can run in CI on a fork, or on a laptop that has never been through SSO.

This page is about Depot Test — DepotTest.in(namespace)… with setContent and check steps. Depot Local can run against fakesnow too, and the same emulator brings the same parity boundaries; see Depot Local against fakesnow for that surface.

Running a whole suite against fakesnow​

Set the environment variable. It becomes the default location type, so every dataset that does not name one goes to fakesnow:

DEPOT_TEST_LOCATION_TYPE=FAKESNOW yarn test

Most tests name no location type and simply take the default, which is why one variable moves a whole suite. The resolution order is:

  1. the locationType a test sets explicitly, through options() or per dataset,
  2. DEPOT_TEST_LOCATION_TYPE,
  3. SNOWFLAKE.

A test that names its location type keeps it, whichever one it named — including SNOWFLAKE. Asking for SNOWFLAKE explicitly is as much a choice as asking for AURORA, and the variable exists for the tests that expressed no preference.

One consequence worth knowing: in a multi-dataset test, naming a location type on some datasets and leaving others to the default can produce a job whose datasets span two backends. All datasets in one test must share a backend, so the run fails with a message naming the datasets that disagree. Either name the location type on all of them or on none.

An unrecognised value throws at job-build time rather than being ignored. A typo that silently ran the whole suite against real Snowflake would be a far more expensive mistake than a startup failure.

Opting in one test at a time​

Set the location type the same way you would choose any other:

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

await DepotTest.in(namespace)
.options({ locationType: TestLocationType.FAKESNOW })
.setContent({ data: { "petstore.dogs": dogs } })
.check({ data: [{ scope: { schema: "petstore.AnimalCount" }, values: [{ animal: "dog", count: 2 }] }] })
.run();

Or per dataset, in the multi-dataset form. As with the other location types, all datasets in one test must share a backend — and FAKESNOW counts as its own, so it cannot be mixed with real SNOWFLAKE either. They reach different endpoints, and a job gets one connection.

Naming FAKESNOW on the test needs no environment variable and no credentials: a test whose datasets all resolve to FAKESNOW skips the credentials lookup entirely, so there is nothing to configure and no depot.properties to find.

What works, and what does not​

Everything Depot does over the Snowflake SQL API and JDBC works: schema and view deployment, setContent, transactions, queries, migrations, UDFs, content and count checks. Stages work too — fakesnow serves them from inside its own container, so PUT into an internal stage and seeding a test from a staged file behave as they do against Snowflake.

What does not work is anything that needs a real bucket:

  • Export steps (testing exports) fail fast with an explanation. There is no S3 behind the stage URL to export into.
  • s3:// stage URLs used as actual S3 locations, for the same reason. A schema may still declare an s3:// URL; it is simply never treated as a bucket, and the stage is created inside the container.

This is a deliberate boundary rather than a missing feature: the FAKESNOW path holds no AWS credentials at all, which is much of its value. A test that genuinely exercises export belongs on SNOWFLAKE.

Separately, fakesnow implements no stored procedures, so a CALL step comes back as Catalog Error: Table Function with name ... does not exist. Those tests belong on SNOWFLAKE too.

Snowflake's proprietary HASH values differ as well, since fakesnow computes DuckDB's hash. A test that asserts a recorded Snowflake hash belongs on SNOWFLAKE.

SNOWFLAKE_MOCK_ICEBERG has no fakesnow flavour.

Requirements​

Docker, and nothing else. The backend pulls a pinned image from a public registry and runs a pool of fakesnow instances, one container each. Inside an instance, it isolates concurrent tests from each other by schema prefix in a single database, as it does on real Snowflake.

To pull the images before the tests, use @stage-tech/depot-test/global-setup-docker-fakesnow as the global setup of the test runner, or depot-test-pull-docker-images fakesnow in a CI step; see Pull the Docker images before the tests.

The number of instances defaults to auto, which sizes the pool to the machine: one instance per CPU less one, and no more than the memory available to Docker divided by the memory budget of one instance. To set a fixed count, set DEPOT_TEST_FAKESNOW_INSTANCES to a positive integer. The backend's ops limits do not bound fakesnow jobs; they queue at their emulator.

An instance starts only when the backend routes a test to it. All datasets of one test go to the same instance, and so do all jobs that share a schema prefix. An idle instance stops after 30 seconds (DEPOT_TEST_FAKESNOW_IDLE_DURATION_SECONDS), unless a long-running session still uses it or it holds a kept schema (KEEP_SNOWFLAKE_SCHEMA or SNOWFLAKE_SCHEMA_PREFIX).

Each instance runs under a memory limit, DEPOT_FAKESNOW_MEMORY: a whole number of bytes, or a whole number with a unit k, m or g (binary), such as 2g or 1536m. DuckDB uses about 70% of it, and a query that needs more spills to disk instead of failing. When the variable is unset, the backend picks a budget between 1 GiB and 4 GiB from the memory available to Docker.

The limit is a hard cap: an instance that needs more memory than its limit is killed. The tests on it then fail on a lost connection, not on an out-of-memory error, and docker inspect shows "OOMKilled": true for its container. Set a larger DEPOT_FAKESNOW_MEMORY if that happens.

Like the other backend variables, these three take effect only when the run starts the backend. A run that finds a backend already running uses the settings of that backend.

Each container's host port is assigned by Docker rather than fixed, so several backends can run against one Docker daemon without colliding.

Using a locally-built fakesnow image​

If you are changing fakesnow itself, point the backend at your own build:

DEPOT_FAKESNOW_IMAGE=my-fakesnow:dev DEPOT_TEST_LOCATION_TYPE=FAKESNOW yarn test

Depot needs the Snowflake SQL API v2 (POST /api/v2/statements), which upstream fakesnow does not implement — so the published image carries that support and an arbitrary ghcr.io/tekumara/fakesnow tag will not work. Building and publishing the image happens in the fakesnow repository, not in Depot.

Pointing at a fakesnow you manage yourself​

FAKESNOW exists so you do not have to run anything by hand. If you nonetheless want to — to attach a debugger to the emulator, to keep one instance warm across many runs, or to inspect its state between tests — start fakesnow yourself and use the sql.endpointOverride escape hatch instead of this location type.

Either way, one check does not run: Depot normally compares each view's stored DDL against the text it submitted, and only Snowflake echoes that text verbatim. fakesnow reports the definition after a round trip through SQLGlot — CREATE VIEW for create or replace view, identifiers re-cased, aliases and parens added — so the comparison is skipped against any endpointOverride, managed or not. Your CONTENTS checks are unaffected; they go over the SQL API and compare data.