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.
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 Specification | Tag CLI Option | Run Test |
|---|---|---|
| prod | N/A | NO |
| prod | prod | YES |
| prod | dev | NO |
| !prod | N/A | YES |
| !prod | prod | NO |
| !prod | dev | YES |
| N/A | N/A | YES |
| N/A | prod | YES |
| N/A | dev | YES |
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:
| Name | Type | Description |
|---|---|---|
| testing.state | STRING | Return state which is called return statement in a subroutine |
| testing.synthetic_body | STRING | The body generated via a call to synthetic or synthetic.base64 |
| testing.origin_host_header | STRING | The value of Host header that will send to an origin |
| testing.call_subroutine | FUNCTION | Call subroutine which is defined in main VCL; accepts optional args and returns a value for functional subroutines |
| testing.fixed_time | FUNCTION | Use fixed time whole the test suite |
| testing.override_host | FUNCTION | Override request host with provided argument in the test case |
| testing.inject_variable | FUNCTION | Inject variable that returns tentative value |
| testing.inspect | FUNCTION | Inspect predefined variables for any scopes |
| testing.table_set | FUNCTION | Inject value for key to main VCL table |
| testing.table_merge | FUNCTION | Merge values from testing VCL table to main VCL table |
| testing.mock | FUNCTION | Mock the subroutine with specified subroutine in the testing VCL |
| testing.restore_mock | FUNCTION | Restore specific mocked subroutine |
| testing.restore_all_mocks | FUNCTION | Restore all mocked subroutines |
| testing.get_env | FUNCTION | Get environment variable value on running machine |
| testing.fixed_access_rate | FUNCTION | Set fixed access rate value |
| testing.set_backend_health | FUNCTION | Set health status of backend |
| assert | FUNCTION | Assert provided expression should be true |
| assert.true | FUNCTION | Assert actual value should be true |
| assert.false | FUNCTION | Assert actual value should be false |
| assert.is_json | FUNCTION | Assert actual string should be valid JSON |
| assert.is_notset | FUNCTION | Assert actual value should be NotSet |
| assert.equal | FUNCTION | Assert actual value should be equal to expected value (alias of assert.strict_equal) |
| assert.not_equal | FUNCTION | Assert actual value should not be equal to expected value (alias of assert.not_strict_equal) |
| assert.strict_equal | FUNCTION | Assert actual value should be equal to expected value strictly |
| assert.not_strict_equal | FUNCTION | Assert actual value should not be equal to expected value strictly |
| assert.equal_fold | FUNCTION | Assert actual value should be equal to with case insensitive |
| assert.match | FUNCTION | Assert actual string should be matched against expected regular expression |
| assert.not_match | FUNCTION | Assert actual string should not be matches against expected regular expression |
| assert.contains | FUNCTION | Assert actual string should contain the expected string |
| assert.not_contains | FUNCTION | Assert actual string should not contain the expected string |
| assert.starts_with | FUNCTION | Assert actual string should start with expected string |
| assert.ends_with | FUNCTION | Assert actual string should end with expected string |
| assert.subroutine_called | FUNCTION | Assert subroutine has called in testing subroutine (with times) |
| assert.not_subroutine_called | FUNCTION | Assert subroutine has not called in testing subroutine |
| assert.restart | FUNCTION | Assert restart statement has called |
| assert.not_restart | FUNCTION | Assert restart statement has not been called |
| assert.state | FUNCTION | Assert after state is expected one |
| assert.not_state | FUNCTION | Assert after state is not expected one |
| assert.error | FUNCTION | Assert error status code (and response) if error statement has called |
| assert.not_error | FUNCTION | Assert 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:SSformatted 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.10sratecounter.rc.bucket.20sratecounter.rc.bucket.30sratecounter.rc.bucket.40sratecounter.rc.bucket.50sratecounter.rc.bucket.60sratecounter.rc.bucket.60sratecounter.rc.rate.1sratecounter.rc.rate.10sratecounter.rc.rate.60sratelimit.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:truefor healthy,falsefor 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();
}