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.

Install with skills.sh (any agent)

npx skills add testland/qa --skill varnish-test-vtc-syntax
View source

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 (opens in new window), 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 cache-coherence-patterns-reference references/stale-while-revalidate.md:

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

Related skills

cache-coherence-patterns-reference

Pure-reference catalog of cache-coherence patterns across the request path. Defines the five-tier cache stack (browser → CDN → reverse-proxy → application → data store), the per-tier cache-writing patterns (cache-aside, write-through, write-back, write-around, refresh-ahead), and the canonical invalidation strategies (TTL-only, event-driven purge, surrogate keys, version-tagged URLs, soft purge), plus an anti-pattern table and a worked multi-tenant coherence-test example. Deep detail lives in references/: RFC 9111 Cache-Control / Vary / ETag directive tables, the cross-tier test surface, cache-stampede (thundering-herd) mitigations incl. the XFetch formula, and RFC 5861 stale-while-revalidate / stale-if-error semantics. Use for pattern selection, Cache-Control header design, coherence audits, stampede-refresh strategy, and SWR/SIE window design; use a cache-key-collision check when the question is whether two concrete requests collide on a key scheme.

cache-key-discriminator-audit

Audits whether a cache key carries every discriminator the cached response actually depends on, so two requests that must not share a slot cannot collide. Ranks identity discriminators (tenant, user, authorization scope, plan tier) above presentation ones (locale, region, currency, feature flag), maps each missing discriminator to the data-exposure or wrong-content consequence it causes, classifies each key-and-value pair into a critical / high / medium severity band (including the Python lru_cache-on-an-instance-method trap), and writes the fix as a namespaced key builder plus the matching HTTP Vary header. Use when a cache key is being designed or changed, when a per-user or per-tenant response is about to be stored in a shared cache or CDN, or when investigating a report that one user or tenant saw another's data.

cdn-cache-purge-tests

Wraps CDN cache-purge testing patterns for Cloudflare (POST /zones/{zone_id}/purge_cache, single-file / everything / cache-tags / hostname / prefix), Fastly (POST purge-by-key / purge-all, surrogate-keys via Surrogate-Key header), and CloudFront (CreateInvalidation API + paths). Covers end-to-end test patterns (write origin → trigger purge → assert edge serves fresh), purge-propagation delay testing (typically 1-30s globally), surrogate-key + cache-tag patterns for group-purge, and Cache-Status header verification (cf-cache-status: HIT/MISS/BYPASS). Also owns the client tier: browser-side Cache-Control verification with Playwright (served-from-cache via CDP, ETag 304 round-trips, Workbox service-worker strategies, reload semantics) in references/browser-cache-control.md. Use when designing or auditing CDN cache-invalidation workflows or browser-tier caching behaviour in E2E tests.

redis-cache-tests

Wraps Redis cache testing: EXPIRE / PEXPIRE / TTL verification (Redis 7+ NX/XX/GT/LT flags), cache-aside write-then-invalidate (write source → DEL key → assert fresh read), eviction under memory pressure (maxmemory + allkeys-lru), pub/sub invalidation across nodes, and tenant key-namespacing. Use when Redis is the app's primary cache. For the CDN/browser HTTP tier use cdn-cache-purge-tests; a runnable test, not the cache-coherence-patterns-reference catalog (which owns the stampede + stale-while-revalidate references).