Skip to content

Commit d62985b

Browse files
simllllclaude
andauthored
test: add comprehensive test coverage for caching library (#5)
* test: add comprehensive test coverage for caching library - Add dedicated test file for SyncCache decorator with 12 tests - Add test file for JSONStringifyKeyStrategy with 17 tests - Expand MemoryStorage tests from 1 to 13 tests (clear, edge cases) - Expand FsJsonStorage tests from 3 to 17 tests (persistence, edge cases) - Expand ExpirationStrategy tests from 4 to 25 tests (TTL, lazy/eager) - Expand Cache decorator tests with DISABLE_CACHE_DECORATOR and error handling - Expand MultiCache decorator tests with proper assertions and edge cases Test count increased from 45 to 138 tests total. * chore: add CLAUDE.md and fix prettier formatting - Create CLAUDE.md with development guidelines - Document build, test, lint, and format commands - Fix prettier formatting in json.stringify.strategy.test.ts --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ea791e commit d62985b

8 files changed

Lines changed: 1505 additions & 48 deletions

CLAUDE.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is **node-ts-cache**, a TypeScript/Node.js caching library featuring:
8+
9+
- Decorator-based caching (@Cache, @SyncCache, @MultiCache)
10+
- Multiple storage backends (Memory, File System, Redis, LRU, etc.)
11+
- Flexible expiration strategies (TTL-based with lazy/eager invalidation)
12+
13+
It's a monorepo using Lerna with independent versioning.
14+
15+
## Common Commands
16+
17+
### Building
18+
19+
```bash
20+
npm run build # Build all packages
21+
```
22+
23+
### Testing
24+
25+
```bash
26+
npm test # Run all tests across all packages
27+
```
28+
29+
### Linting and Type Checking
30+
31+
```bash
32+
npm run lint # Run ESLint on all source files
33+
npm run lint:fix # Run ESLint with auto-fix
34+
```
35+
36+
### Formatting
37+
38+
```bash
39+
npm run format # Check formatting with Prettier
40+
npm run format:fix # Fix formatting with Prettier
41+
```
42+
43+
## Before Committing
44+
45+
Always run these checks before committing:
46+
47+
1. `npm run lint` - Ensure no linting errors
48+
2. `npm run format` - Ensure code is properly formatted
49+
3. `npm test` - Ensure all tests pass
50+
4. `npm run build` - Ensure the project builds successfully
51+
52+
## Project Structure
53+
54+
```
55+
├── ts-cache/ # Core caching package (@node-ts-cache/core)
56+
│ ├── src/
57+
│ │ ├── decorator/ # @Cache, @SyncCache, @MultiCache decorators
58+
│ │ ├── storage/ # MemoryStorage, FsJsonStorage
59+
│ │ ├── strategy/ # ExpirationStrategy, key strategies
60+
│ │ └── types/ # TypeScript interfaces
61+
│ └── test/ # Test files
62+
├── storages/ # Storage adapter packages
63+
│ ├── lru/ # LRU cache storage
64+
│ ├── redis/ # Redis storage (redis package v3.x)
65+
│ ├── redisio/ # Redis storage (ioredis with compression)
66+
│ ├── node-cache/ # node-cache storage
67+
│ └── lru-redis/ # Two-tier LRU + Redis storage
68+
```
69+
70+
## Testing Framework
71+
72+
- Uses Mocha with ts-node ESM loader
73+
- Tests use Node's built-in `assert` module
74+
- Mock Redis instances using `redis-mock` and `ioredis-mock`

ts-cache/test/cache.decorator.test.ts

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,4 +268,231 @@ describe('CacheDecorator', () => {
268268
Assert.strictEqual(await strategy.getItem<string[]>('getUsersPromise'), data);
269269
});
270270
});
271+
272+
describe('DISABLE_CACHE_DECORATOR environment variable', () => {
273+
afterEach(() => {
274+
delete process.env.DISABLE_CACHE_DECORATOR;
275+
});
276+
277+
it('Should skip caching when DISABLE_CACHE_DECORATOR is set', async () => {
278+
process.env.DISABLE_CACHE_DECORATOR = 'true';
279+
280+
const disableStorage = new MemoryStorage();
281+
const disableStrategy = new ExpirationStrategy(disableStorage);
282+
283+
class TestClassDisabled {
284+
callCount = 0;
285+
286+
@Cache(disableStrategy, { ttl: 1000 })
287+
public getUsers(): string[] {
288+
this.callCount++;
289+
return data;
290+
}
291+
}
292+
293+
const myClass = new TestClassDisabled();
294+
295+
await myClass.getUsers();
296+
await myClass.getUsers();
297+
await myClass.getUsers();
298+
299+
// Method should be called every time when cache is disabled
300+
Assert.strictEqual(myClass.callCount, 3);
301+
});
302+
303+
it('Should work normally when DISABLE_CACHE_DECORATOR is not set', async () => {
304+
delete process.env.DISABLE_CACHE_DECORATOR;
305+
306+
const normalStorage = new MemoryStorage();
307+
const normalStrategy = new ExpirationStrategy(normalStorage);
308+
309+
class TestClassNormal {
310+
callCount = 0;
311+
312+
@Cache(normalStrategy, { ttl: 1000 })
313+
public getUsers(): string[] {
314+
this.callCount++;
315+
return data;
316+
}
317+
}
318+
319+
const myClass = new TestClassNormal();
320+
321+
await myClass.getUsers();
322+
await myClass.getUsers();
323+
await myClass.getUsers();
324+
325+
// Method should be called only once when caching is enabled
326+
Assert.strictEqual(myClass.callCount, 1);
327+
});
328+
});
329+
330+
describe('Cache error handling', () => {
331+
it('Should handle cache read errors gracefully', async () => {
332+
const failingStorage = {
333+
getItem: () => {
334+
throw new Error('Read error');
335+
},
336+
setItem: async () => {},
337+
clear: async () => {}
338+
};
339+
const failStrategy = new ExpirationStrategy(failingStorage as unknown as MemoryStorage);
340+
341+
class TestClassReadFail {
342+
callCount = 0;
343+
344+
@Cache(failStrategy, { ttl: 1000 })
345+
public getUsers(): string[] {
346+
this.callCount++;
347+
return data;
348+
}
349+
}
350+
351+
const myClass = new TestClassReadFail();
352+
353+
// Should not throw, just log warning and continue
354+
const result = await myClass.getUsers();
355+
Assert.deepStrictEqual(result, data);
356+
});
357+
358+
it('Should handle cache write errors gracefully', async () => {
359+
const failingStorage = {
360+
getItem: async () => undefined,
361+
setItem: async () => {
362+
throw new Error('Write error');
363+
},
364+
clear: async () => {}
365+
};
366+
const failStrategy = new ExpirationStrategy(failingStorage as unknown as MemoryStorage);
367+
368+
class TestClassWriteFail {
369+
callCount = 0;
370+
371+
@Cache(failStrategy, { ttl: 1000 })
372+
public getUsers(): string[] {
373+
this.callCount++;
374+
return data;
375+
}
376+
}
377+
378+
const myClass = new TestClassWriteFail();
379+
380+
// Should not throw, just log warning and continue
381+
const result = await myClass.getUsers();
382+
Assert.deepStrictEqual(result, data);
383+
});
384+
});
385+
386+
describe('Different argument types', () => {
387+
const argStorage = new MemoryStorage();
388+
const argStrategy = new ExpirationStrategy(argStorage);
389+
390+
beforeEach(async () => {
391+
await argStrategy.clear();
392+
});
393+
394+
class TestClassArgs {
395+
callCount = 0;
396+
397+
@Cache(argStrategy, { ttl: 1000 })
398+
public getWithArgs(...args: unknown[]): unknown[] {
399+
this.callCount++;
400+
return args;
401+
}
402+
}
403+
404+
it('Should cache with object arguments', async () => {
405+
const myClass = new TestClassArgs();
406+
const arg = { id: 1, name: 'test' };
407+
408+
await myClass.getWithArgs(arg);
409+
await myClass.getWithArgs(arg);
410+
411+
Assert.strictEqual(myClass.callCount, 1);
412+
});
413+
414+
it('Should cache with array arguments', async () => {
415+
const myClass = new TestClassArgs();
416+
const arg = [1, 2, 3];
417+
418+
await myClass.getWithArgs(arg);
419+
await myClass.getWithArgs(arg);
420+
421+
Assert.strictEqual(myClass.callCount, 1);
422+
});
423+
424+
it('Should differentiate between different arguments', async () => {
425+
const myClass = new TestClassArgs();
426+
427+
await myClass.getWithArgs(1);
428+
await myClass.getWithArgs(2);
429+
await myClass.getWithArgs(3);
430+
431+
Assert.strictEqual(myClass.callCount, 3);
432+
});
433+
434+
it('Should cache with multiple arguments', async () => {
435+
const myClass = new TestClassArgs();
436+
437+
await myClass.getWithArgs('a', 1, true);
438+
await myClass.getWithArgs('a', 1, true);
439+
440+
Assert.strictEqual(myClass.callCount, 1);
441+
});
442+
});
443+
444+
describe('Error propagation', () => {
445+
it('Should propagate errors from decorated method', async () => {
446+
const errorStorage = new MemoryStorage();
447+
const errorStrategy = new ExpirationStrategy(errorStorage);
448+
449+
class TestClassError {
450+
@Cache(errorStrategy, { ttl: 1000 })
451+
public async throwingMethod(): Promise<string> {
452+
throw new Error('Test error');
453+
}
454+
}
455+
456+
const myClass = new TestClassError();
457+
458+
await Assert.rejects(
459+
async () => {
460+
await myClass.throwingMethod();
461+
},
462+
{ message: 'Test error' }
463+
);
464+
});
465+
466+
it('Should not cache failed method calls', async () => {
467+
const errorStorage = new MemoryStorage();
468+
const errorStrategy = new ExpirationStrategy(errorStorage);
469+
470+
class TestClassErrorCount {
471+
callCount = 0;
472+
473+
@Cache(errorStrategy, { ttl: 1000 })
474+
public async throwingMethod(): Promise<string> {
475+
this.callCount++;
476+
throw new Error('Test error');
477+
}
478+
}
479+
480+
const myClass = new TestClassErrorCount();
481+
482+
try {
483+
await myClass.throwingMethod();
484+
} catch {
485+
// Expected
486+
}
487+
488+
try {
489+
await myClass.throwingMethod();
490+
} catch {
491+
// Expected
492+
}
493+
494+
// Each call should execute since errors are not cached
495+
Assert.strictEqual(myClass.callCount, 2);
496+
});
497+
});
271498
});

0 commit comments

Comments
 (0)