Back to blog
Test Automation#uuid#test-data#test-automation#database-testing#qa-tools#unique-identifiers

UUIDs in Test Automation: Why, When, and How to Use Them

A practical guide to using UUIDs in your test automation suite. Learn what UUID versions mean, why UUID v4 is the standard for test data, how to generate them in bulk, and common pitfalls when using UUIDs in database-backed tests.

InnovateBits6 min read

A UUID (Universally Unique Identifier) is a 128-bit value formatted as 32 hexadecimal digits in five groups: 550e8400-e29b-41d4-a716-446655440000. It's the standard solution to a fundamental testing problem: how do you create test records that are guaranteed not to conflict with existing data or with other tests running in parallel?

This guide explains when and how to use UUIDs effectively in test automation, covering the different versions, generation strategies, and common mistakes.


Why UUIDs matter in test automation

The core problem: tests that create database records need unique identifiers. If two tests create a user with id = 1, or two parallel test runs create a product with sku = "TEST-001", they collide — one test overwrites or conflicts with the other.

Sequential IDs (auto-incremented integers) depend on the current database state. If the database was reset at different points in different environments, the same test will create records with different IDs. Any test that hardcodes an expected ID will be environment-specific.

UUIDs solve both problems. They're generated independently of the database, their uniqueness is statistical rather than sequential (the probability of two random UUID v4s colliding is approximately 1 in 5.3 × 10^36), and they work identically across all environments.


UUID versions: what they mean

There are eight standardised UUID versions. In test automation, only a few matter:

VersionAlgorithmWhen to use
v1Time + MAC addressAvoid — reveals machine identity and creation time
v4RandomStandard for test data — no information leak, statistically unique
v5SHA-1 hash of namespace + nameDeterministic — same input always produces same UUID
v7Time-ordered randomUseful for time-ordered IDs; increasingly common in modern systems
NilAll zerosSpecial case: 00000000-0000-0000-0000-000000000000

UUID v4 is the right choice for test data IDs in almost all cases. It requires no coordination, contains no sensitive information, and is supported by every language and database.

UUID v5 is useful for deterministic test data — when you need the same UUID for the same input across different test runs. For example, a test that creates a user with email alice@test.com always needs the same UUID for that user:

import { v5 as uuidv5 } from 'uuid'
 
const NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8' // DNS namespace
const userId = uuidv5('alice@test.com', NAMESPACE)
// Always: 'a987fbc9-4bed-3078-cf07-9141ba07c9f3' for this email

Generating UUIDs in tests

In the browser (modern)

// Available in all modern browsers and Node.js 14.17+
const id = crypto.randomUUID()
// "550e8400-e29b-41d4-a716-446655440000"

In Node.js

// Built-in — no package needed in Node.js 14.17+
import { randomUUID } from 'crypto'
const id = randomUUID()
 
// Or use the uuid package for version options
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid'
const id = uuidv4()

Bulk generation for test fixtures

The UUID Generator tool generates up to 500 UUIDs at once and lets you download them as a text file. This is useful for pre-populating fixture files with stable IDs:

// tests/fixtures/users.json — stable UUIDs for predictable test state
[
  { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "Alice Chen", "role": "admin" },
  { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "name": "Bob Smith",  "role": "member" },
  { "id": "a87ff679-a2f3-461d-a6ef-53a89dd812ef", "name": "Carol López", "role": "viewer" }
]

UUID formats across different systems

Different systems expect UUIDs in different formats. The UUID Generator tool supports all common formats:

FormatExampleUsed by
Standard550e8400-e29b-41d4-a716-446655440000Most APIs, PostgreSQL
No hyphens550e8400e29b41d4a716446655440000Some databases, compact storage
Uppercase550E8400-E29B-41D4-A716-446655440000Some .NET and Windows systems
Braces{550e8400-e29b-41d4-a716-446655440000}Microsoft SQL Server, COM

When writing cross-system tests, normalise UUIDs before comparing:

function normaliseUuid(id: string): string {
  return id.toLowerCase().replace(/[{}]/g, '').replace(/-/g, '')
}
 
// These all represent the same UUID
const ids = [
  '550e8400-e29b-41d4-a716-446655440000',
  '550E8400-E29B-41D4-A716-446655440000',
  '{550e8400-e29b-41d4-a716-446655440000}',
  '550e8400e29b41d4a716446655440000',
]
expect(new Set(ids.map(normaliseUuid)).size).toBe(1) // all the same

Common UUID mistakes in test automation

1. Using the same UUID across tests

Reusing a UUID from a previous test run is tempting when you want predictable IDs. But if Test A creates a user with id = "abc..." and doesn't clean up, Test B that also tries to create a user with id = "abc..." will fail with a uniqueness constraint violation.

Always generate fresh UUIDs per test, or use fixtures with stable IDs that are reset between runs.

// Bad — reuses same UUID, breaks if record already exists
const USER_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
 
// Good — fresh UUID per test run
const USER_ID = crypto.randomUUID()

2. Not testing the UUID format returned by the API

APIs sometimes return IDs in an unexpected format. Assert the format explicitly:

const response = await request.post('/api/users', { data: userData })
const { id } = await response.json()
 
// Assert it's a valid UUID v4
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i)

3. Assuming UUID generation is cryptographically secure everywhere

Math.random() based UUID generation (common in older libraries) is not cryptographically secure. For test data IDs this doesn't matter, but for security tokens or session identifiers that happen to use UUIDs, assert that the API uses a secure generation method.

4. Comparing UUIDs with the wrong case sensitivity

A UUID stored as 550E8400-E29B-... won't match 550e8400-e29b-... with a strict equality check. Always normalise case before comparing:

expect(responseId.toLowerCase()).toBe(expectedId.toLowerCase())

UUIDs in parallel test execution

Parallel test execution is where UUIDs pay the biggest dividend. When 8 test workers each need to create their own user, 8 UUID v4s will never conflict — no coordination required.

// playwright.config.ts
export default defineConfig({
  workers: 8, // All workers create their own test data with UUIDs — no conflicts
})
 
// In each test worker
test('user can update profile', async ({ request }) => {
  const userId = crypto.randomUUID()
  
  // Create unique user for this test
  await request.post('/api/users', {
    data: { id: userId, name: 'Test User', email: `test-${userId}@example.com` }
  })
  
  // Run the test
  const updateRes = await request.put(`/api/users/${userId}`, {
    data: { name: 'Updated Name' }
  })
  expect(updateRes.status()).toBe(200)
  
  // Cleanup
  await request.delete(`/api/users/${userId}`)
})

UUID-based test data traceability

In long-running test environments, UUIDs make it easy to trace which records were created by which test run. Prefix the UUID with a test run identifier:

// Using a consistent prefix makes CI-created records identifiable
const CI_PREFIX = process.env.CI ? `ci-${process.env.GITHUB_RUN_ID ?? 'local'}` : 'local'
 
function testId(label: string): string {
  return `${CI_PREFIX}-${label}-${crypto.randomUUID()}`
}
 
const orderId = testId('order')
// "ci-9087654321-order-f47ac10b-58cc-4372-a567-0e02b2c3d479"

This makes it trivial to query and clean up records left by CI runs after a test environment is shared across multiple pipelines.

Free newsletter

Stay ahead in AI-driven QA

Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.