Media Application Testing: Video, Audio, Images, and File Uploads

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 bitrate

Each 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 data

Video 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:

  1. Abstract storage — swap real S3 with in-memory storage in unit tests
  2. Assert on metadata — format, dimensions, duration — not bytes
  3. Generate test media programmatically — don't commit large binary fixtures
  4. Test security invariants — file type validation, size limits, magic bytes
  5. Use Playwright fake media--use-fake-device-for-media-stream for camera/mic
  6. Test cache headers — immutable content vs dynamic manifests

Read more