Testing
@fozooni/nestjs-storage provides first-class testing utilities that make it easy to write fast, deterministic tests without touching real cloud storage. The centerpiece is FakeDisk -- an in-memory implementation of FilesystemContract with built-in assertion methods.
FakeDisk
FakeDisk stores all files in memory and implements the full FilesystemContract interface. It is fast, requires no configuration, and provides assertion methods for verifying storage operations in tests.
Standalone Usage
Use FakeDisk directly when testing services in isolation:
import { FakeDisk } from '@fozooni/nestjs-storage/testing';
describe('DocumentProcessor', () => {
let disk: FakeDisk;
beforeEach(() => {
disk = new FakeDisk();
});
it('should store processed content', async () => {
const processor = new DocumentProcessor(disk);
await processor.process('Hello, World!');
disk.assertExists('output/processed.txt');
disk.assertContentEquals('output/processed.txt', 'HELLO, WORLD!');
});
it('should delete temporary files after processing', async () => {
const processor = new DocumentProcessor(disk);
await processor.processWithTempFile('data');
disk.assertExists('output/result.txt');
disk.assertMissing('temp/working.tmp');
});
});Assertion Methods
FakeDisk provides the following assertion methods that throw descriptive errors on failure:
assertExists(path)
Assert that a file exists on disk:
await disk.put('uploads/photo.jpg', imageBuffer);
disk.assertExists('uploads/photo.jpg'); // passes
disk.assertExists('uploads/missing.jpg'); // throws: "Expected file 'uploads/missing.jpg' to exist, but it does not"assertMissing(path)
Assert that a file does NOT exist:
await disk.delete('temp/old-file.txt');
disk.assertMissing('temp/old-file.txt'); // passesassertContentEquals(path, expected)
Assert that a file exists and its content matches exactly:
await disk.put('config.json', '{"version": 1}');
disk.assertContentEquals('config.json', '{"version": 1}'); // passes
disk.assertContentEquals('config.json', '{"version": 2}'); // throws with diffassertCount(expected, directory?)
Assert the total number of stored files, optionally scoped to a directory:
await disk.put('a.txt', 'a');
await disk.put('b.txt', 'b');
await disk.put('sub/c.txt', 'c');
disk.assertCount(3); // all files
disk.assertCount(1, 'sub'); // files in 'sub/' directoryassertDirectoryEmpty(directory)
Assert that a directory contains no files:
disk.assertDirectoryEmpty('uploads'); // passes if no files under uploads/getStoredFiles()
Get a map of all stored file paths and their content (useful for debugging):
await disk.put('a.txt', 'content-a');
await disk.put('b.txt', 'content-b');
const files = disk.getStoredFiles();
// Map { 'a.txt' => Buffer, 'b.txt' => Buffer }getStoredFile(path)
Get the content of a specific stored file:
await disk.put('config.json', '{"key": "value"}');
const content = disk.getStoredFile('config.json');
// Buffer containing '{"key": "value"}'reset()
Clear all stored files and reset the disk to a clean state:
await disk.put('a.txt', 'a');
disk.reset();
disk.assertCount(0); // passesTIP
Always call disk.reset() in beforeEach or create a fresh FakeDisk instance for each test to prevent state leakage between tests:
beforeEach(() => {
disk = new FakeDisk();
// OR
disk.reset();
});StorageTestUtils
StorageTestUtils provides helper methods for common testing scenarios.
StorageTestUtils.fake(storageService, diskName)
Swap a real disk for a FakeDisk within a StorageService instance. Returns the FakeDisk for assertions:
import { StorageTestUtils, FakeDisk } from '@fozooni/nestjs-storage/testing';
import { StorageService } from '@fozooni/nestjs-storage';
let storage: StorageService;
let fakeDisk: FakeDisk;
beforeEach(() => {
fakeDisk = StorageTestUtils.fake(storage, 'local');
});
it('should upload to the local disk', async () => {
await myService.uploadFile('test.txt', 'content');
fakeDisk.assertExists('test.txt');
});StorageTestUtils.fakeFile(options?)
Create a mock Express.Multer.File object for testing upload handlers:
import { StorageTestUtils } from '@fozooni/nestjs-storage/testing';
const mockFile = StorageTestUtils.fakeFile({
originalname: 'photo.jpg',
mimetype: 'image/jpeg',
buffer: Buffer.from('fake-image-data'),
size: 1024,
});
// Use in tests
await myService.handleUpload(mockFile);Without arguments, fakeFile() returns a minimal valid file with defaults:
const mockFile = StorageTestUtils.fakeFile();
// { originalname: 'test.txt', mimetype: 'text/plain', buffer: Buffer, size: 4 }StorageTestUtils.fakeFileWithSize(bytes, name?)
Create a mock file with a specific size. Useful for testing file size limits and quota enforcement:
import { StorageTestUtils } from '@fozooni/nestjs-storage/testing';
// Create a 5 MB mock file
const largeFile = StorageTestUtils.fakeFileWithSize(
5 * 1024 * 1024,
'large-video.mp4',
);
// Test quota enforcement
it('should reject files over quota', async () => {
await expect(
quotaDisk.put('uploads/large.mp4', largeFile.buffer),
).rejects.toThrow(StorageQuotaExceededError);
});Full NestJS Testing Example
Here is a complete example of testing a service that depends on storage using NestJS's TestingModule:
// documents.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { StorageModule, StorageService } from '@fozooni/nestjs-storage';
import { StorageTestUtils, FakeDisk } from '@fozooni/nestjs-storage/testing';
import { DocumentsService } from './documents.service';
describe('DocumentsService', () => {
let module: TestingModule;
let service: DocumentsService;
let storage: StorageService;
let fakeDisk: FakeDisk;
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [
StorageModule.forRoot({
default: 'local',
disks: {
local: {
driver: 'local',
root: './test-storage',
},
},
}),
],
providers: [DocumentsService],
}).compile();
service = module.get(DocumentsService);
storage = module.get(StorageService);
// Replace the real disk with FakeDisk
fakeDisk = StorageTestUtils.fake(storage, 'local');
});
afterEach(async () => {
await module.close();
});
describe('uploadDocument', () => {
it('should store the document on disk', async () => {
const file = StorageTestUtils.fakeFile({
originalname: 'report.pdf',
mimetype: 'application/pdf',
buffer: Buffer.from('pdf-content'),
});
const result = await service.uploadDocument('user-123', file);
expect(result.path).toContain('documents/user-123');
fakeDisk.assertExists(result.path);
});
it('should store metadata alongside the document', async () => {
const file = StorageTestUtils.fakeFile({
originalname: 'report.pdf',
mimetype: 'application/pdf',
});
const result = await service.uploadDocument('user-123', file);
fakeDisk.assertExists(`${result.path}.meta.json`);
const meta = JSON.parse(
fakeDisk.getStoredFile(`${result.path}.meta.json`).toString(),
);
expect(meta.author).toBe('user-123');
});
});
describe('deleteDocument', () => {
it('should remove the document and its metadata', async () => {
// Arrange
await fakeDisk.put('documents/user-123/doc.pdf', 'content');
await fakeDisk.put('documents/user-123/doc.pdf.meta.json', '{}');
// Act
await service.deleteDocument('documents/user-123/doc.pdf');
// Assert
fakeDisk.assertMissing('documents/user-123/doc.pdf');
fakeDisk.assertMissing('documents/user-123/doc.pdf.meta.json');
});
});
describe('listUserDocuments', () => {
it('should return all documents for a user', async () => {
await fakeDisk.put('documents/user-123/a.pdf', 'a');
await fakeDisk.put('documents/user-123/b.pdf', 'b');
await fakeDisk.put('documents/user-456/c.pdf', 'c');
const docs = await service.listUserDocuments('user-123');
expect(docs).toHaveLength(2);
expect(docs).toContain('documents/user-123/a.pdf');
expect(docs).toContain('documents/user-123/b.pdf');
});
});
});Testing with Interceptors
Test controllers that use StorageFileInterceptor by sending real multipart requests through the test server:
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { StorageModule, StorageService } from '@fozooni/nestjs-storage';
import { StorageTestUtils, FakeDisk } from '@fozooni/nestjs-storage/testing';
import { UploadsController } from './uploads.controller';
describe('UploadsController (e2e)', () => {
let app: INestApplication;
let fakeDisk: FakeDisk;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
StorageModule.forRoot({
default: 'local',
disks: {
local: { driver: 'local', root: './test-storage' },
},
}),
],
controllers: [UploadsController],
}).compile();
app = module.createNestApplication();
await app.init();
const storage = module.get(StorageService);
fakeDisk = StorageTestUtils.fake(storage, 'local');
});
afterEach(async () => {
await app.close();
});
it('POST /uploads/avatar should store the file', async () => {
const response = await request(app.getHttpServer())
.post('/uploads/avatar')
.attach('avatar', Buffer.from('fake-image'), {
filename: 'photo.jpg',
contentType: 'image/jpeg',
})
.expect(201);
expect(response.body.path).toBeDefined();
expect(response.body.url).toBeDefined();
fakeDisk.assertCount(1, 'avatars');
});
it('POST /uploads/avatar should reject non-image files', async () => {
await request(app.getHttpServer())
.post('/uploads/avatar')
.attach('avatar', Buffer.from('not-an-image'), {
filename: 'malware.exe',
contentType: 'application/octet-stream',
})
.expect(400);
fakeDisk.assertCount(0);
});
});Testing Decorator Disks
When your application uses decorator disks (encrypted, cached, retry, etc.), test them by wrapping a FakeDisk:
import { FakeDisk } from '@fozooni/nestjs-storage/testing';
import { EncryptedDisk, CachedDisk, RetryDisk } from '@fozooni/nestjs-storage';
describe('Encrypted + Cached storage', () => {
let fakeDisk: FakeDisk;
let encryptedDisk: EncryptedDisk;
let cachedDisk: CachedDisk;
beforeEach(() => {
fakeDisk = new FakeDisk();
// Stack decorators on top of FakeDisk
encryptedDisk = new EncryptedDisk(fakeDisk, {
key: 'test-encryption-key-32-chars-ok!',
algorithm: 'aes-256-cbc',
});
cachedDisk = new CachedDisk(encryptedDisk, {
ttl: 60,
});
});
it('should encrypt content before storage', async () => {
await encryptedDisk.put('secret.txt', 'sensitive data');
// The raw content in FakeDisk should be encrypted
const rawContent = fakeDisk.getStoredFile('secret.txt').toString();
expect(rawContent).not.toBe('sensitive data');
// But reading through the encrypted disk should decrypt
const decrypted = await encryptedDisk.get('secret.txt');
expect(decrypted).toBe('sensitive data');
});
it('should serve from cache on second read', async () => {
await cachedDisk.put('data.json', '{"key": "value"}');
// First read -- hits the underlying disk
const first = await cachedDisk.get('data.json');
// Delete the underlying file
await fakeDisk.delete('data.json');
// Second read -- served from cache
const second = await cachedDisk.get('data.json');
expect(second).toBe(first);
});
});Testing RetryDisk
import { FakeDisk } from '@fozooni/nestjs-storage/testing';
import { RetryDisk, StorageNetworkError } from '@fozooni/nestjs-storage';
describe('RetryDisk', () => {
it('should retry on network errors', async () => {
const fakeDisk = new FakeDisk();
let callCount = 0;
// Mock a network failure on the first two attempts
const originalGet = fakeDisk.get.bind(fakeDisk);
jest.spyOn(fakeDisk, 'get').mockImplementation(async (...args) => {
callCount++;
if (callCount <= 2) {
throw new StorageNetworkError('Connection reset');
}
return originalGet(...args);
});
await fakeDisk.put('data.txt', 'hello');
const retryDisk = new RetryDisk(fakeDisk, {
maxRetries: 3,
delay: 10, // Short delay for tests
});
const result = await retryDisk.get('data.txt');
expect(result).toBe('hello');
expect(callCount).toBe(3); // Failed twice, succeeded on third
});
});Testing with Multiple Disks
describe('CrossDiskSync', () => {
let primaryDisk: FakeDisk;
let backupDisk: FakeDisk;
beforeEach(async () => {
const module = await Test.createTestingModule({
imports: [
StorageModule.forRoot({
default: 'primary',
disks: {
primary: { driver: 'local', root: './primary' },
backup: { driver: 'local', root: './backup' },
},
}),
],
providers: [CrossDiskSyncService],
}).compile();
const storage = module.get(StorageService);
primaryDisk = StorageTestUtils.fake(storage, 'primary');
backupDisk = StorageTestUtils.fake(storage, 'backup');
});
it('should sync files from primary to backup', async () => {
await primaryDisk.put('important.pdf', 'data');
await service.syncToBackup('important.pdf');
primaryDisk.assertExists('important.pdf');
backupDisk.assertExists('important.pdf');
backupDisk.assertContentEquals('important.pdf', 'data');
});
});Best Practices
Reset Between Tests
Always create a fresh FakeDisk or call reset() in beforeEach. Shared state between tests leads to flaky, order-dependent test suites:
beforeEach(() => {
fakeDisk = new FakeDisk(); // Fresh instance every test
});FakeDisk Supports All Operations
FakeDisk implements the full FilesystemContract including exists(), get(), put(), delete(), copy(), move(), files(), allFiles(), directories(), size(), lastModified(), mimeType(), getMetadata(), checksum(), getRange(), putIfMatch(), putIfNoneMatch(), setVisibility(), getVisibility(), and more. You can test any operation without real storage.
WARNING
FakeDisk does not simulate network latency, rate limiting, or eventual consistency. If you need to test those behaviors, use the real driver against a local instance (e.g., MinIO for S3 compatibility testing) or mock specific methods on FakeDisk.
Next Steps
Learn how to leverage AI-ready documentation with LLM Docs -- use llm.md and llm-full.md with Cursor, Claude Code, GitHub Copilot, and other AI tools.