GDPR Testing Guide: How to Test Data Privacy Compliance

GDPR Testing Guide: How to Test Data Privacy Compliance

GDPR testing is not a single audit — it's ongoing verification that your application handles personal data according to the regulation's requirements. This guide covers what to test, how to structure tests for the major GDPR obligations, and how to integrate compliance testing into your development workflow.

What GDPR Testing Covers

GDPR imposes obligations in several areas that require functional testing:

  1. Lawful basis for processing: Is there consent, contract necessity, or legitimate interest for each data operation?
  2. Consent management: Are consent flows valid? Can users withdraw consent easily?
  3. Data subject rights: Can users access, correct, delete, and export their data?
  4. Data minimization: Does the system collect only what's necessary?
  5. Data retention: Is data deleted after the retention period?
  6. Privacy by design: Are privacy controls built in, not bolted on?
  7. Breach notification: Can you detect and report breaches within 72 hours?
  8. Third-party processors: Are your vendor agreements and data flows documented and tested?

Consent under GDPR must be freely given, specific, informed, and unambiguous. Pre-ticked boxes, bundled consent, and dark patterns are all violations.

Test cases for consent forms:

Feature: Cookie consent
  
  Scenario: Initial page load shows consent banner
    Given a new user visits the site
    Then a consent banner is displayed
    And the banner has Accept and Decline options
    And no tracking cookies are set before consent

  Scenario: Consent is granular
    When the user opens cookie preferences
    Then they can separately accept or decline:
      | Category      | Toggleable |
      | Essential     | No (always on) |
      | Analytics     | Yes |
      | Marketing     | Yes |

  Scenario: Declining consent prevents tracking
    Given the user clicks Decline
    Then no analytics cookies are set
    And no marketing pixels load
    And the page remains fully functional

  Scenario: Withdrawing consent is as easy as giving it
    Given the user has previously accepted all cookies
    When the user opens cookie preferences
    Then they can withdraw consent with a single action
    And tracking stops immediately after withdrawal

  Scenario: Consent is not required for service access
    Given a new user visits the site
    When the user declines all non-essential cookies
    Then the user can access all core features
    And the user is not shown a paywall or degraded experience

Automated test for cookie behavior:

// tests/gdpr/consent.test.js
const { chromium } = require('@playwright/test');

test('no tracking cookies before consent', async ({ page }) => {
  await page.goto('/');
  
  // Before interacting with consent banner
  const cookies = await page.context().cookies();
  const trackingCookies = cookies.filter(c => 
    ['_ga', '_gid', 'fbp', '_fbp', 'intercom-id'].includes(c.name)
  );
  
  expect(trackingCookies).toHaveLength(0);
});

test('tracking cookies set after consent', async ({ page }) => {
  await page.goto('/');
  await page.click('[data-testid="accept-all-cookies"]');
  
  const cookies = await page.context().cookies();
  const analyticsCookies = cookies.filter(c => c.name.startsWith('_ga'));
  expect(analyticsCookies.length).toBeGreaterThan(0);
});

test('no tracking after decline', async ({ page }) => {
  await page.goto('/');
  await page.click('[data-testid="decline-cookies"]');
  
  const cookies = await page.context().cookies();
  const trackingCookies = cookies.filter(c => 
    ['_ga', '_gid', 'fbp'].includes(c.name)
  );
  expect(trackingCookies).toHaveLength(0);
});

Testing Data Subject Rights

GDPR gives individuals rights over their data. Test each one.

Right of Access (Article 15)

Users can request all personal data you hold about them.

test('data subject access request returns complete data', async ({ request }) => {
  const response = await request.post('/api/gdpr/access-request', {
    headers: { Authorization: `Bearer ${userToken}` }
  });
  
  expect(response.ok()).toBe(true);
  
  const data = await response.json();
  
  // Verify all expected data categories are present
  expect(data).toMatchObject({
    profile: expect.objectContaining({
      email: expect.any(String),
      name: expect.any(String),
      createdAt: expect.any(String),
    }),
    activityLog: expect.any(Array),
    preferences: expect.any(Object),
  });
  
  // Verify no unexpected internal fields are included
  expect(data.passwordHash).toBeUndefined();
  expect(data.internalUserId).toBeUndefined();
});

test('access request includes processing purposes', async ({ request }) => {
  const response = await request.post('/api/gdpr/access-request', {
    headers: { Authorization: `Bearer ${userToken}` }
  });
  
  const data = await response.json();
  
  // GDPR requires disclosure of purposes, retention periods, and recipients
  expect(data.processingInfo).toBeDefined();
  expect(data.processingInfo.purposes).toContain('service provision');
  expect(data.processingInfo.retentionPeriod).toBeDefined();
});

Right to Erasure (Article 17)

Users can request deletion of their personal data.

test('erasure request deletes all personal data', async ({ request }) => {
  // Create test user
  const user = await createTestUser({ email: 'delete-me@test.com' });
  
  // Submit erasure request
  const deleteResponse = await request.delete('/api/gdpr/erasure', {
    headers: { Authorization: `Bearer ${user.token}` }
  });
  expect(deleteResponse.status()).toBe(200);
  
  // Verify data is gone
  const verifyResponse = await request.get(`/api/admin/users/${user.id}`, {
    headers: { Authorization: `Bearer ${adminToken}` }
  });
  expect(verifyResponse.status()).toBe(404);
  
  // Verify pseudonymization/anonymization of retained records
  const orderResponse = await request.get(`/api/admin/orders?userId=${user.id}`, {
    headers: { Authorization: `Bearer ${adminToken}` }
  });
  const orders = await orderResponse.json();
  // Orders may be retained for legal/accounting reasons, but must be anonymized
  orders.forEach(order => {
    expect(order.email).toBeNull();
    expect(order.name).toBe('[deleted]');
  });
});

test('erasure request is logged for audit trail', async ({ request }) => {
  const user = await createTestUser();
  
  await request.delete('/api/gdpr/erasure', {
    headers: { Authorization: `Bearer ${user.token}` }
  });
  
  // Audit log should record the erasure (without storing the deleted data)
  const auditLog = await getAuditLog({ action: 'erasure', userId: user.id });
  expect(auditLog).toBeDefined();
  expect(auditLog.timestamp).toBeDefined();
  expect(auditLog.requestedBy).toBe(user.id);
});

Right to Portability (Article 20)

Users can export their data in a machine-readable format.

test('data export is in machine-readable format', async ({ request }) => {
  const response = await request.get('/api/gdpr/export', {
    headers: { Authorization: `Bearer ${userToken}` }
  });
  
  expect(response.ok()).toBe(true);
  
  const contentType = response.headers()['content-type'];
  // Must be JSON, CSV, or XML — not proprietary format
  expect(
    contentType.includes('application/json') ||
    contentType.includes('text/csv') ||
    contentType.includes('application/xml')
  ).toBe(true);
  
  // Data should be parseable
  if (contentType.includes('application/json')) {
    const data = await response.json();
    expect(data).toBeDefined();
  }
});

Right to Rectification (Article 16)

Users can correct inaccurate data.

test('user can update their personal data', async ({ page }) => {
  await page.goto('/settings/profile');
  await page.fill('[name="name"]', 'Updated Name');
  await page.click('[type="submit"]');
  
  // Verify change persisted
  await page.reload();
  const name = await page.inputValue('[name="name"]');
  expect(name).toBe('Updated Name');
});

Testing Data Retention

GDPR requires data to be kept no longer than necessary.

// tests/gdpr/retention.test.js

test('inactive accounts are purged after retention period', async () => {
  // Create a user who last logged in 3 years ago
  const user = await createTestUser({
    lastLoginAt: new Date(Date.now() - 3 * 365 * 24 * 60 * 60 * 1000)
  });
  
  // Run the retention job
  await runRetentionJob();
  
  // User should be deleted or anonymized
  const found = await User.findById(user.id);
  expect(found).toBeNull();
});

test('recent users are not purged', async () => {
  const user = await createTestUser({
    lastLoginAt: new Date()
  });
  
  await runRetentionJob();
  
  const found = await User.findById(user.id);
  expect(found).not.toBeNull();
});

test('retention job logs its actions', async () => {
  const logs = await runRetentionJob({ dryRun: false });
  
  expect(logs).toMatchObject({
    processedCount: expect.any(Number),
    deletedCount: expect.any(Number),
    errors: [],
    completedAt: expect.any(Date),
  });
});

Testing Data Minimization

Your API should not return more data than necessary.

test('user list does not expose sensitive fields', async ({ request }) => {
  const response = await request.get('/api/users', {
    headers: { Authorization: `Bearer ${adminToken}` }
  });
  
  const users = await response.json();
  
  users.forEach(user => {
    // These fields should never appear in list responses
    expect(user.passwordHash).toBeUndefined();
    expect(user.twoFactorSecret).toBeUndefined();
    expect(user.socialSecurityNumber).toBeUndefined();
    expect(user.fullBankAccountNumber).toBeUndefined();
  });
});

test('public profile does not expose private data', async ({ request }) => {
  const response = await request.get('/api/profile/johndoe');
  const profile = await response.json();
  
  // Public profiles should only show explicitly public fields
  expect(profile.email).toBeUndefined();
  expect(profile.phone).toBeUndefined();
  expect(profile.address).toBeUndefined();
});

Automated Compliance Scanning

For cookie scanning, use tools like cookie-scanner:

// tests/gdpr/cookie-audit.js
const { CookieScanner } = require('cookie-scanner');

async function auditCookies(url) {
  const scanner = new CookieScanner();
  const results = await scanner.scan(url);
  
  results.cookies.forEach(cookie => {
    // Every cookie should have a documented purpose
    const purpose = COOKIE_REGISTRY[cookie.name];
    if (!purpose) {
      console.warn(`Undocumented cookie: ${cookie.name}`);
    }
    
    // Session cookies should not persist beyond session
    if (purpose?.type === 'session' && cookie.persistent) {
      console.error(`Session cookie ${cookie.name} is persistent`);
    }
    
    // Third-party cookies should have consent
    if (cookie.domain !== 'yourapp.com' && !cookie.hasConsent) {
      console.error(`Third-party cookie ${cookie.name} set without consent`);
    }
  });
}

GDPR Testing Checklist

## Pre-Release GDPR Checklist

### Consent
- [ ] Consent banner shown before any non-essential processing
- [ ] Granular consent per category (analytics, marketing)
- [ ] Pre-ticked boxes not used
- [ ] Declining consent doesn't block core functionality
- [ ] Consent withdrawal is easy and immediate

### Data Subject Rights
- [ ] Access request returns complete personal data
- [ ] Access request includes processing purposes and retention periods
- [ ] Erasure request deletes or anonymizes all personal data
- [ ] Erasure request is logged in audit trail
- [ ] Data export is in machine-readable format
- [ ] Profile update (rectification) works correctly

### Data Handling
- [ ] API responses include only necessary fields
- [ ] Inactive account deletion/anonymization runs correctly
- [ ] Audit logs don't contain unnecessary personal data
- [ ] Error messages don't expose personal data

### Third Parties
- [ ] Third-party scripts load only after consent
- [ ] No undocumented cookies set by third parties

Compliance testing is not a one-time exercise. Run these tests on every release. GDPR violations from software bugs — not just policy decisions — have resulted in regulatory fines.

Read more