Unit Testing

June 7, 2026 · View on GitHub

You can run VCL unit test on our interpreter to make sure the subroutine works as you expected.

Note

Some variables are limited because to interpreter runs locally, so the variables that are used in your production VCL may have unexpected values, and it may affect to testing. Please see simulator documentation about limitations before.

Usage

falco test -h
=========================================================
    ____        __
   / __/______ / /_____ ____
  / /_ / __  // //  __// __ \
 / __// /_/ // // /__ / /_/ /
/_/   \____//_/ \___/ \____/  Fastly VCL developer tool

=========================================================
Usage:
    falco test [flags]

Flags:
    -I, --include_path : Add include path
    -h, --help         : Show this help
    -r, --remote       : Connect with Fastly API
    -json              : Output results as JSON
    -o, --override     : Override tentative variable value (e.g., -o "req.protocol=https")
    -request           : Override request config
    --max_backends     : Override max backends limitation
    --max_acls         : Override max acl limitation
    --watch            : Watch VCL file changes and run test
    --coverage         : Report code coverage

Local testing example:
    falco test -I . -I ./tests /path/to/vcl/main.vcl

Configuration

You can override default configurations via .falco.yml configuration file or cli arguments. See configuration documentation in detail.

You can run testing as following:

falco test -I . /path/to/your/default.vcl

Overriding Tentative Variables

You can override tentative variable values via the -o (or --override) flag or .falco.yml configuration file. This is useful for simulating different conditions in your tests without modifying the VCL code.

Via CLI flag:

# Override a single variable
falco test -I . -o "req.protocol=https" /path/to/your/default.vcl

# Override multiple variables
falco test -I . -o "req.protocol=https" -o "server.region=ASIA" /path/to/your/default.vcl

Via .falco.yml configuration:

testing:
  overrides:
    req.protocol: https
    server.region: ASIA
    client.geo.country_code: JP

You can also use the testing.inject_variable() function within your test VCL to override variables per test case.

How to write test VCL

When you run the testing command, falco finds test files that match the glob syntax of *.test.vcl in the include_paths, or you can override this by providing -f,--filter option to filter test target files you want.

Normally you can put the testing file in the same place as main VCL:

tree .

.
└── vcl
   ├── default.test.vcl  <= testing file
   └── default.vcl       <= main VCL

falco test ./vcl/default.vcl

Or, you may place testing files at a different directory, can place them and set more include_paths options on CLI:

tree .

.
├── vcl
 └── default.vcl
└── vcl_tests
   └── default.test.vcl

falco test -I vcl_tests ./vcl/default.vcl

falco finds default.test.vcl as testing file for both case.

Incremental Testing

If you provide --watch option for testing command, test runner watches source and testing VCL file change and run tests. For example,

falco test -I vcl_tests ./vcl/default.vcl --watch

Then falco observes vcl_tests/* and vcl/* file changes and run test incrementally.

Report Code Coverage

If you provide --coverage option for testing command, falco collects and calculates code coverage after the test. For example,

falco test -I vcl_tests ./vcl/default.vcl --coverage

After testing finished, falco will display the coverage report.

CleanShot 2025-02-24 at 18 31 29@2x

Note

To collect the code coverage, falco needs instrumenting to your VCL code by transforming the AST. This process is heavy so coverage mode is disabled when incremental testing is active.

Testing Subroutine

Unit testing file can be written as VCL subroutine, example is the following:

// default.test.vcl

// @scope: recv
// @suite: Foo request header should contains "hoge"
sub test_vcl_recv {
  set req.http.Foo = "bar";
  testing.call_subroutine("vcl_recv");

  assert.equal(req.backend, httpbin_org);
  assert.contains(req.http.Foo, "hoge");
}

// @scope: deliver
// @suite: X-Custom-Header response header should contains "hoge"
sub test_vcl_deliver {
  set req.http.Foo = "bar";
  testing.call_subroutine("vcl_deliver");
  assert.contains(resp.http.X-Custom-Header, "hoge");
}

You can see many interesting syntaxes, The test case is controlled with annotation and assertion functions.

Grouped Testing

falco supports special syntax of describe, before_[scope], and after_[scope] - jest like syntax - only for the testing. Here is the example of grouped testing:

// Grouping test
describe grouped_tests {

    // run before recv scoped subroutine
    before_recv {
        // you can use variables that is enable to access in RECV scope
        set req.http.BeforeRecv = "1";
    }

    sub test_recv {
        // ensure http header which is injected via hook
        assert.equal(req.http.BeforeRecv, "1");
        // Do unit testing for RECV scope
    }

    ...
}

Note

Testing subroutines are stateful through the grouped testing. A interpreter only be initialized for the group, the same interpreter will be used for each testing subroutine. It is useful for testing across scopes but this behavior may be different from the jest one.

Scope Recognition

falco recognizes @scope annotation for execution scope.

@scope: deliver means the testing should run on DELIVER scope. You can specify multiply by separating commas like @scope: hit,pass.

// @scope: recv,fetch,pass
sub some_test_suite {
    ...
}

Suite Name

You can specify the test suite name with @suite annotation value. Otherwise, the suite name will be set as the subroutine name.

// @suite: test suite name here
sub some_test_suite {
    ...
}

Skipping Test

You can skip test case by a couple of ways.

Adding @skip annotation comment

When you write @skip annotation comment in leading comment of testing subroutine, falco skips this testing subroutine.

// @skip
sub some_test_suite {
    ...
}

Note

Testing subroutines which is applied @skip annotation comment are always skipped.

Specify @tag annotation and match against -t,--tag cli option

When you write @tag: [tag1],[tag2],... annotation comment in leading comment of testing subroutine, falco evaluates tag matching and run if matched..

// @tag: prod
sub some_test_suite {
    ...
}

And you can provide matcher tags via -t,--tag option:

falco test -t prod /path/to/vcl/default.vcl

Then, falco evaluates whether prod tag matches against @tag values (on the above case, test will be run). And the @tag annotation could expect inverse flag like !prod:

// @tag: !prod
sub some_test_suite {
    ...
}

Then this test suite will be run if prod tag is NOT provided. We describes the falco treats and evaluates tag specification and providing cli option as the following table:

Tag SpecificationTag CLI OptionRun Test
prodN/ANO
prodprodYES
proddevNO
!prodN/AYES
!prodprodNO
!proddevYES
N/AN/AYES
N/AprodYES
N/AdevYES

Important

Above table describes significant thing that if you specify some tag annotation, the test suite only runs when some tag option is provided.

Testing preparation

When the test suite runs on a specific scope like FETCH, you need to set up a pre-condition to run target VCL. It means you need to set up variables which is needed until the directive moves to FETCH, setting up before calling testing.call_subroutine.

This is because the test target subroutine (in this case, vcl_fetch) is called independently, so that the other lifecycle subroutines like vcl_recv and vcl_pass are not called.

For example:

// @scope: fetch
// @suite: all needed variables are satisfied
sub pre_condition_test {

    // You need to set up some variables before calling "vcl_fetch".

    // Typically set query string might be set on vcl_recv.
    // But vcl_recv is not called in testing process, so you need to set in here.
    set req.url = querystring.add(req.url, "foo", "bar")

    // Override backend response before calling "vcl_fetch" for test case of 500 status code.
    // It means there is a `hook` point before calling `vcl_fetch`.
    set beresp.status = 500;

    // call target subroutine
    testing.call_subroutine("vcl_fetch");

    // Some assertions below
    assert.equal(beresp.ttl, 1s) // TTL should be set 1s when status code is 500
}

Testing Variables and Functions

On running tests, falco injects special runtime functions and variables to assert.

We describe them following table and examples:

NameTypeDescription
testing.stateSTRINGReturn state which is called return statement in a subroutine
testing.synthetic_bodySTRINGThe body generated via a call to synthetic or synthetic.base64
testing.origin_host_headerSTRINGThe value of Host header that will send to an origin
testing.call_subroutineFUNCTIONCall subroutine which is defined in main VCL; accepts optional args and returns a value for functional subroutines
testing.fixed_timeFUNCTIONUse fixed time whole the test suite
testing.override_hostFUNCTIONOverride request host with provided argument in the test case
testing.inject_variableFUNCTIONInject variable that returns tentative value
testing.inspectFUNCTIONInspect predefined variables for any scopes
testing.table_setFUNCTIONInject value for key to main VCL table
testing.table_mergeFUNCTIONMerge values from testing VCL table to main VCL table
testing.mockFUNCTIONMock the subroutine with specified subroutine in the testing VCL
testing.restore_mockFUNCTIONRestore specific mocked subroutine
testing.restore_all_mocksFUNCTIONRestore all mocked subroutines
testing.get_envFUNCTIONGet environment variable value on running machine
testing.fixed_access_rateFUNCTIONSet fixed access rate value
testing.set_backend_healthFUNCTIONSet health status of backend
assertFUNCTIONAssert provided expression should be true
assert.trueFUNCTIONAssert actual value should be true
assert.falseFUNCTIONAssert actual value should be false
assert.is_jsonFUNCTIONAssert actual string should be valid JSON
assert.is_notsetFUNCTIONAssert actual value should be NotSet
assert.equalFUNCTIONAssert actual value should be equal to expected value (alias of assert.strict_equal)
assert.not_equalFUNCTIONAssert actual value should not be equal to expected value (alias of assert.not_strict_equal)
assert.strict_equalFUNCTIONAssert actual value should be equal to expected value strictly
assert.not_strict_equalFUNCTIONAssert actual value should not be equal to expected value strictly
assert.equal_foldFUNCTIONAssert actual value should be equal to with case insensitive
assert.matchFUNCTIONAssert actual string should be matched against expected regular expression
assert.not_matchFUNCTIONAssert actual string should not be matches against expected regular expression
assert.containsFUNCTIONAssert actual string should contain the expected string
assert.not_containsFUNCTIONAssert actual string should not contain the expected string
assert.starts_withFUNCTIONAssert actual string should start with expected string
assert.ends_withFUNCTIONAssert actual string should end with expected string
assert.subroutine_calledFUNCTIONAssert subroutine has called in testing subroutine (with times)
assert.not_subroutine_calledFUNCTIONAssert subroutine has not called in testing subroutine
assert.restartFUNCTIONAssert restart statement has called
assert.not_restartFUNCTIONAssert restart statement has not been called
assert.stateFUNCTIONAssert after state is expected one
assert.not_stateFUNCTIONAssert after state is not expected one
assert.errorFUNCTIONAssert error status code (and response) if error statement has called
assert.not_errorFUNCTIONAssert runtime state will not move to error status

testing.synthetic_body STRING

Returns the response body as set by a call to synthetic or synthetic.base64. Only valid in the error scope.

// @scope: error
sub generate_response {
    synthetic "No dice.";
}

// @scope: error
sub test_vcl {
    testing.call_subroutine("generate_response");
    assert.equal(testing.synthetic_body, "No dice.");
}

testing.origin_host_header STRING

Returns the Host header value that will send to the origin. This value is calculated from backend configuration so you need to choose the backend before accessing this variable.

backend example {
    .host = "example.com";
    .port = "443";
    .always_use_host_header = true;
}

// @scope: recv
sub test_vcl {
    // Choose backend before calling
    set req.backend = example;
    assert.equal(testing.origin_host_header, "example.com");
}

testing.call_subroutine(STRING subroutine [, ...args])

Call subroutine that is defined at main VCL and included modules. This function can also call Fastly reserved subroutine like vcl_recv for testing but ensure the call corresponds to the expected scope.

For functional subroutines (subroutines that accept typed parameters and return a value), pass the arguments after the subroutine name and capture the return value with set.

Calling a scoped subroutine (no extra args):

// @scope: recv
sub test_vcl {
    // call vcl_recv Fastly reserved subroutine in RECV scope
    testing.call_subroutine("vcl_recv");
}

Calling a functional BOOL subroutine with arguments:

// url_path_is_either returns true when the request path matches
// either of the two provided path strings.
// @scope: recv
sub url_path_is_either(STRING var.path1, STRING var.path2) BOOL {
    if (req.url.path == var.path1 || req.url.path == var.path2) {
        return true;
    }
    return false;
}

// @scope: recv
// @suite: functional BOOL subroutine returns true for first matching path
sub test_path_matches_first {
    set req.url = "/path1/foo";
    declare local var.result BOOL;
    set var.result = testing.call_subroutine("url_path_is_either", "/path1/foo", "/path2/bar");
    assert.true(var.result);
}

Calling a functional STRING subroutine with arguments:

// classify_path returns "matched" when the request path equals
// var.expected, otherwise "unmatched".
// @scope: recv
sub classify_path(STRING var.expected) STRING {
    if (req.url.path == var.expected) {
        return "matched";
    }
    return "unmatched";
}

// @scope: recv
// @suite: functional STRING subroutine returns matched for known path
sub test_classify_matched {
    set req.url = "/api/v1";
    declare local var.label STRING;
    set var.label = testing.call_subroutine("classify_path", "/api/v1");
    assert.equal(var.label, "matched");
}

testing.fixed_time(INTEGER|TIME|STRING time)

Use fixed time in the current test case. After this function is called, now and now.sec always return the fixed time value. so it is useful for time-related tests, for example, checking session cookie is live or not.

The argument can accept some types:

  • INTEGER: unix time seconds
  • TIME: VCL time like std.integer2time() return value
  • STRING: YYYY-mm-dd HH:MM:SS formatted string, human readable
// @scope: recv
sub test_vcl {
    // Accepts INTEGER of unix time seconds
    testing.fixed_time(1694159940);

    // Accepts TIME that is made from VCL function
    testing.fixed_time(std.integer2time(1694159940));

    // Accepts STRING that has acceptable format
    testing.fixed_time("2023-09-08 16:59:00");

    // call vcl_recv Fastly reserved subroutine in RECV scope
    testing.call_subroutine("vcl_recv");

    // some time related assertions here
    assert.true(req.http.Is-Session-Expired)
}

testing.override_host(STRING host)

Use fixed Host header in the current test case. On the interpreter, the Host header value is always localhost and it inconvenient for the origin testing. Then calling this function use a fixed Host header.

// @scope: recv
sub test_vcl {
    // Use fixed host header
    testing.override_host("example.com");

    // call vcl_recv Fastly reserved subroutine in RECV scope
    testing.call_subroutine("vcl_recv");

    // some time related assertions here
    assert.true(resp.status == 200);
}

testing.inspect(STRING var_name)

Inspect specific variable. This function has special permission that all variable value can inspect.

// @scope: recv
sub test_vcl {
    // call vcl_recv Fastly reserved subroutine in RECV scope,
    // will call error statement in this subroutine.
    testing.call_subroutine("vcl_recv");

    // Typically obj.status could not access in recv scope,
    // but can inspect via this function.
    assert.equal(testing.inspect("obj.status"), 400);
}

testing.inject_variable(STRING var_name, ANY value)

Inject variable that returns tentative value.

Note

This function could override tentative values which are written in https://github.com/ysugimoto/falco/blob/main/docs/variables.md, typical variables could not be overridden.

// @scope: recv
sub test_vcl {
    // Override server.region variable value
    testing.inject_variable("server.region", "Asia");
    testing.call_subroutine("vcl_recv");

    assert.equal(req.http.Region, "Asia");
}

You can also inject req.body and req.body.base64 to test subroutines that depend on the request body. Since req.body is read-only in Fastly production, set req.body is not supported — use testing.inject_variable instead.

// @scope: recv
sub test_vcl {
    // Inject request body for testing
    testing.inject_variable("req.body", "bodytext");
    testing.call_subroutine("vcl_recv");

    assert.equal(req.body, "bodytext");
}

testing.table_set(ID table, STRING key, STRING value)

Inject value for key to main VCL table.

// @scope: recv
sub test_vcl {
    // Inject table value
    testing.table_set(example_dict, "foo", "bar");

    // call vcl_recv Fastly reserved subroutine in RECV scope,
    // will call error statement in this subroutine.
    testing.call_subroutine("vcl_recv");

    // Assert injected value
    assert.equal(table.lookup(example_dict, "foo", ""), "bar");
}

testing.table_merge(ID base, ID merge)

Merge values from testing VCL table to main VCL table.


table merge_dict {
    "foo": "bar",
}

// @scope: recv
sub test_vcl {
    // Merge table value
    testing.table_merge(example_dict, merge_dict);

    // call vcl_recv Fastly reserved subroutine in RECV scope,
    // will call error statement in this subroutine.
    testing.call_subroutine("vcl_recv");

    // Assert injected value
    assert.equal(table.lookup(example_dict, "foo", ""), "bar");
}

testing.mock(STRING from, STRING to)

Mock the subroutine with testing subroutine.

Note

You cannot mock Fastly reserved (lifecycle) subroutine that starts with vcl_ like vcl_recv, vcl_fetch, etc. But you can mock the functional subroutine that returns some value.


sub mock_add_header {
    set req.http.Mocked = "1";
}

// @scope: recv
sub test_vcl {
    // Mock the subroutine
    testing.mock("add_header", "mock_add_header");

    // vcl_recv has a dependency that calls "add_header" subroutine inside.
    testing.call_subroutine("vcl_recv");

    // Assert mocked subroutine result
    assert.equal(req.http.Mocked, "1");
}

testing.restore_mock(STRING from)

Restore mocked subroutine to the original. Normally This function is used inside describe grouped testing hooks.


sub mock_add_header {
    set req.http.Mocked = "1";
}

describe add_header_mock {

    before_recv {
        // Mock subroutine
        testing.mock("add_header", "mock_add_header");
    }

    after_recv {
        // Restore mock
        testing.restore_mock("add_header");
    }

    // @scope: recv
    sub test_vcl {
        // Mock the subroutine
        testing.mock("add_header", "mock_add_header");

        // vcl_recv has a dependency that calls "add_header" subroutine inside.
        testing.call_subroutine("vcl_recv");

        // Assert mocked subroutine result
        assert.equal(req.http.Mocked, "1");
    }

    // @scope: fetch
    sub test_fetch {
        // This subroutine no longer uses mocked subroutine
        ...
    }
}

testing.restore_all_mocks()

Restore all mocked subroutines. Normally This function is used inside describe grouped testing hooks.


sub mock_add_header {
    set req.http.Mocked = "1";
}

describe add_header_mock {

    before_recv {
        // Mock subroutine
        testing.mock("add_header", "mock_add_header");
    }

    after_recv {
        // Restore all mocks
        testing.restore_all_mocks();
    }

    // @scope: recv
    sub test_vcl {
        // Mock the subroutine
        testing.mock("add_header", "mock_add_header");

        // vcl_recv has a dependency that calls "add_header" subroutine inside.
        testing.call_subroutine("vcl_recv");

        // Assert mocked subroutine result
        assert.equal(req.http.Mocked, "1");
    }

    // @scope: fetch
    sub test_fetch {
        // This subroutine no longer uses mocked subroutine
        ...
    }
}

STRING testing.get_env(STRING name)

Get environment value from name. This is only enabled on the testing environment and it will be useful for switching test to environment (dev,prod) differences.

sub test_vcl {
    if (testing.get_env("IS_DEV")) {
        // skip this test on development environment
        return;
    }
    ...do some assertions
}

testing.fixed_access_rate(FLOAT|INTEGER rate)

Set fixed access rate. This function affects to ratelimit related values and functions like:

  • ratecounter.rc.bucket.10s
  • ratecounter.rc.bucket.20s
  • ratecounter.rc.bucket.30s
  • ratecounter.rc.bucket.40s
  • ratecounter.rc.bucket.50s
  • ratecounter.rc.bucket.60s
  • ratecounter.rc.bucket.60s
  • ratecounter.rc.rate.1s
  • ratecounter.rc.rate.10s
  • ratecounter.rc.rate.60s
  • ratelimit.check_rate()
  • ratelimit.check_rates()
  • ratelimit.ratecounter_increment()
ratecounter rc {}
penaltybox pb {}

sub test_vcl {
    declare local var.exceeded BOOL;
    set var.exceeded = false;

    // set the fixed rate
    testing.fixed_access_rate(100.0);

    if (ratelimit.check_rate(client.ip, rc, 1, 10, 100, pb, 10s)) {
        set var.exceeded  = true;
    }

    assert.true(var.exceeded);
}

testing.set_backend_health(BACKEND backend, BOOL healthy)

Sets the health status of a backend for testing purposes.

Parameters:

  • backend - The backend to modify (e.g., backend1)
  • healthy - Boolean value: true for healthy, false for unhealthy

Example:

// Test backend health check variable
// @scope: recv
// @suite: Test unhealthy backend
sub test_backend_health_status {
  // Initially all backends are healthy
  assert.true(backend.backend1.healthy);
  assert.true(backend.backend2.healthy);
  assert.true(backend.backend3.healthy);

  // Mark backend1 as unhealthy
  testing.set_backend_health(backend1, false);

  // Now backend1 should be unhealthy
  assert.false(backend.backend1.healthy);
  assert.true(backend.backend2.healthy);
  assert.true(backend.backend3.healthy);
}

assert(ANY expr [, STRING message])

Assert provided expression should be truthy. Note that the expression result must be BOOL or STRING type to evaluate value is truthy. Otherwise, the assertion failed with TypeMismatch.

sub test_vcl {
    declare local var.testing STRING;

    set var.Testing = "foo";
    set req.http Foo = "foo";

    // Pass because expression to be true
    assert(req.http.Foo == var.testing);

    // Pass because expression is truthy
    assert(req.http.Foo);

    // Fail because expression to be false
    assert(req.http.Foo == "bar");
}

assert.true(ANY actual [, STRING message])

Assert actual value should be true.

sub test_vcl {
    declare local var.testing BOOL;

    set var.testing = true;

    // Pass because value is true
    assert.true(var.testing);

    // Pass because expression value is  true
    assert.true(var.testing == true);

    // Fail because expression value is not BOOL true
    assert.true(req.http.Foo);
}

assert.false(ANY actual [, STRING message])

Assert actual value should be false.

sub test_vcl {
    declare local var.testing BOOL;

    // Pass because value is false
    assert.false(var.testing);

    // Pass because expression value is  false
    assert.false(var.testing != true);

    // Fail because expression value is not BOOL false
    assert.false(req.http.Foo);
}

assert.is_json(STRING actual [, STRING message])

Assert actual string should be valid JSON.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "[1,2,3]";

    // Pass because value contains valid JSON array.
    assert.is_json(var.testing);

    set var.testing = "[1,2,3,]";

    // Fail because value contains array with trailing comma.
    assert.is_json(var.testing);
}

assert.is_notset(ANY actual [, STRING message])

Assert actual value should be NotSet.

sub test_vcl {
    declare local var.testing STRING;

    // Pass because value is NotSet
    assert.is_notset(var.testing);

    set var.testing = "";

    // Fail because value is empty, not NotSet
    assert.is_notset(var.testing);

    // Pass because value is NotSet
    assert.is_notset(req.http.Foo);
}

assert.strict_equal(ANY actual, ANY expect [, STRING message])

Assert actual value should be equal to the expected value. Note that falco asserts strict type equality so both value types must be equal too.

sub test_vcl {
    declare local var.testing STRING;
    declare local var.testing2 INTEGER;

    set var.testing = "foo";
    set var.testing2 = 10;

    // Pass because value is equal
    assert.strict_equal(var.testing, "foo");

    // Fail because value is not equal
    assert.strict_equal(var.testing, "bar");

    // Fail because value type is not equal
    assert.strict_equal(var.testing, var.testing2);
}

assert.not_strict_equal(ANY actual, ANY expect [, STRING message])

Assert actual value should NOT be equal to the expected value.

sub test_vcl {
    declare local var.testing STRING;
    declare local var.testing2 INTEGER;

    set var.testing = "foo";
    set var.testing2 = 10;

    // Fail because value is equal
    assert.not_strict_equal(var.testing, "foo");

    // Pass because value is not equal
    assert.not_strict_equal(var.testing, "bar");

    // Fail because value type is not equal
    assert.not_strict_equal(var.testing, var.testing2);
}

assert.equal_fold(STRING actual, STRING expect [, STRING message])

Assert actual value should be equal to the expected value as case insensitive.

sub test_vcl {
    declare local var.testing STRING;
    declare local var.testing2 INTEGER;

    set var.testing = "foo";

    // Pass because value is equal with case insensitive
    assert.strict_equal(var.testing, "Foo");
}

assert.equal(ANY actual, ANY expect [, STRING message])

Alias of assert.strict_equal.


assert.not_equal(ANY actual, ANY expect [, STRING message])

Alias of assert.not_strict_equal.


assert.match(STRING actual, STRING expect [, STRING message])

Assert actual string should be matched against expected regular expression.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value matches regular expression
    assert.match(var.testing, ".+bar.+");

    // Fail because value does not match regular expression
    assert.match(var.testing, "bar");

    // Fail because value type is not a string (default client.ip is 192.0.2.1)
    assert.match(client.ip, "10");
}

assert.not_match(STRING actual, STRING expect [, STRING message])

Assert actual string should NOT be matched against expected regular expression.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value does not match regular expression
    assert.not_match(var.testing, ".+other.+");

    // Fail because value matches regular expression
    assert.not_match(var.testing, "^foo");
}

assert.contains(STRING actual, STRING expect [, STRING message])

Assert actual string should be contained in expected string.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value contains "baz"
    assert.contains(var.testing, "baz");

    // Fail because value does not contain "other"
    assert.contains(var.testing, "other");
}

assert.not_contains(STRING actual, STRING expect [, STRING message])

Assert actual string should NOT be contained in expected string.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value does not contain "other"
    assert.not_contains(var.testing, "other");

    // Fail because value contains "baz"
    assert.not_contains(var.testing, "baz");
}

assert.starts_with(STRING actual, STRING expect [, STRING message])

Assert the actual string should start with the expected string.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value starts with "foo"
    assert.starts_with(var.testing, "foo");

    // Fail because value does not start with "bar"
    assert.starts_with(var.testing, "bar");
}

assert.ends_with(STRING actual, STRING expect [, STRING message])

Assert actual string should end with the expected string.

sub test_vcl {
    declare local var.testing STRING;

    set var.testing = "foobarbaz";

    // Pass because value ends with "baz"
    assert.ends_with(var.testing, "baz");

    // Fail because value does not end with "bar"
    assert.ends_with(var.testing, "bar");
}

assert.subroutine_called(STRING name [, INTEGER times, STRING message])

Assert subroutine has called in testing subroutine (with times).

sub test_vcl {
    // Like "auth_recv" subroutine will be called in vcl_recv
    testing.call_subroutine("vcl_recv");

    // Assert "auth_recv" subroutine has called in processing vcl_recv
    assert.subroutine_called("auth_recv");

    // Additionally, "auth_recv" called only once
    assert.subroutine_called("auth_recv", 1);
}

assert.not_subroutine_called(STRING name [, STRING message])

Assert subroutine has not called in testing subroutine (with times).

sub test_vcl {
    // Like "auth_recv" subroutine will be called in vcl_recv
    testing.call_subroutine("vcl_recv");

    // Assert "flag_recv" subroutine has not called in processing vcl_recv
    assert.not_subroutine_called("flag_recv");
}

assert.restart([, STRING message])

Assert restart statement has called.

sub test_vcl {
    // restart statement will be called on some request condition
    testing.call_subroutine("vcl_recv");

    // Assert restart statement has called
    assert.restart();
}

assert.not_restart([, STRING message])

Assert restart statement has NOT been called.

sub test_vcl {
    // vcl_recv will process normally without calling restart
    testing.call_subroutine("vcl_recv");

    // Assert restart statement has not been called
    assert.not_restart();

    // With custom error message
    assert.not_restart("Expected no restart in this flow");
}

assert.state(ID state [, STRING message])

Assert current state is expected.

sub test_vcl {
    // vcl_recv will move state to lookup to lookup cache
    testing.call_subroutine("vcl_recv");

    // Assert state moves to lookup
    assert.state(lookup);
}

assert.not_state(ID state [, STRING message])

Assert current state is not expected one.

sub test_vcl {
    // vcl_recv will move state to lookup to lookup cache
    testing.call_subroutine("vcl_recv");

    // Assert state does not move to lookup
    assert.not_state(lookup);
}

assert.error(INTEGER status [, STRING response, STRING message])

Assert error status code (and response) if error statement has called.

sub test_vcl {
    // vcl_recv will call error statement with status code and response
    testing.call_subroutine("vcl_recv");

    // Assert error statement has called with expected status
    assert.error(900);

    // Assert error statement has called with expected status and response text
    assert.error(900, "Fastly Internal");
}

assert.not_error([STRING message])

Assert runtime state will not move to error status.

sub test_vcl {
    // vcl_recv will call error statement with status code and response
    testing.call_subroutine("vcl_recv");

    // Assert error statement has not called
    assert.not_error();
}