Testland
Browse all skills & agents

varnish-test-vtc-syntax

Wraps the varnishtest CLI + VTC (Varnish Test Case) syntax for testing VCL configurations. Covers the VTC test-file format (varnishtest scripts with server { ... } + client { ... } + varnish v1 -vcl+backend { ... } blocks), the grace-mode + saint-mode behaviours (stale-while-revalidate + stale-if-error equivalents in VCL), the PURGE method handler pattern (vcl_purge subroutine + ACL guards), and surrogate-key invalidation via xkey vmod. Use when authoring or testing Varnish-based caching layers.

varnish-test-vtc-syntax

Overview

varnishtest (CLI) executes .vtc (Varnish Test Case) files that spin up a Varnish instance + a mock backend + a mock client in-process. Per varnish-cache.org/docs, VTC is the canonical way to test VCL - Varnish's configuration language - without a real network.

When to use

  • Authoring or modifying VCL.
  • Testing PURGE handlers, grace/saint mode, surrogate-key logic.
  • Regression-testing VCL after Varnish version bump.
  • CI gate before a VCL deploy.

Authoring

Install

Varnish ships varnishtest in the standard distribution:

apt install varnish        # Debian/Ubuntu
brew install varnish       # macOS
varnishtest -v             # verify

Anatomy of a VTC

varnishtest "basic cache hit test"

server s1 {
  rxreq
  txresp -hdr "Cache-Control: max-age=60" -body "hello"
} -start

varnish v1 -vcl+backend {
  // VCL goes here
} -start

client c1 {
  txreq -url "/"
  rxresp
  expect resp.status == 200
  expect resp.body == "hello"

  txreq -url "/"
  rxresp
  expect resp.http.x-cache == "HIT"   // assumes vcl_deliver sets this
} -run

varnish v1 -expect cache_hit == 1

Per Varnish docs, the VTC file has four block types:

BlockPurpose
server sNMock origin
varnish vNVarnish instance with VCL
client cNSend requests, assert responses
barrier, delay, shellSynchronisation + setup

Testing PURGE

The canonical pattern from Varnish docs:

acl purge {
  "localhost";
  "10.0.0.0"/8;
}

sub vcl_recv {
  if (req.method == "PURGE") {
    if (!client.ip ~ purge) {
      return (synth(403, "Not allowed"));
    }
    return (purge);
  }
}

sub vcl_purge {
  return (synth(200, "Purged"));
}

The VTC:

client c1 {
  txreq -url "/foo"           // populate cache
  rxresp
  expect resp.status == 200

  txreq -url "/foo" -method "PURGE"
  rxresp
  expect resp.status == 200

  // Next request should hit origin again
  txreq -url "/foo"
  rxresp
  expect resp.http.x-cache == "MISS"
} -run

Grace mode (stale-while-revalidate equivalent)

Per Varnish docs and per stale-while-revalidate-reference:

sub vcl_backend_response {
  set beresp.grace = 1h;       // serve stale for 1h while async-refreshing
}

sub vcl_deliver {
  if (obj.ttl < 0s) {
    set resp.http.x-cache = "GRACE";
  } else if (obj.hits == 0) {
    set resp.http.x-cache = "MISS";
  } else {
    set resp.http.x-cache = "HIT";
  }
}

VTC for grace mode:

varnishtest "grace serves stale while refreshing"

server s1 {
  rxreq
  txresp -hdr "Cache-Control: max-age=1"

  rxreq
  delay 2                        // simulate slow refresh
  txresp -hdr "Cache-Control: max-age=1"
} -start

varnish v1 -vcl+backend {
  sub vcl_backend_response {
    set beresp.grace = 60s;
  }
} -start

client c1 {
  txreq -url "/"
  rxresp
  expect resp.status == 200

  delay 1.5                      // past TTL

  txreq -url "/"
  rxresp
  expect resp.status == 200
  expect resp.http.x-cache == "GRACE"
} -run

Surrogate-key (xkey vmod)

import xkey;

sub vcl_backend_response {
  set beresp.http.xkey = "user-1 posts-feed";   // tag the object
}

sub vcl_recv {
  if (req.method == "PURGE" && req.http.xkey-purge) {
    set req.http.X-Purges = xkey.softpurge(req.http.xkey-purge);
    return (synth(200));
  }
}

Running

varnishtest -v cache-tests.vtc
# Or: varnishtest -v tests/*.vtc

# Verbose with full Varnish log output:
varnishtest -vvv cache-tests.vtc

Exit code 0 = pass; non-zero = fail.

Parallel runs

varnishtest -j 4 tests/*.vtc

Each VTC runs in an isolated Varnish instance; parallel-safe.

Parsing results

varnishtest output:

**** v1   vsl:  0     SLT_End
**** v1   vsl_dispatch_complete
*    top  TEST cache-tests.vtc passed (1.34)

passed or FAILED. The failure output shows the failing expect line and the actual value.

For CI: parse passed / FAILED count in the summary.

CI integration

jobs:
  vcl-tests:
    runs-on: ubuntu-latest
    container: varnish:7
    steps:
      - uses: actions/checkout@v5
      - name: Run VCL tests
        run: varnishtest -v tests/vcl/*.vtc

Anti-patterns

Anti-patternWhy it failsFix
Testing VCL by hitting a real Varnish in devSlow; environment-dependentUse varnishtest - in-process
delay 60 for TTL testTests slow; flakySet short TTL (max-age=1, delay 1.5)
Missing -expect cache_hit == NPass relies on observer-affects-systemAlways assert cache counters
Untested PURGE ACLOpen PURGE = cache-flush DoSTest 403 from external IP
No grace-mode testProduction grace surprisesVerify x-cache GRACE on stale fetch
One mega-VTCFailures opaqueOne concern per file
Skip varnishlog inspectionHard to debug failuresUse -vvv in CI for failure logs
Hand-roll surrogate-key without xkey vmodManual ban-list grows; slow regex matchesUse xkey vmod

Limitations

  • VTC is Varnish-specific. Doesn't transfer to nginx / Apache cache testing.
  • Mock client/server is in-process. Doesn't catch network- layer issues (TLS handshake, slow-client, hostname resolution).
  • xkey vmod requires Varnish Plus or self-compile. Open-source Varnish has purge.soft but not surrogate keys natively.
  • Doesn't test CDN-edge behaviour. Varnish is one tier; full multi-tier tests need cdn-cache-purge-tests patterns.

References