ผ่าน sandbox ไม่ได้แปลว่าใช้ได้จริง — สัญญาณหลอก 7 แบบ
sandbox มีไว้ให้ทดสอบว่ารูปแบบคำขอถูก ไม่ได้มีไว้ให้ทดสอบว่าระบบจะทำงานยังไงในโลกจริง ความผิดพลาดที่แพงที่สุดคือเอาพฤติกรรมของ sandbox ไปออกแบบหน้าจอและตรรกะตัดสินใจ
พอดีผมเขียนระบบสั่งซื้ออัตโนมัติต่อกับ API ของผู้ให้บริการรายหนึ่ง เขามี sandbox ให้เล่นพร้อมเงินปลอมในบัญชีก้อนใหญ่ ผมไล่เทสจนผ่านครบทุกคำสั่ง ทั้งตรวจของว่าง ทั้งสั่งซื้อ ทั้งดึงรายการกลับมา คิดว่าจบแล้ว พอเอาขึ้นของจริงถึงรู้ว่าที่นั่งเทสมาสองวัน มีอย่างน้อยเจ็ดจุดที่ sandbox บอกคนละเรื่องกับ production และหลายจุดผมหลงไปแก้โค้ดตัวเองอยู่นานเพราะคิดว่าเราเขียนผิด บทความนี้ผมรวบรวมไว้ให้ครบ พร้อมวิธีเขียนโค้ดให้ไม่หลงเชื่อสัญญาณพวกนี้
อาการทั้งหมดที่เล่าเป็นรูปแบบที่เจอได้กับ sandbox แทบทุกเจ้า ผมตัดชื่อผู้ให้บริการ ชื่อผู้ใช้ทั้งฝั่งทดสอบและฝั่งจริง รายการที่อยู่ในบัญชีอนุญาต และเลขคำสั่งซื้อออกหมด ประเด็นอยู่ที่วิธีอ่านสัญญาณ ไม่ใช่ว่าใครทำ sandbox ห่วย
สัญญาณที่ 1 — ช้าจนคิดว่าโค้ดค้าง
คำสั่งสร้างรายการบน sandbox ใช้เวลาตอบกลับ 60 ถึง 120 วินาที ส่วน production ตอบกลับในไม่ถึง 5 วินาที ตอนแรกผมคิดว่า HTTP client ผมตั้งค่าอะไรผิด เพราะ timeout ปริยายของไลบรารีอยู่ที่ 30 วินาที ยิงกี่ครั้งก็โดนตัดทุกครั้ง
ก่อนจะไปแก้อะไร ผมจับเวลาจริงเก็บไว้ก่อน เพราะเถียงกับตัวเองด้วยความรู้สึกว่า "มันช้า" ไม่มีประโยชน์
# จับเวลาจริงหลายรอบ แล้วดูค่าที่แย่ที่สุด ไม่ใช่ค่าเฉลี่ย
for i in $(seq 1 5); do
curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' \
--max-time 200 -X POST "$ENDPOINT/create" -d @payload.json
done
# 200 78.412
# 200 112.905
# 200 64.330
ทางแก้ไม่ใช่การไล่ลด timeout แต่ต้องแยกค่าตามสภาพแวดล้อมไปเลย แล้วตั้งฝั่ง sandbox ให้ยาวกว่าที่ช้าที่สุดที่เคยเจอเผื่อไว้อีกครึ่งหนึ่ง
// config/provider.php — แยกค่าตามสภาพแวดล้อม อย่าใช้ค่าเดียวกันทั้งสองฝั่ง
return [
'endpoint' => env('PROVIDER_ENDPOINT'),
'timeout' => (int) env('PROVIDER_TIMEOUT', 180), // sandbox ช้ากว่ามาก
'retries' => (int) env('PROVIDER_RETRIES', 0), // คำสั่งที่ตัดเงิน ห้าม retry อัตโนมัติ
];
คำสั่งที่มีผลกับโลกภายนอก เช่น สั่งซื้อหรือตัดเงิน เวลา client timeout ไม่ได้แปลว่าฝั่งโน้นไม่ได้ทำ ผมเจอเคสที่ cURL ตัดที่ 180 วินาทีพอดี แล้วอีกสองนาทีต่อมาอีเมลยืนยันคำสั่งซื้อเข้ามา แปลว่ามันสำเร็จ ถ้าโค้ดตั้ง retry อัตโนมัติไว้ตรงนั้น จะกลายเป็นสั่งซ้ำสองครั้ง
สัญญาณที่ 2 — บอกว่าของไม่ว่าง ทั้งที่ว่าง
คำสั่งตรวจความพร้อมของ sandbox คืนค่าว่าไม่ว่างกับชื่อที่สุ่มขึ้นมาใหม่เอี่ยม ยิงซ้ำชื่อเดิมอีกรอบบางทีก็ว่าง อันนี้ไม่ใช่บั๊กในโค้ดเราและไม่ใช่ว่ามีคนจองไปจริง มันเป็นความไม่เสถียรของ sandbox เฉย ๆ
สิ่งที่ผมทำคือแยกให้ชัดในโค้ดว่า "ไม่ว่าง" กับ "ตอบไม่ชัดเจน" เป็นคนละสถานะ แล้วเขียน log ให้เห็นทั้งคำขอและคำตอบดิบ เวลาย้อนกลับมาดูจะได้ไม่ต้องเดา
$r = $client->check($name);
match (true) {
$r->available === true => $this->proceed($name),
$r->available === false => Log::info('unavailable', ['name' => $name, 'raw' => $r->raw]),
default => Log::warning('ambiguous availability', ['raw' => $r->raw]),
};
สัญญาณที่ 3 — ชื่อที่มีคำว่า test ถูกปฏิเสธ
อันนี้ตลกดี sandbox ปฏิเสธคำขอที่ชื่อรายการมีคำว่า test, demo หรือ sandbox อยู่ข้างใน ซึ่งเป็นคำที่คนเทสระบบทุกคนพิมพ์เป็นอย่างแรก พอโดนปฏิเสธด้วยรหัสเดียวกับ "ชื่อไม่ถูกต้อง" ผมก็ไล่ตรวจตัวตรวจสอบรูปแบบฝั่งตัวเองอยู่เกือบชั่วโมง
วิธีที่ผมใช้แล้วจบคือให้ตัวสร้างชื่อสำหรับทดสอบ ออกชื่อที่หน้าตาเหมือนของจริงเสมอ แต่ยังแยกออกได้จากคำนำหน้ากับวันที่
function testName(string $prefix): string
{
// หน้าตาเหมือนของจริง แต่ไล่ย้อนได้ว่าเกิดจากการทดสอบวันไหน
return sprintf('%s-%s-%s', $prefix, date('Ymd'), bin2hex(random_bytes(3)));
}
// ตัวอย่างผลลัพธ์: acme-20261121-9f31c0
สัญญาณที่ 4 — สร้างสำเร็จ ตัดเงินแล้ว แต่ค้นไม่เจอ
อันนี้คือตัวที่หลอกหนักสุด ลำดับเหตุการณ์คือคำสั่งสร้างตอบว่าสำเร็จ ยอดเงินในบัญชีทดสอบถูกตัดจริง อีเมลสรุปคำสั่งซื้อเข้ามาจริง แต่พอเรียกคำสั่งดึงรายการทั้งหมดกลับได้ศูนย์รายการ
ผมเสียเวลาไปครึ่งวันกับการไล่ดูว่าส่ง parameter การแบ่งหน้าผิดหรือเปล่า สุดท้ายคำตอบคือ sandbox ไม่ได้บันทึกรายการเข้าบัญชีผู้ใช้ให้เสมอไป แม้คำสั่งสร้างจะสำเร็จทุกขั้น
บทเรียนที่เอาไปใช้ได้จริงคือ อย่าใช้คำสั่งค้นรายการเป็นตัวยืนยันว่าสร้างสำเร็จ ให้ยืนยันจากสิ่งที่ผู้ให้บริการออกให้เป็นหลักฐานของรายการนั้น เช่นเลขอ้างอิงที่ติดมากับคำตอบหรือกับอีเมลสรุป แล้วเก็บเลขนั้นลง DB ฝั่งเราตั้งแต่วินาทีแรก
-- เก็บหลักฐานตั้งแต่ก่อนยิง แล้วค่อยเติมผลทีหลัง
INSERT INTO provider_orders (local_ref, payload_hash, status, created_at)
VALUES (?, ?, 'sending', NOW());
UPDATE provider_orders
SET status = 'confirmed', remote_ref = ?, confirmed_at = NOW()
WHERE local_ref = ? AND status = 'sending';
สัญญาณที่ 5 — ยอดที่ถูกตัดไม่ใช่ราคาจริง
รายการที่ production คิดประมาณสิบถึงสิบห้าหน่วย ฝั่ง sandbox ตัดไปสี่สิบกว่าหน่วย เพราะเขาพ่วงบริการเสริมเข้ามาให้ในโหมดทดสอบโดยไม่บอก ถ้าเอาตัวเลขนี้ไปคำนวณส่วนต่างหรือไปโชว์บนหน้าจอ จะพังทันทีวันสลับเป็นของจริง
ใช้ยืนยันรูปแบบคำขอ
- ตอบกลับช้ากว่าหลายสิบเท่า
- ยอดที่ตัดเป็นเงินปลอมและไม่ตรงราคาจริง
- ชื่อฟิลด์ราคาในแคตตาล็อกไม่เหมือน production
- ข้อมูลอาจไม่ถูกบันทึกถาวร
ใช้ยืนยันพฤติกรรมจริง
- ตอบกลับเร็วและคงเส้นคงวา
- ยอดที่ตัดคือเงินจริง ทดสอบพลาดแล้วเสียเงิน
- ชื่อฟิลด์ตรงกับเอกสาร
- ข้อมูลคงอยู่จริง ค้นเจอทุกครั้ง
กฎที่ผมตั้งให้ตัวเองหลังจากนั้นคือ ห้ามออกแบบหน้าจอหรือเขียนตรรกะตัดสินใจจากตัวเลขที่ได้จาก sandbox ถ้ายังไม่มีตารางราคาจริง ให้หน้าจอแสดงว่ายังไม่มีข้อมูลราคาไปก่อน ดีกว่าแสดงเลขที่ผิด
สัญญาณที่ 6 — โครงสร้างข้อมูลแคตตาล็อกไม่เหมือนกัน
ต่อจากข้อที่แล้ว ตอนผมดึงรายการสินค้าพร้อมราคามาเตรียมทำหน้าจอ ตัวรวมผลบอกว่าหาราคาไม่เจอเลยสักรายการ ทั้งที่รายการมีมาเป็นพัน สาเหตุคือ sandbox กับ production ใช้ชื่อฟิลด์ราคาคนละชื่อ โค้ดที่อ่านฟิลด์ตรง ๆ เลยได้ค่าว่าง
วิธีกันคือเขียนตัวแปลงผลลัพธ์ให้ยอมรับได้หลายชื่อฟิลด์ แล้ว ดังให้สุดเวลาไม่เจอ ไม่ใช่เงียบแล้วคืนค่าศูนย์
function priceOf(array $row): ?float
{
foreach (['price', 'retailPrice', 'yourPrice', 'amount'] as $k) {
if (isset($row[$k]) && is_numeric($row[$k])) {
return (float) $row[$k];
}
}
Log::error('price field not found', ['keys' => array_keys($row)]);
return null; // คืน null ให้ชั้นบนตัดสินใจ ห้ามคืน 0
}
การคืนศูนย์ตรงนี้คือบั๊กที่อันตรายที่สุดในบทความนี้ เพราะศูนย์เป็นตัวเลขที่ดูสมเหตุสมผล แล้วมันจะไหลไปทั้งระบบโดยไม่มีใครสะดุด
สัญญาณที่ 7 — ทดสอบจากเครื่องตัวเองไม่ได้เลย
ผู้ให้บริการจำกัดว่าคำขอต้องมาจากที่อยู่ที่ลงทะเบียนไว้เท่านั้น ซึ่งเป็นที่อยู่ของเซิร์ฟเวอร์ ไม่ใช่เครื่องที่ผมนั่งเขียนโค้ดอยู่ แปลว่าเทสจากเครื่องตัวเองไม่ได้เลยสักครั้ง
ผมไม่ไปแก้ด้วยการเปิดช่องเพิ่ม เพราะบ้านใช้ที่อยู่แบบเปลี่ยนไปเรื่อย ๆ ทางที่ผมใช้คือรันคำสั่งบนเครื่องที่ได้รับอนุญาตแล้วดู log สดไปพร้อมกัน
# รันบนเครื่องที่อยู่ในรายการอนุญาต แล้วดู log สดอีกหน้าต่าง
ssh [email protected] \
'cd /srv/app && php artisan provider:demo-order --confirm-sandbox-mutation --name=acme-20261121-9f31c0'
ssh [email protected] 'tail -f /srv/app/storage/logs/laravel-$(date +%F).log'
สังเกตธงบังคับ --confirm-sandbox-mutation อันนั้นผมใส่เอง เป็นกติกาที่ผมตั้งไว้ว่าคำสั่งไหนที่ไปเปลี่ยนสถานะฝั่งผู้ให้บริการ ต้องพิมพ์ธงนี้ทุกครั้ง ไม่มีค่าปริยาย กันตัวเองเผลอกด enter ซ้ำจากประวัติคำสั่ง
สรุปเป็นรายการตรวจก่อนสลับเป็นของจริง
- แยกค่า endpoint, timeout, และ credential ของสองสภาพแวดล้อมออกจากกันคนละชุด อ่านจาก environment ล้วน
- ทุกคำสั่งที่มีผลภายนอก ต้องจองสถานะใน DB ฝั่งเราก่อนยิง แล้วเติมผลทีหลังแบบเขียนซ้ำได้
- ห้าม retry อัตโนมัติกับคำสั่งที่ตัดเงินหรือสร้างของ ให้ค้างไว้เป็นสถานะรอตรวจสอบแทน
- ยืนยันความสำเร็จจากเลขอ้างอิงที่ผู้ให้บริการออกให้ ไม่ใช่จากคำสั่งค้นรายการ
- ตัวเลขราคาและปริมาณทุกตัวที่ได้จาก sandbox ให้ถือว่าไม่จริง จนกว่าจะเห็นจาก production
- ตัวแปลงผลลัพธ์ต้องดังเมื่อหาฟิลด์ไม่เจอ ห้ามคืนค่าศูนย์หรือค่าว่างเงียบ ๆ
- ทดสอบครั้งแรกบน production ให้เลือกรายการที่ราคาต่ำสุดและยกเลิกได้ แล้วเฝ้า log สดตลอดการรัน
มันยืนยันได้ว่า รูปแบบคำขอของเราถูกต้องและระบบยืนยันตัวตนผ่าน เท่านั้น เรื่องเวลา เรื่องราคา เรื่องความคงอยู่ของข้อมูล และเรื่องกฎทางธุรกิจ ต้องไปพิสูจน์ที่ production ด้วยรายการจริงที่เล็กที่สุดเท่าที่จะเล็กได้
ผมเจอชุดนี้ตอนทำระบบสั่งซื้ออัตโนมัติ แต่พอไปทำงานต่อ API ฝั่งรับชำระเงินก็เจอแบบเดียวกันอีก ที่เล่าไว้ใน ต่อ Open API ธนาคารออก QR รับเงิน แปลว่ามันไม่ใช่เรื่องของผู้ให้บริการรายใดรายหนึ่ง แต่เป็นธรรมชาติของสภาพแวดล้อมทดสอบที่ไม่ได้ผูกกับของจริง ใครจะออกแบบรายการตรวจของตัวเองยาวกว่านี้ก็ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือเจอสัญญาณหลอกแบบอื่นเพิ่ม เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ
อยากให้เราทำให้ไหม?
ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า
เคยเจองานแบบนี้ไหม? ติดตรงไหน?
มาแลกวิธีกันต่อในบอร์ด LinuxThai



ความคิดเห็น 0
ยังไม่มีความคิดเห็น — เป็นคนแรกเลย