Skip to main content
Back to blog

Test Data Management in Azure DevOps Pipelines

How to manage test data in Azure DevOps CI/CD pipelines. Covers database seeding, factory patterns, environment isolation, data cleanup strategies, and using Azure Key Vault for sensitive test credentials.

InnovateBits4 min read
Share

Test data management is the unglamorous but critical work that determines whether your automated test suite is reliable or flaky. In Azure DevOps pipelines, it requires deliberate design: data must be available, isolated, and cleaned up.


The four test data problems in CI

  1. Stale data: tests depend on records that were manually created and may change
  2. Shared data conflicts: parallel tests read/write the same records
  3. Leftover data: failed tests leave records that cause subsequent runs to fail
  4. Sensitive data: production credentials or PII in test fixtures

Strategy 1: Database seeding in pipelines

Seed the test database at the start of each pipeline run:

stages:
  - stage: PrepareEnvironment
    jobs:
      - job: SeedDatabase
        steps:
          - script: npm ci
          - script: npm run db:reset     # Drop and recreate test database
            env:
              DATABASE_URL: $(TEST_DB_URL)
          - script: npm run db:migrate   # Apply schema migrations
            env:
              DATABASE_URL: $(TEST_DB_URL)
          - script: npm run db:seed      # Insert baseline test data
            env:
              DATABASE_URL: $(TEST_DB_URL)
 
  - stage: Tests
    dependsOn: PrepareEnvironment
    jobs:
      - job: RunTests
        steps:
          - script: npx playwright test
            env:
              DATABASE_URL: $(TEST_DB_URL)
              BASE_URL: $(STAGING_URL)

The seed script populates known, stable data. Tests that need dynamic data create it themselves.

Example seed script

// scripts/db-seed.ts
import { db } from '../src/database'
 
async function seed() {
  // Create base users (stable across runs)
  await db.users.upsert({
    where: { email: 'admin@test.com' },
    create: {
      id: '00000000-0000-0000-0000-000000000001',
      email: 'admin@test.com',
      role: 'admin',
      name: 'Test Admin'
    },
    update: {}
  })
 
  // Create test products
  await db.products.createMany({
    data: SEED_PRODUCTS,
    skipDuplicates: true
  })
 
  console.log('Seed complete')
}
 
seed().catch(console.error).finally(() => db.$disconnect())

Strategy 2: Factory pattern for dynamic data

Each test creates and destroys its own data:

// tests/helpers/factory.ts
import { randomUUID } from 'crypto'
 
export class UserFactory {
  private createdIds: string[] = []
 
  async create(overrides = {}) {
    const userData = {
      id: randomUUID(),
      email: `test-${randomUUID()}@example.com`,
      name: 'Test User',
      role: 'member',
      ...overrides
    }
 
    const res = await fetch(`${process.env.API_URL}/admin/users`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.ADMIN_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(userData)
    })
 
    const user = await res.json()
    this.createdIds.push(user.id)
    return user
  }
 
  async cleanup() {
    for (const id of this.createdIds) {
      await fetch(`${process.env.API_URL}/admin/users/${id}`, {
        method: 'DELETE',
        headers: { 'Authorization': `Bearer ${process.env.ADMIN_TOKEN}` }
      }).catch(err => console.warn(`Cleanup failed for ${id}: ${err.message}`))
    }
    this.createdIds = []
  }
}
// In tests
test.beforeEach(async () => {
  userFactory = new UserFactory()
})
 
test.afterEach(async () => {
  await userFactory.cleanup()
})
 
test('Admin can view user details', async ({ request }) => {
  const user = await userFactory.create({ role: 'member' })
  const res = await request.get(`/api/users/${user.id}`)
  expect(res.status()).toBe(200)
})

Storing test credentials securely

Never hardcode credentials in test code or YAML. Use Azure Key Vault with a service connection:

steps:
  - task: AzureKeyVault@2
    displayName: Load test secrets
    inputs:
      azureSubscription: $(AZURE_SERVICE_CONNECTION)
      KeyVaultName: 'myapp-test-keyvault'
      SecretsFilter: 'TEST-DB-URL, TEST-ADMIN-TOKEN, STAGING-URL'
      RunAsPreJob: true
 
  - script: npx playwright test
    env:
      DATABASE_URL: $(TEST-DB-URL)          # Loaded from Key Vault
      ADMIN_TOKEN: $(TEST-ADMIN-TOKEN)
      BASE_URL: $(STAGING-URL)

Pre-run cleanup for resilience

Even with good cleanup, CI failures can leave orphaned test data. Add a pre-run cleanup:

// scripts/cleanup-test-data.ts
async function cleanupTestData() {
  const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000) // 24 hours ago
 
  // Delete users created by tests (identified by email pattern)
  const deleted = await db.users.deleteMany({
    where: {
      email: { contains: '@example.com' },
      createdAt: { lt: cutoff }
    }
  })
  console.log(`Cleaned up ${deleted.count} stale test users`)
}

Add this to your pipeline's preparation stage.


Common errors and fixes

Error: Database seed fails because tables don't exist yet Fix: Run db:migrate before db:seed. Migrations create the schema; seeds populate data. The order matters.

Error: Parallel test jobs conflict on shared test data Fix: Use the factory pattern with UUID-based identifiers. Each test creates its own records and doesn't depend on shared data.

Error: Test data cleanup fails silently and data accumulates Fix: Log cleanup results explicitly. Monitor the test database size over time. If it grows between pipeline runs, cleanup is failing. Use a pre-run cleanup as a safety net.

Error: Key Vault access fails in pipeline with "Forbidden" Fix: The service connection's service principal must have "Key Vault Secrets User" role on the Key Vault. Grant this in the Azure portal under Key Vault → Access control (IAM).

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.

Discussion

Sign in with GitHub to comment · powered by Giscus