Vitest and cache

Hello, I'm trying to test the cache process of a worker (proxy requests). It's a very simple setup, it receives a request, if there's a cached response, return it, otherwise fetch a different origin, cache that response and return it. The worker works as expected, but in the tests the cache.match never matches, always returns undefined. Am I doing something wrong? Or is this not supported? Example code in attachment.
11 Replies
James
James5d ago
I have cache tests working like in the following: https://github.com/nodecraft/crafthead/blob/cefb0846ed2ec0e8133c2ea235c66e93be8da062/test/worker.test.ts#L255 Can you share a full reproducible snippet/repo that we can take and run, if the above doesn't help? There's lots missing from your snippet there getMiniflareFetchMock is from the very old and outdated jest testing environment https://developers.cloudflare.com/workers/testing/vitest-integration/get-started/migrate-from-miniflare-2/ might be of help to you?
peterz
peterzOP5d ago
Thank you @James, there are indeed some differences in your example, I'll take a closer look. However, putting aside if I may the request part, only the cache is failing. This fails on the expect(cachedResponse2).toBeDefined(); line.
test.only('test cache only', async () => {
const cache = await caches.default;
const cachedResponse1 = new Response('cached', { status: 200 });
const cacheKey = 'https://example.com/foo';
await cache.put(cacheKey, cachedResponse1);

const cachedResponse2 = await cache.match(cacheKey);
expect(cachedResponse2).toBeDefined();
expect(cachedResponse2.status).toBe(200);
expect(await cachedResponse2.text()).toBe('cached');
});
test.only('test cache only', async () => {
const cache = await caches.default;
const cachedResponse1 = new Response('cached', { status: 200 });
const cacheKey = 'https://example.com/foo';
await cache.put(cacheKey, cachedResponse1);

const cachedResponse2 = await cache.match(cacheKey);
expect(cachedResponse2).toBeDefined();
expect(cachedResponse2.status).toBe(200);
expect(await cachedResponse2.text()).toBe('cached');
});
Another thing then, where can I find the "best practices" currently for testing workers?
James
James5d ago
I would recommend you make a request to the worker to validate the cache is working via a header or similar - testing cache.match in the jest/vitest environment is likely going to be weird and not make much sense
James
James5d ago
The Cloudflare Blog
Improved Cloudflare Workers testing via Vitest and workerd
Today, we’re excited to announce a new Workers Vitest integration - allowing you to write unit and integration tests via the popular testing framework, Vitest, that execute directly in our runtime, workerd!
Cloudflare Docs
Get started · Cloudflare Workers docs
Install and set up the Workers Vitest integration.
peterz
peterzOP5d ago
ok, thank you very much @James! I changed the implementation and it still doesn't work. Now I'm testing with two sequential requests to same url, and both are hitting the origin... tried both with SELF.fetch and worker.fetch. No luck.
it('responds with cached response in second request', async () => {
const ctx = createExecutionContext();
const response = await worker.fetch(new IncomingRequest(DOCS_ENDPOINT), env, ctx);
// const response = await SELF.fetch(new IncomingRequest(DOCS_ENDPOINT));
await waitOnExecutionContext(ctx);
expect(await response.text()).toBe(`fresh`);
expect(response.headers.get("CF-Cache-Status")).toBe(null);
console.log('cache status header:', response.headers.get("CF-Cache-Status"));

const ctx2 = createExecutionContext();
const response2 = await worker.fetch(new IncomingRequest(DOCS_ENDPOINT), env, ctx);
// const response2 = await SELF.fetch(new IncomingRequest(DOCS_ENDPOINT));
await waitOnExecutionContext(ctx2);
expect(await response2.text()).toBe(`fresh`);
expect(response2.headers.get("CF-Cache-Status")).toBe('HIT');
console.log('cache status header2:', response2.headers.get("CF-Cache-Status"));
});
it('responds with cached response in second request', async () => {
const ctx = createExecutionContext();
const response = await worker.fetch(new IncomingRequest(DOCS_ENDPOINT), env, ctx);
// const response = await SELF.fetch(new IncomingRequest(DOCS_ENDPOINT));
await waitOnExecutionContext(ctx);
expect(await response.text()).toBe(`fresh`);
expect(response.headers.get("CF-Cache-Status")).toBe(null);
console.log('cache status header:', response.headers.get("CF-Cache-Status"));

const ctx2 = createExecutionContext();
const response2 = await worker.fetch(new IncomingRequest(DOCS_ENDPOINT), env, ctx);
// const response2 = await SELF.fetch(new IncomingRequest(DOCS_ENDPOINT));
await waitOnExecutionContext(ctx2);
expect(await response2.text()).toBe(`fresh`);
expect(response2.headers.get("CF-Cache-Status")).toBe('HIT');
console.log('cache status header2:', response2.headers.get("CF-Cache-Status"));
});
James
James4d ago
I don’t think the cf cache headers are set locally What do you see when you hit the worker manually? In my examples above, I set my own cache header after a cache match and that works without issue
peterz
peterzOP3d ago
I wasn't using it to check, it was just to make the test fail. I was checking in the logs. I created this minimal example (from here) where we can see both in the logs and the fail test. Worker:
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = request.url;
const cacheKey = url;
const cache = caches.default;

let response = await cache.match(cacheKey);
if (response) {
console.log('Cache HIT:', url);
return new Response('Cached response');
}
console.log('Cache MISS:', url);

response = new Response('Fresh response');
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
} satisfies ExportedHandler<Env>;
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = request.url;
const cacheKey = url;
const cache = caches.default;

let response = await cache.match(cacheKey);
if (response) {
console.log('Cache HIT:', url);
return new Response('Cached response');
}
console.log('Cache MISS:', url);

response = new Response('Fresh response');
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return response;
},
} satisfies ExportedHandler<Env>;
Tests:
describe('Hello World worker', () => {
it.only('responds with Hello World! (twice)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toEqual('Fresh response');

const response2 = await SELF.fetch('https://example.com');
expect(await response2.text()).toEqual('Cached response');
});
});
describe('Hello World worker', () => {
it.only('responds with Hello World! (twice)', async () => {
const response = await SELF.fetch('https://example.com');
expect(await response.text()).toEqual('Fresh response');

const response2 = await SELF.fetch('https://example.com');
expect(await response2.text()).toEqual('Cached response');
});
});
James
James3d ago
Taking a look at this for you today 😄 You're simply missing any headers that'd cause the request to actually get cached. If you change to:
response = new Response('Fresh response', {
headers: {
'cache-control': 'public, max-age=60',
}
});
response = new Response('Fresh response', {
headers: {
'cache-control': 'public, max-age=60',
}
});
for example, then your tests work as expected.
peterz
peterzOP3d ago
OMG 🤦 I just tested, of course, it makes sense... Thank you very much @James!!! BTW, have a great new year!
James
James3d ago
You too!
peterz
peterzOP2d ago
just to add another detail, as I said previously, the original worker behaves as expected. The difference is that the origin responses had the cache-control header, and my mock responses didn't. now I'm a bit confused about secrets. What's the proper way of testing a SELF.fetch(request) and having some secrets as environment variables? oh nevermind, I just found the config place in vitest 🙂

Did you find this page helpful?