Environment-Dependent Behavior - The Hidden Time Bomb
Code that behaves differently across environments creates:
- False confidence - Tests pass but production fails
- Impossible debugging - Works locally, breaks in production
- Security vulnerabilities - Protection only in some environments
- Silent failures - Different behavior with no warnings
// BAD: Validation only in development
if (WP_DEBUG) {
if (!is_email($user->user_email)) {
return false;
}
}
// Sends to invalid emails in production!
return wp_mail($user->user_email, $subject, $message);What happens:
- ✅ Development:
WP_DEBUG = true→ Validation runs → Works fine - ❌ Staging:
WP_DEBUG = false→ No validation → Sometimes fails - ❌ Production:
WP_DEBUG = false→ No validation → Silent failures - ✅ Tests:
WP_DEBUG = true→ Validation runs → All pass
Result: Tests pass, production fails for 15 months!
The Code:
function send_email_notification($user_id, $subject, $message) {
$user = get_user_by('id', $user_id);
// Only validate in development
if (WP_DEBUG) {
if (!is_email($user->user_email)) {
error_log("Invalid email for user {$user_id}");
return false;
}
}
return wp_mail($user->user_email, $subject, $message);
}Timeline:
- Dev: Clean test data, all emails valid → ✅ Works
- Tests: Mock data, all emails valid → ✅ Pass
- Production: Legacy users with invalid emails → ❌ Fails silently
- 15 months later: Users report missing notifications
- Impact: Thousands of users never received critical emails
The Code:
function api_request($endpoint, $data) {
// Only check rate limit in production
if (!WP_DEBUG && !check_rate_limit(get_current_user_id())) {
return new WP_Error('rate_limit', 'Too many requests');
}
return wp_remote_post(API_URL . $endpoint, ['body' => $data]);
}What happened:
- Dev: No rate limiting → Easy to test → ✅ Happy developers
- Tests: Rate limiting bypassed → ✅ All pass
- Production: Rate limiting enabled but has a bug → ❌ Fails
- Result: Bug not caught until production, $5000 unexpected API bill
The Code:
function process_payment($amount, $card_token) {
// Skip validation in dev
if (!WP_DEBUG) {
if ($amount < 0.50) {
return new WP_Error('amount_too_small', 'Minimum $0.50');
}
if (!validate_card_token($card_token)) {
return new WP_Error('invalid_card', 'Invalid card token');
}
}
return charge_card($amount, $card_token);
}Result:
- Dev: Validation skipped → Can test quickly
- Tests: Validation skipped → Can't test validation logic!
- Production: Validation runs but has bugs → Chargebacks and fraud
public function test_send_email() {
// Test runs with WP_DEBUG = true
$result = send_email_notification(123, 'Test', 'Message');
$this->assertTrue($result);
// ✅ Test passes!
}But in production (WP_DEBUG = false), the code path is completely different!
// SQL injection protection only in production
if (WP_DEBUG) {
$sql = "SELECT * FROM products WHERE name LIKE '%{$search}%'";
} else {
$sql = $wpdb->prepare("SELECT * FROM products WHERE name LIKE %s", $search);
}Security audit discovers:
- Development environment: Vulnerable to SQL injection
- Questions entire codebase security practices
- Even though production is "safe," the practice is condemned
// Error logging completely disabled
try {
process_import($data);
} catch (Exception $e) {
// error_log('Import failed: ' . $e->getMessage());
return false;
}In development: IDE shows exceptions
In production: Silent failures, no logs, impossible to debug
function process_order($order_id) {
$order = wc_get_order($order_id);
// Condition commented out!
// if (WP_DEBUG) {
echo '<pre>';
print_r($order); // Contains payment info!
var_dump($_POST); // Contains card details!
// }
return finalize_order($order);
}Result: Customer payment information exposed to all users!
| Code Behavior | Development | Staging | Production | Tests |
|---|---|---|---|---|
| Validation | ✅ Runs | ❌ Skipped | ❌ Skipped | ✅ Runs |
| Rate Limiting | ❌ Disabled | ✅ Enabled | ✅ Enabled | ❌ Disabled |
| SQL Protection | ❌ Vulnerable | ✅ Protected | ✅ Protected | ❌ Vulnerable |
| Permission Checks | ❌ Skipped | ✅ Enforced | ✅ Enforced | ❌ Skipped |
| File Upload Validation | ❌ Skipped | ✅ Enforced | ✅ Enforced | ❌ Skipped |
| Caching | ❌ Disabled | ❌ Disabled | ❌ Disabled | ❌ Disabled |
| Error Logging | ❌ Disabled | ❌ Disabled | ❌ Disabled | ❌ Disabled |
Every environment runs different code!
// BAD: Different code paths
if (WP_DEBUG) {
// Validation in dev only
}
// GOOD: Same code, different settings
class RateLimiter {
private function get_max_requests(): int {
// Config-driven, not code-driven
return defined('API_RATE_LIMIT') ? API_RATE_LIMIT : 60;
}
}Configuration per environment:
// wp-config.php - Development
define('API_RATE_LIMIT', 1000); // High limit for testing
// wp-config.php - Production
define('API_RATE_LIMIT', 60); // Low limit for safety// GOOD: Always validate
function send_email_notification(int $user_id, string $subject, string $message): bool {
$user = get_user_by('id', $user_id);
// ALWAYS validate - no exceptions!
if (!$user || !is_email($user->user_email)) {
error_log("Invalid email for user {$user_id}");
return false;
}
return wp_mail($user->user_email, $subject, $message);
}// GOOD: Log differently per environment, but always log
function process_order(int $order_id): bool {
$order = wc_get_order($order_id);
// Log to file in dev
if (WP_DEBUG && WP_DEBUG_LOG) {
error_log("Processing order {$order_id}");
}
// Send to monitoring service in production
if (defined('SENTRY_DSN') && !WP_DEBUG) {
sentry_capture_message("Processing order {$order_id}");
}
// No debug output to screen - EVER
return finalize_order($order);
}// BAD: Comments to enable features
function calculate_loyalty_points($amount) {
// return $amount * 1.5; // New algorithm
return $amount * 1.0; // Old algorithm
}
// GOOD: Proper feature flags
function calculate_loyalty_points(float $amount): float {
$features = new FeatureFlags();
if ($features->is_enabled('new_loyalty_algorithm')) {
return $amount * 1.5;
}
return $amount * 1.0;
}// GOOD: Tests run same code as production
class EmailNotificationTest extends WP_UnitTestCase {
public function test_invalid_email_handling() {
// Create user with invalid email
$user_id = $this->factory->user->create([
'user_email' => 'not-an-email'
]);
// Should fail validation
$result = send_email_notification($user_id, 'Test', 'Message');
$this->assertFalse($result);
// This test actually tests production code path!
}
}Wrong: If validation is slow, optimize it. Don't skip it.
Right: Mock the validator, don't disable it globally.
Wrong: Your tests should test rate limiting! Use dependency injection.
Wrong: Use proper logging and debugging tools, not conditional code.
# Search for WP_DEBUG conditions
grep -r "if.*WP_DEBUG" . --include="*.php"
# Search for environment checks
grep -r "WP_ENVIRONMENT_TYPE" . --include="*.php"
# Search for commented conditionals
grep -r "//.*if.*WP_DEBUG" . --include="*.php"- Does this change behavior? → ❌ Remove it
- Does this change settings? → ✅ Use config
- Does this add logging? → ✅ Keep, but log differently
- Does this skip validation? → ❌ Always validate
// wp-config-development.php
define('API_RATE_LIMIT', 1000);
define('CACHE_TTL', 60);
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', true);
// wp-config-production.php
define('API_RATE_LIMIT', 60);
define('CACHE_TTL', 3600);
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);
define('SENTRY_DSN', 'https://...');// Set test environment to match production
class ProductionLikeTest extends WP_UnitTestCase {
protected function setUp(): void {
parent::setUp();
// Temporarily disable WP_DEBUG for this test
$this->wp_debug_original = WP_DEBUG;
if (!defined('WP_DEBUG')) {
define('WP_DEBUG', false);
}
}
protected function tearDown(): void {
// Restore
parent::tearDown();
}
}✅ Same code in all environments
✅ Different config, not different code
✅ Always validate, always protect
✅ Test production code paths
✅ Log differently, never skip logging
✅ Use feature flags, not comments
✅ Never skip security in dev
❌ Don't use if (WP_DEBUG) for behavior changes
❌ Don't skip validation in development
❌ Don't disable security for testing
❌ Don't comment out protection
❌ Don't have different code paths per environment
❌ Don't give tests false confidence
If your code behaves differently in development and production, your tests are lying to you.
// This is a time bomb:
if (WP_DEBUG) {
validate($input); // Only in dev
}
process($input); // Always runs
// This is safe:
validate($input); // Always runs
process($input); // Always runsWrite code that works the same everywhere. Use configuration for environment differences.
Your production users will thank you (because they'll actually receive their notifications)!