Media Application Testing: Video, Audio, Images, and File Uploads
Media-heavy applications — platforms that handle user-uploaded images, videos, audio, or documents — have unique testing requirements. The data is binary, the processing is compute-intensive, and the output quality is hard to assert on with text comparisons. This guide covers the testing strategy across all media types.
The Media Testing Stack
Browser Layer ← file picker UI, drag-and-drop, player controls
API Layer ← upload endpoints, validation, storage triggers
Processing Layer ← transcoding, resizing, format conversion
Storage Layer ← S3/GCS, CDN, signed URLs
Delivery Layer ← Range requests, streaming, adaptive bitrateEach layer needs a different testing approach.
Common Testing Challenges
Binary output: You can't string-compare a JPEG. Use metadata assertions (format, dimensions, file size) and library-provided validation.
Processing time: Video transcoding takes seconds or minutes. Mock the processor in unit tests; use small files in integration tests.
External services: S3, Cloudinary, Mux — mock these in unit tests with library-specific mock clients.
Non-determinism: Lossy compression produces slightly different outputs across versions. Test properties, not exact byte equality.
Hardware: Camera/microphone tests require browser-specific flags. Use --use-fake-device-for-media-stream in Playwright.
Image Testing Summary
| Test Type | What to Test | Tool |
|---|---|---|
| Unit | Resize dimensions, format conversion, EXIF strip | Sharp metadata |
| Unit | Magic byte validation | Buffer inspection |
| Unit | S3 upload parameters | aws-sdk-client-mock |
| Integration | Full upload pipeline | Supertest + test storage |
| E2E | File picker, crop UI, preview | Playwright setInputFiles |
Key assertions:
const metadata = await sharp(outputBuffer).metadata()
expect(metadata.width).toBe(200)
expect(metadata.format).toBe('webp')
expect(metadata.exif).toBeUndefined() // No GPS dataVideo Testing Summary
| Test Type | What to Test | Tool |
|---|---|---|
| Unit | Transcoding output | FFmpeg + ffprobe |
| Unit | HLS manifest format | Custom validator |
| Unit | Segment existence | fs.existsSync |
| API | Range request handling | Supertest |
| E2E | Player playback | Playwright + fake media |
Key assertions:
const manifest = fs.readFileSync(manifestPath, 'utf-8')
expect(manifest).toContain('#EXTM3U')
expect(manifest).toContain('#EXT-X-ENDLIST')
// Segments exist and are non-empty
const segments = manifest.match(/output\d+\.ts/g)
for (const seg of segments) {
expect(fs.statSync(path.join(outputDir, seg)).size).toBeGreaterThan(0)
}Audio Testing Summary
| Test Type | What to Test | Tool |
|---|---|---|
| Unit | Format conversion | FFprobe metadata |
| Unit | Duration preservation | Format duration comparison |
| Unit | Waveform generation | Synthetic buffer input |
| Unit | RSS feed structure | XML parser |
| Unit | Web Audio API logic | Mock AudioContext |
File Upload Security Tests
These tests matter most from a security perspective:
const securityTests = [
// Type validation
{ filename: 'script.php', contentType: 'application/x-php', expectedStatus: 400 },
{ filename: 'malware.exe', contentType: 'application/octet-stream', expectedStatus: 400 },
{ filename: 'xss.svg', contentType: 'image/svg+xml', expectedStatus: 400 },
// Content type spoofing
{ filename: 'image.jpg', content: Buffer.from('<?php echo 1; ?>'), expectedStatus: 400 },
// Size limits
{ filename: 'huge.jpg', size: 100 * 1024 * 1024, expectedStatus: 400 },
// Valid uploads
{ filename: 'photo.jpg', contentType: 'image/jpeg', expectedStatus: 200 },
{ filename: 'doc.pdf', contentType: 'application/pdf', expectedStatus: 200 },
]
for (const tc of securityTests) {
test(`upload: ${tc.filename} → ${tc.expectedStatus}`, async () => {
// ...
})
}Storage Abstraction for Testability
Abstract storage behind an interface to swap in test doubles:
// services/storage.js
export class S3Storage {
async upload(buffer, key) { /* real S3 */ }
async getSignedUrl(key) { /* real S3 */ }
async delete(key) { /* real S3 */ }
}
export class MemoryStorage {
constructor() { this.files = new Map() }
async upload(buffer, key) {
this.files.set(key, buffer)
return `memory://${key}`
}
async getSignedUrl(key) {
return `http://localhost/files/${key}`
}
async delete(key) {
this.files.delete(key)
}
get(key) { return this.files.get(key) }
}
// In tests
const storage = new MemoryStorage()
const uploader = new FileUploadService(storage)
test('stores uploaded file', async () => {
const buffer = await createTestImage()
const url = await uploader.processAndStore(buffer, 'photo.jpg')
expect(url).toBeTruthy()
expect(storage.files.size).toBe(1)
})CDN and Delivery Testing
describe('Media delivery headers', () => {
test.each([
['/images/photo-abc123.jpg', 'public, max-age=31536000, immutable'],
['/videos/segment0.ts', 'public, max-age=31536000, immutable'],
['/live/room1/index.m3u8', 'no-cache, no-store'],
['/uploads/user-123/doc.pdf', 'private, max-age=3600'],
])('%s has cache-control: %s', async (path, expectedCacheControl) => {
const response = await request(app).get(path)
expect(response.headers['cache-control']).toBe(expectedCacheControl)
})
})Test Fixtures Strategy
For binary media tests, keep test fixtures minimal:
// Generate test fixtures programmatically
beforeAll(async () => {
// 2-second silent WAV
await execFile('ffmpeg', ['-f', 'lavfi', '-i', 'anullsrc', '-t', '2', 'test.wav'])
// 2-second black MP4
await execFile('ffmpeg', ['-f', 'lavfi', '-i', 'color=black:s=320x240', '-t', '2', 'test.mp4'])
})Or use Sharp's create API for images and store only the smallest necessary test files in tests/fixtures/.
Summary
Media application testing strategy:
- Abstract storage — swap real S3 with in-memory storage in unit tests
- Assert on metadata — format, dimensions, duration — not bytes
- Generate test media programmatically — don't commit large binary fixtures
- Test security invariants — file type validation, size limits, magic bytes
- Use Playwright fake media —
--use-fake-device-for-media-streamfor camera/mic - Test cache headers — immutable content vs dynamic manifests