Your VAT API integration has four distinct code paths — one for each status code. Without a deliberate testing strategy, most codebases only test the happy path (`active`) and discover the `service_unavailable` handling is broken when VIES actually goes down in production. This guide covers how to test all four paths without making real API calls.
Tip
TaxID provides test API keys (`vat_test_...`) that return predictable responses without consuming your monthly quota. Use your test key in CI/CD and local development, your live key only in production.
Test VAT Numbers
When using a test API key, send specific VAT numbers to trigger each response state:
| VAT number | Response status | Use in tests for |
|---|---|---|
| DE000000001 | active | Happy path — valid VAT, zero-rate applied |
| DE000000002 | inactive | Reject zero-rate, charge VAT |
| DE000000003 | format_invalid | Bad input handling |
| DE000000004 | service_unavailable | VIES outage fallback |
Node.js / TypeScript: Mock with Vitest
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { validateVat } from './vat';
const mockFetch = vi.spyOn(global, 'fetch');
function stubVatResponse(status: string, companyName: string | null = null) {
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
valid: status === 'active',
status,
company_name: companyName,
address: companyName ? 'Test Street 1' : null,
cached: false,
request_id: `test-${status}`,
}),
} as Response);
}
beforeEach(() => vi.clearAllMocks());
describe('validateVat', () => {
it('handles active status', async () => {
stubVatResponse('active', 'Test GmbH');
const r = await validateVat('DE123456789');
expect(r.status).toBe('active');
expect(r.company_name).toBe('Test GmbH');
});
it('handles inactive status', async () => {
stubVatResponse('inactive');
const r = await validateVat('DE000000000');
expect(r.valid).toBe(false);
expect(r.status).toBe('inactive');
});
it('handles service_unavailable without throwing', async () => {
stubVatResponse('service_unavailable');
const r = await validateVat('DE123456789');
expect(r.status).toBe('service_unavailable');
expect(r.valid).toBe(false);
});
it('returns unavailable on network error', async () => {
mockFetch.mockRejectedValueOnce(new Error('Network error'));
// Your error-handled wrapper should catch and return unavailable
const r = await validateVatSafe('DE123456789'); // uses try/catch wrapper
expect(r.status).toBe('service_unavailable');
});
});Python: Mock with unittest.mock
import pytest
from unittest.mock import patch, MagicMock
def mock_vat_response(status: str, company_name=None):
m = MagicMock()
m.json.return_value = {
'valid': status == 'active',
'status': status,
'company_name': company_name,
'request_id': f'test-{status}',
}
m.raise_for_status = MagicMock()
return m
@patch('vat_client.requests.get')
def test_active_vat(mock_get):
mock_get.return_value = mock_vat_response('active', 'Test GmbH')
result = validate_vat_safe('DE123456789')
assert result['status'] == 'active'
assert result['company_name'] == 'Test GmbH'
@patch('vat_client.requests.get')
def test_service_unavailable(mock_get):
mock_get.return_value = mock_vat_response('service_unavailable')
result = validate_vat_safe('DE123456789')
assert result['status'] == 'service_unavailable'
assert not result['valid']
@patch('vat_client.requests.get', side_effect=Exception('timeout'))
def test_network_error_returns_unavailable(mock_get):
result = validate_vat_safe('DE123456789')
assert result['status'] == 'service_unavailable'Testing the Full Checkout Flow
Unit tests for the API call function are necessary but not sufficient. Also write integration tests that exercise your full checkout or invoice creation flow with each status code. These tests should verify that: (1) `active` results in zero-rate applied and company name stored; (2) `inactive` returns a user-facing validation error; (3) `service_unavailable` charges standard VAT and queues for re-validation; (4) network errors do not crash the checkout.
Related guides
Start validating EU VAT numbers
Free plan — 100 validations/month. No credit card required.