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.
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:
| Version | Algorithm | When to use |
|---|---|---|
| v1 | Time + MAC address | Avoid — reveals machine identity and creation time |
| v4 | Random | Standard for test data — no information leak, statistically unique |
| v5 | SHA-1 hash of namespace + name | Deterministic — same input always produces same UUID |
| v7 | Time-ordered random | Useful for time-ordered IDs; increasingly common in modern systems |
| Nil | All zeros | Special 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 emailGenerating 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:
| Format | Example | Used by |
|---|---|---|
| Standard | 550e8400-e29b-41d4-a716-446655440000 | Most APIs, PostgreSQL |
| No hyphens | 550e8400e29b41d4a716446655440000 | Some databases, compact storage |
| Uppercase | 550E8400-E29B-41D4-A716-446655440000 | Some .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 sameCommon 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.
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.
Discussion
Sign in with GitHub to comment · powered by Giscus