HIPAA Software Testing Guide: Testing for Healthcare Data Compliance
HIPAA (Health Insurance Portability and Accountability Act) imposes specific technical safeguards on software that handles Protected Health Information (PHI). Testing for HIPAA compliance means verifying these safeguards work as designed — not just documenting that policies exist.
This guide focuses on what to test, how to structure those tests, and how to integrate compliance testing into your development workflow.
Note: HIPAA compliance involves legal, administrative, and technical dimensions. This guide covers the software testing aspects. Consult your legal and compliance team for the full compliance picture.
What Counts as PHI
Protected Health Information includes any individually identifiable health information. In software:
- Name + any health data
- Social Security Number
- Date of birth with health data
- Geographic data smaller than state level with health data
- Medical record numbers
- Health plan beneficiary numbers
- Diagnoses, treatments, medications
- Lab results
- Insurance information
Test data should either use synthetic data or properly de-identified data. Using real patient records in test environments is a HIPAA violation.
Access Control Testing (Technical Safeguard §164.312(a))
HIPAA requires unique user identification and automatic logoff.
// tests/hipaa/access-control.test.js
test('each user has a unique identifier — no shared accounts', async ({ request }) => {
const users = await request.get('/api/admin/users', {
headers: { Authorization: `Bearer ${adminToken}` }
});
const data = await users.json();
const ids = data.map(u => u.id);
const uniqueIds = new Set(ids);
expect(uniqueIds.size).toBe(ids.length);
// No test accounts or shared accounts
const sharedAccounts = data.filter(u =>
u.email.includes('shared') || u.email.includes('generic')
);
expect(sharedAccounts).toHaveLength(0);
});
test('session expires after inactivity', async ({ page }) => {
await loginAsUser(page);
await page.goto('/patient-records');
// Simulate inactivity by waiting (or manipulating session)
await simulateInactivity(page, 15 * 60 * 1000); // 15 minutes
// Attempt to access PHI
await page.goto('/patient-records');
// Should be redirected to login
expect(page.url()).toContain('/login');
});
test('role-based access prevents unauthorized PHI access', async ({ request }) => {
// Billing staff should not access clinical notes
const response = await request.get('/api/patients/123/clinical-notes', {
headers: { Authorization: `Bearer ${billingStaffToken}` }
});
expect(response.status()).toBe(403);
// Clinical staff can access clinical notes
const clinicalResponse = await request.get('/api/patients/123/clinical-notes', {
headers: { Authorization: `Bearer ${clinicalStaffToken}` }
});
expect(clinicalResponse.ok()).toBe(true);
});
test('emergency access override is logged', async ({ request }) => {
// Emergency access ("break glass") should work but must be logged
const response = await request.post('/api/emergency-access', {
headers: { Authorization: `Bearer ${emergencyToken}` },
data: { patientId: '123', reason: 'Emergency care required' }
});
expect(response.ok()).toBe(true);
// Verify the override was logged
const auditLog = await getAuditLog({ action: 'emergency_access', patientId: '123' });
expect(auditLog).toBeDefined();
expect(auditLog.userId).toBeDefined();
expect(auditLog.reason).toBe('Emergency care required');
});Audit Control Testing (Technical Safeguard §164.312(b))
HIPAA requires audit logs for all PHI access.
test('all PHI access is logged', async ({ request }) => {
const patientId = 'test-patient-123';
// Clear audit log for this patient (in test environment)
await clearAuditLog(patientId);
// Access patient record
await request.get(`/api/patients/${patientId}`, {
headers: { Authorization: `Bearer ${clinicalToken}` }
});
// Verify access was logged
const logs = await getAuditLog({ resourceId: patientId });
expect(logs).toHaveLength(1);
expect(logs[0]).toMatchObject({
action: 'read',
resource: 'patient',
resourceId: patientId,
userId: expect.any(String),
timestamp: expect.any(String),
ipAddress: expect.any(String),
});
});
test('PHI modification is logged with before/after values', async ({ request }) => {
const patientId = 'test-patient-456';
await request.patch(`/api/patients/${patientId}`, {
headers: { Authorization: `Bearer ${clinicalToken}` },
data: { diagnosis: 'Updated diagnosis' }
});
const logs = await getAuditLog({ resourceId: patientId, action: 'update' });
expect(logs[0]).toMatchObject({
action: 'update',
changes: expect.arrayContaining([
expect.objectContaining({ field: 'diagnosis' })
])
});
});
test('failed access attempts are logged', async ({ request }) => {
await request.get('/api/patients/999', {
headers: { Authorization: `Bearer ${unauthorizedToken}` }
});
const logs = await getAuditLog({ action: 'access_denied' });
const recentDenial = logs.find(l => l.resource === 'patient');
expect(recentDenial).toBeDefined();
expect(recentDenial.userId).toBeDefined();
});
test('audit logs are tamper-evident', async ({ request }) => {
// Audit logs should be append-only — no update or delete endpoints
const response = await request.delete('/api/audit-logs/1', {
headers: { Authorization: `Bearer ${adminToken}` }
});
expect(response.status()).toBe(405); // Method Not Allowed
const patchResponse = await request.patch('/api/audit-logs/1', {
headers: { Authorization: `Bearer ${adminToken}` },
data: { action: 'modified_action' }
});
expect(patchResponse.status()).toBe(405);
});Integrity Testing (Technical Safeguard §164.312(c))
PHI must not be altered or destroyed improperly.
test('PHI has cryptographic integrity check', async ({ request }) => {
const response = await request.get('/api/patients/123/record', {
headers: { Authorization: `Bearer ${clinicalToken}` }
});
const data = await response.json();
// Response should include integrity hash
expect(data.checksum).toBeDefined();
// Verify the checksum matches the data
const computedChecksum = computeChecksum(data.content);
expect(data.checksum).toBe(computedChecksum);
});
test('corrupted data is detected', async () => {
// Store a patient record with a known hash
const record = await storePatientRecord({ data: 'original data' });
// Simulate corruption in the database
await corruptRecord(record.id);
// Retrieval should detect corruption and fail
await expect(retrievePatientRecord(record.id))
.rejects.toThrow('integrity check failed');
});Transmission Security Testing (Technical Safeguard §164.312(e))
PHI must be protected during transmission.
test('PHI is only transmitted over HTTPS', async ({ request }) => {
// HTTP requests to the API should be rejected or redirected
const httpResponse = await fetch('http://api.example.com/patients/123', {
headers: { Authorization: `Bearer ${clinicalToken}` }
});
// Either rejected (400) or redirected to HTTPS
expect(
httpResponse.status === 400 ||
httpResponse.url.startsWith('https://')
).toBe(true);
});
test('API does not return PHI in error messages', async ({ request }) => {
// Invalid request should return generic error, not patient data
const response = await request.get('/api/patients/invalid-id', {
headers: { Authorization: `Bearer ${clinicalToken}` }
});
const error = await response.json();
expect(error.message).not.toContain('John');
expect(error.message).not.toContain('diagnosis');
expect(error.message).not.toContain('SSN');
});
test('response headers do not expose PHI', async ({ request }) => {
const response = await request.get('/api/patients/123', {
headers: { Authorization: `Bearer ${clinicalToken}` }
});
const headers = response.headers();
// No PHI in debug headers or error headers
Object.entries(headers).forEach(([key, value]) => {
expect(value).not.toMatch(/\d{3}-\d{2}-\d{4}/); // SSN pattern
expect(value).not.toMatch(/diagnosis|treatment|medication/i);
});
});Encryption at Rest Testing
test('PHI fields are encrypted in database', async () => {
// Write a patient record
const patient = await createPatient({
ssn: '123-45-6789',
diagnosis: 'Test diagnosis'
});
// Read directly from database (bypassing application layer)
const dbRecord = await db.query(
'SELECT ssn, diagnosis FROM patients WHERE id = $1',
[patient.id]
);
// Values should be encrypted (not plaintext)
expect(dbRecord.ssn).not.toBe('123-45-6789');
expect(dbRecord.diagnosis).not.toBe('Test diagnosis');
// Should be decryptable through the application
const fetched = await getPatient(patient.id);
expect(fetched.ssn).toBe('123-45-6789');
expect(fetched.diagnosis).toBe('Test diagnosis');
});Test Data Management
A common HIPAA testing mistake: using real patient data in test environments.
Synthetic data generation:
// test-helpers/generate-phi.js
const { faker } = require('@faker-js/faker');
function generateSyntheticPatient() {
return {
id: `TEST-${faker.string.uuid()}`,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
dateOfBirth: faker.date.birthdate({ min: 18, max: 90, mode: 'age' }).toISOString(),
ssn: `TEST-${faker.string.numeric(9)}`, // Prefix makes it obviously fake
diagnosis: faker.helpers.arrayElement([
'Hypertension (test)',
'Type 2 Diabetes (test)',
'Asthma (test)',
]),
// Mark all test data clearly
testData: true,
createdBy: 'test-harness',
};
}Synthetic data is functionally equivalent for testing but doesn't create HIPAA liability.
HIPAA Testing Checklist
## Pre-Release HIPAA Technical Safeguards Checklist
### Access Controls
- [ ] Unique user IDs enforced (no shared accounts)
- [ ] Automatic session timeout implemented and tested
- [ ] Role-based access controls tested for each role
- [ ] Emergency access override works and is logged
### Audit Controls
- [ ] All PHI reads are logged with user, timestamp, IP
- [ ] All PHI writes are logged with before/after values
- [ ] Failed access attempts are logged
- [ ] Audit logs are append-only (no modification/deletion)
### Integrity
- [ ] PHI records have integrity verification
- [ ] Corruption is detected on retrieval
### Transmission Security
- [ ] API only accessible over HTTPS
- [ ] HTTP requests rejected or redirected
- [ ] Error messages do not expose PHI
- [ ] Response headers do not contain PHI
### Encryption
- [ ] PHI fields encrypted at rest
- [ ] Encryption keys managed separately from data
- [ ] Decryption only through authorized application layer
### Test Data
- [ ] No real patient data in test environments
- [ ] Synthetic data clearly marked as test data
- [ ] Test data purged from non-production environments on scheduleHIPAA compliance testing doesn't happen once at launch — it should run continuously as the codebase evolves. Any change to data models, API endpoints, access control logic, or audit logging warrants re-running the compliance test suite.