4 คะแนน โดย GN⁺ 2024-04-08 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • pgmock คือเซิร์ฟเวอร์จำลอง PostgreSQL แบบ in-memory สำหรับการทดสอบหน่วยและการทดสอบ E2E โดยรันด้วย WebAssembly บน Node.js และเบราว์เซอร์ได้โดยไม่ต้องพึ่งพา dependency ภายนอก
  • ผู้ใช้ node-postgres สามารถเชื่อมต่อได้ผ่านออบเจ็กต์การตั้งค่าที่ไม่ต้องเปิดพอร์ต และวิธีนี้ก็ใช้ได้บนเบราว์เซอร์เช่นกัน
  • บนเบราว์เซอร์ เว็บแอปไม่สามารถเปิด TCP port ได้ แต่สามารถใช้ PostgresMock.createSocket และการตั้งค่า node-postgres ได้ โดยหาก bundler วิเคราะห์ static import ก็อาจมีคำเตือนเรื่องโมดูล Node.js แบบ optional
  • การติดตั้งใช้งานปัจจุบันใช้วิธีรันเซิร์ฟเวอร์ PostgreSQL ภายใน x86 emulator และให้ความสำคัญกับการป้องกันความต่างของพฤติกรรมระหว่างการทดสอบกับโปรดักชันมากกว่าประสิทธิภาพ
  • ในระยะยาว หาก native PostgreSQL WASM fork มีความพร้อมมากขึ้น ก็มีแผนจะรองรับทั้งสองแนวทาง และหลังจากนั้นจะสลับไปใช้ native WASM เป็นค่าเริ่มต้น

สิ่งที่ pgmock มอบให้

  • pgmock คือเซิร์ฟเวอร์จำลอง PostgreSQL แบบ in-memory สำหรับการทดสอบหน่วยและการทดสอบ E2E
  • ไม่ต้องใช้ dependency ภายนอก และรันภายใน WebAssembly ได้ทั้งบน Node.js และเบราว์เซอร์
  • ติดตั้งได้ผ่าน npm
npm install pgmock

ลำดับการใช้งานพื้นฐาน

  • สามารถสร้างเซิร์ฟเวอร์ in-memory ได้ด้วย PostgresMock.create() และรับ connection string ได้ด้วย listen(5432)
import { PostgresMock } from "pgmock";

const mock = await PostgresMock.create();
const connectionString = await mock.listen(5432);
  • หากใช้ node-postgres mock.getNodePostgresConfig() จะให้ออบเจ็กต์การตั้งค่าที่เชื่อมต่อได้โดยไม่ต้อง listen บนพอร์ต
  • เมื่อทำงานเสร็จแล้ว แนะนำให้เรียก mock.destroy() เพื่อคืนทรัพยากร
mock.destroy();

การรองรับเบราว์เซอร์และความต่างจาก pglite

  • pgmock รองรับสภาพแวดล้อมเบราว์เซอร์อย่างสมบูรณ์
  • เว็บแอปไม่สามารถเปิด TCP port ได้ แต่สามารถใช้ PostgresMock.createSocket และการตั้งค่า node-postgres ได้
  • หาก bundler วิเคราะห์ import แบบ static อาจมีคำเตือนว่าไม่มีโมดูล Node.js แบบ optional โดยตัวอย่างการตั้งค่า Webpack อยู่ที่ examples/web-demo/next.config.mjs
  • หากต้องการรันเฉพาะฐานข้อมูลบนเบราว์เซอร์ อาจพิจารณา pglite
    • pglite เร็วกว่าและเบากว่า แต่มีชุดความสามารถจำกัดกว่า
    • pgmock ถูกออกแบบมาโดยมีเป้าหมายเรื่องความเทียบเท่าด้านฟีเจอร์กับ PostgreSQL ในโปรดักชันที่ต้องการในสภาพแวดล้อมทดสอบ

วิธีรัน PostgreSQL บน WebAssembly

  • มีสองแนวทางในการรัน PostgreSQL บน WebAssembly
  • แนวทาง native WASM fork เร็วกว่าและใช้หน่วยความจำน้อยกว่ามาก แต่รองรับเพียงโหมดผู้ใช้คนเดียว และไม่รองรับการเชื่อมต่อกับส่วนขยาย
  • ปัจจุบัน pgmock ใช้แนวทาง x86 emulator
    • เป้าหมายคือป้องกันความไม่ตรงกันระหว่างการทดสอบกับโปรดักชัน
    • เพราะโดยทั่วไปแล้วประสิทธิภาพไม่ใช่ปัญหาใหญ่ในการทดสอบ
  • ในระยะกลาง หาก native PostgreSQL WASM fork มีความพร้อมมากขึ้น ก็มีแผนจะรองรับทั้งสองตัวเลือก
  • หลังจากนั้นมีแผนจะสลับ native WASM ให้เป็นค่าเริ่มต้น และคาดว่าน่าจะไม่มี breaking change ขนาดใหญ่มากนักนอกเหนือจาก internal API ของ PostgresMock.subtle

ความต่างจากโปรเจ็กต์ PostgreSQL บนเบราว์เซอร์ที่มีอยู่

  • pgmock ให้ความเข้ากันได้ของฟีเจอร์แบบครบถ้วนภายใน JavaScript runtime และไม่พึ่งพาnetwork proxyในการสื่อสาร
  • มีการจำลอง network stack ด้วย JavaScript เพื่อให้ทำงานเหมือนเครือข่ายจริง และสามารถจำลองการเชื่อมต่อ TCP ได้แม้บนแพลตฟอร์มที่ไม่อนุญาตให้เข้าถึง raw socket

การขยายต่อและโปรเจ็กต์ที่เกี่ยวข้อง

  • ในทางทฤษฎีสามารถรัน Docker image หรือฐานข้อมูลอื่นได้ด้วย แต่ยังไม่ได้ผ่านการทดสอบ
  • มีการกล่าวถึง implementation และโปรเจ็กต์พื้นฐานที่เกี่ยวข้องดังนี้
    • v86: x86 emulator
    • Supabase & Snaplet: พื้นฐานของแนวทางการรัน PostgreSQL ภายใน WebAssembly
    • Stackframe: ถูกกล่าวถึงว่าเป็นบริษัทที่จ่ายเงินเดือนระหว่างการพัฒนา pgmock

1 ความคิดเห็น

 
GN⁺ 2024-04-08
ความคิดเห็นจาก Hacker News
  • หลายเดือนที่ผ่านมา บริษัทกำลังสร้าง Postgres แบบ in-memory และมีความเทียบเท่าด้านฟีเจอร์กับฐานข้อมูลที่ใช้จริงในโปรดักชัน
    ข้อดีคือไม่ต้องใช้โปรเซสภายนอกหรือพร็อกซี หากแพลตฟอร์มรัน WASM ได้ ก็สามารถรัน pgmock ได้ทั้งใน Node.js หรือเบราว์เซอร์ และการสร้างฐานข้อมูลใหม่ที่ใส่ข้อมูล mock ไว้ก็ง่ายพอ ๆ กับการสร้างอ็อบเจกต์ JavaScript
    มันต่างจาก pglite ที่เป็นแรงผลักดันให้เปิดซอร์ส pgmock อยู่เล็กน้อย pgmock รัน Postgres ต้นฉบับภายในอีมูเลเตอร์ x86 ส่วน pglite คอมไพล์ fork ของ Postgres เป็น WASM แบบเนทีฟโดยตรง จึงเร็วและเบากว่า
    อย่างไรก็ตาม pglite รองรับเฉพาะโหมดผู้ใช้เดี่ยวและ extension บางส่วน จึงไม่สามารถเชื่อมต่อด้วยไคลเอนต์ Postgres ทั่วไปได้ ซึ่งค่อนข้างสำคัญสำหรับการทดสอบ E2E
    ในทางทฤษฎี น่าจะปรับให้รัน Docker image ใด ๆ บนแพลตฟอร์ม WebAssembly ได้ เลยอยากรู้ว่ามีเป้าหมายเฉพาะอะไรที่อยากเห็นหรือไม่

    • งานเจ๋งมาก เป็นความจริงที่ตอนนี้ PGlite รองรับแค่ ผู้ใช้เดี่ยว และอาจเป็นปัญหาสำหรับการใช้ทดสอบแบบ integration ในบางสภาพแวดล้อม
      กำลังคิดไว้หลายวิธีที่จะเพิ่มโหมดหลายการเชื่อมต่อ แต่น่าจะใช้เวลาสักพัก PGlite ยังมีข้อจำกัดอื่น ๆ ที่เกี่ยวกับโหมดผู้ใช้เดี่ยวด้วย เช่น ยังไม่รองรับ pg_notify และก็มีแผนจะแก้เรื่องนี้เช่นกัน
      ในทางกลับกัน โปรเจกต์นี้ใกล้เคียงกับ Postgres จริงมากกว่าเยอะ จึงมีโอกาสสูงที่จะทำงานได้เลย โปรเจกต์ Postgres แบบ in-memory เหล่านี้ดูเหมือนจะลดเวลารันเทสต์ลงได้เหลือ ไม่ถึงหนึ่งในสี่ และมีศักยภาพสูงมากในด้านการทดสอบ
      นี่คือความเห็นจากมุมของคนทำ PGlite
    • ไอเดียเรื่อง การรัน Docker image บน WASM ดูมีแววสำหรับหลายปัญหา
      ช่วงหลังอยากรัน pipeline FFMPEG/SoX ฝั่งไคลเอนต์ แต่มี dependency เยอะมากจนคอมไพล์ใหม่ด้วย Emscripten ได้ไม่ง่าย เลยสงสัยว่าแนวทางนี้จะช่วยกรณีแบบนั้นได้ไหม
    • ถ้ารองรับ extension pgvector ได้ ก็อาจกลายเป็นฐานข้อมูลเวกเตอร์ที่เร็วมากและมีพลังทั้งหมดของ Postgres
      ด้วยฟีเจอร์เชิงสัมพันธ์ ทำให้เพิ่มเมทาดาทาเฉพาะโดเมนที่หลากหลายซึ่งมักอยู่ในฐานข้อมูลเชิงสัมพันธ์ และ query ร่วมกันได้
    • หมายเหตุ เดโมออนไลน์ดูเหมือนจะแตกเมื่อเจอ query ที่ไม่ถูกใจ
      รัน select foo(); แล้วได้ Error.captureStackTrace is not a function เกิดขึ้นบน Firefox 124.0.2 บน Linux
    • ยอดเยี่ยม แต่แนวคิดของ การทดสอบ E2E เองไม่ได้หมายถึงการใช้สภาพแวดล้อมจริงโดยไม่แทนที่คอมโพเนนต์ด้วย mock หรอกหรือ
  • สงสัยว่าแค่เอาไฟล์ Postgres ไปวางบน ramdisk แล้วรันไม่ได้หรือ
    อัปเดต: ดูเหมือนว่ามันรันในสภาพแวดล้อมเบราว์เซอร์/Node ได้ จึงให้เทสต์สร้าง·อัปเดต·ลบได้ ผมเป็นนักพัฒนาแบ็กเอนด์มากเกินไป เลยไม่ค่อยเห็นข้อดีเมื่อเทียบกับสภาพแวดล้อมพัฒนาทั่วไป ถ้าอธิบายได้ว่าดีกว่าตรงไหน เมื่อไร และอย่างไร ก็คงดี

    • ภายในอีมูเลเตอร์ก็โดยมากเกิดสิ่งคล้าย ๆ กัน ดิสก์ที่ถูก emulate เป็น ระบบไฟล์ 9P แบบ in-memory
      เหตุผลที่ทำด้วย WebAssembly คือทำให้พฤติกรรมพกพาได้สม่ำเสมอมากขึ้นทั้งข้ามแพลตฟอร์ม สถาปัตยกรรม ไปจนถึงเบราว์เซอร์หรือสภาพแวดล้อม edge และเป็นการตั้งค่าที่ไม่มี dependency ภายนอก แม้แต่ Docker ก็ไม่ต้องใช้
      เพราะอีมูเลเตอร์ช่วยให้บูตจากสถานะที่รันอยู่แล้วได้ทันที การบูตฐานข้อมูลที่ถูก emulate จึงเร็วกว่าการเปิดฐานข้อมูลจริงหรือคอนเทนเนอร์ Docker แต่เรื่องนี้ใกล้เคียงกับผลพลอยได้ที่โชคดีมากกว่าจะเป็นเป้าหมายการออกแบบ
    • ผมก็ไม่ค่อยเข้าใจเหมือนกัน ดูเหมือนมี โค้ดที่ไม่จำเป็น เยอะเกินไป ทั้งอีมูเลเตอร์ สแตกเครือข่าย ฯลฯ
      สงสัยว่าใช้ของอย่าง https://testcontainers.com/ ไม่ได้หรือ การที่ container engine เป็น dependency ภายนอกนี่แย่ขนาดนั้นเลยหรือ
    • เป้าหมายของการทดสอบ E2E คือการทดสอบระบบใน สถานะจริง เพราะเป็นการ emulate สภาพแวดล้อมโปรดักชัน จึงตรวจได้ด้วยว่าเมื่อดึงปลั๊กไฟหรือดิสก์เต็มจะเกิดอะไรขึ้น
      ทันทีที่ยัด mock เข้าไปข้างใน มันก็กลายเป็น unit test แม้จะมีประโยชน์ แต่ไม่ใช่สิ่งเดียวกัน หนึ่งในแก่นของ E2E คือเพราะไม่มี mock เราจึงรู้ได้ว่าเทสต์ถูกต้อง นี่ไม่ใช่การทดสอบ Postgres แต่เป็นการทดสอบระบบนี้ทุกครั้ง
      ถ้ากำลังสร้าง PG สำหรับระบบ embedded, น้ำหนักเบา, ประสิทธิภาพต่ำ ก็สมเหตุสมผลในฐานะเทสต์ตรวจสอบก่อนทำ E2E จริงที่ช้า ผมเองก็มีเคสใช้งานแบบนั้น
      นอกเหนือจากนั้น นี่เป็นโปรเจกต์ที่เจ๋ง และดูเป็นเครื่องมือที่ใช้ได้เมื่อต้องการ PG shim
    • อาจมีประโยชน์สำหรับการแยกเทสต์ พอย้ายแบ็กเอนด์ Redis ในเทสต์ไปเป็น FakeRedis เสียงรบกวนใน test suite ก็ลดลงพอสมควร
      Postgres ใช้ savepoint อยู่ แต่ถึงอยู่บน ramdisk ก็ไม่ได้เร็วขนาดนั้น
  • เมื่อก่อนในการทดสอบ เรารัน เซิร์ฟเวอร์ in-memory ปลอม แบบ custom สารพัดอย่าง เดี๋ยวนี้ใช้ https://testcontainers.com เพื่อรันของจริงแทน

  • ถ้านักพัฒนา Prisma/Node.js แค่อยากได้ “Postgres ในกระป๋อง” สำหรับการพัฒนาโลคัล เวอร์ชันที่ทำ PGlite ให้เป็นเซิร์ฟเวอร์ที่เพิ่งออกมาอย่าง pglite-server อาจเหมาะกว่า: https://github.com/kamilogorek/pglite-server
    มันเร็วกว่าและเก็บข้อมูลไว้ในระบบไฟล์ได้ แต่เมื่อใช้งานหนักจะเสถียรน้อยกว่าเซิร์ฟเวอร์ทดสอบ E2E ที่อิงอีมูเลเตอร์ x86 ทั้งตัว pglite-server ใช้หน่วยความจำแค่ 150MB ขณะที่ pgmock-server ใช้ 830MB
    แค่ checkout .env.local ใหม่ด้วย dotenv แล้วเปลี่ยน DATABASE_URL ในสคริปต์รัน package.json ทั้งหมดของ nextjs/prisma ก็พอ
    DATABASE_URL="postgresql://postgres@localhost:5432/awesomeproject"
    "db:pushlocal": "dotenv -e .env.local -- pnpm prisma db push"
    เอาไปติดกับโปรเจกต์ไหนก็ง่ายมาก และก็เข้าใจได้ว่าทำไม Neon ถึงสนับสนุนพื้นที่นี้

  • ไม่ได้อยากสาดน้ำเย็นใส่หรอกนะ แต่ผมคงไม่ใช้สิ่งนี้
    ถ้าเป็นแอปพลิเคชันแบบเรียบง่ายอาจทำงานได้ แต่พอความซับซ้อนเพิ่มขึ้น เช่น มีความเสี่ยงเรื่อง deadlock หรือพึ่งพารูปแบบของฐานข้อมูล ความแตกต่างเล็ก ๆ ในพฤติกรรมก็อาจลุกลามเป็นปัญหาร้ายแรงได้ ทำให้คุณค่าลดลง
    ช่วงนี้ผมชอบ สภาพแวดล้อม E2E ที่มีข้อจำกัดด้านทรัพยากร มากกว่า เพราะตัวรันทดสอบในเครื่องจะมีโอกาสพังเมื่อมีใครเขียนโค้ดที่ไม่มีประสิทธิภาพอย่างรุนแรง
    อีกอย่าง วิธีที่ทำฐานข้อมูลให้เป็น snapshot หลังจากไม่กี่วินาทีแล้วแจกจ่าย snapshot นั้นไปยังพาร์ทิชันทดสอบนั้นเร็วมาก และเคยช่วยลดเวลาจาก test suite ได้ทีละหลายนาทีบ่อยครั้ง
    เป็นไอเดียที่น่าสนใจและน่าจะเป็นประสบการณ์การเรียนรู้ที่ดี แต่ผมมองว่ากลุ่มผู้ใช้เป้าหมายค่อนข้างจำกัด

  • ชื่อเรื่องทำให้สับสนนิดหน่อย ถ้า “ทำที่บริษัท” ก็เหมือนมีสมมติฐานว่าใช้ทรัพยากรของบริษัท ดังนั้น สิทธิ์ในทรัพย์สินทางปัญญา ของโปรเจกต์นี้ก็น่าจะเป็นของนายจ้างไม่ใช่หรือ
    ถ้าอย่างนั้นก็สงสัยว่าในทางเทคนิคแล้วเปิดเป็นโอเพนซอร์สได้หรือไม่

    • เพราะเป็นสตาร์ตอัป การเปิดเป็นโอเพนซอร์สจึงง่ายพอ ๆ กับการขออนุมัติจากสมาชิกคนอื่น ๆ ในทีม
    • repository เป็นของ Stackframe และในไฟล์ LICENSE เขียนว่า Copyright 2024 Stackframe. ดูเหมือนผู้เขียนทำงานที่ Stackframe
  • อยากรู้ว่าสิ่งนี้เทียบกับ โหมดเข้ากันได้กับ Postgres ของ H2 แล้วเป็นอย่างไร

    • ผมอาจผิดก็ได้ แต่คิดว่าใน H2 น่าจะใช้ stored procedure ของ PostgreSQL ไม่ได้
  • เจ๋งทีเดียว ถ้าตอบได้ มีหลายเรื่องที่สงสัย
    อยากรู้ว่าอะไรเป็นแรงผลักดันให้บริษัททำโปรเจกต์นี้ขึ้นมา หรือการรัน Postgres ใน Docker container ช้าเกินไปหรือเปล่า
    และอยากรู้ด้วยว่าการตั้งค่า CI สำหรับ E2E test ก่อนและหลังนำ pgmock เข้าไปผสานใน flow แตกต่างกันอย่างไร
    อีกอย่าง สงสัยว่ากระบวนการย้ายมาใช้โซลูชันนี้ยากไหม

  • ถ้า dump ข้อมูล production แล้วลบข้อมูลอ่อนไหวทั้งหมด จากนั้น truncate ตารางที่ไม่จำเป็น เช่น ตาราง log ก็จะได้สำเนาสำหรับพัฒนาที่ดี
    จากนั้นก็นำไปทำซ้ำสำหรับ development, QA, E2E ฯลฯ สิ่งที่ E2E ต้องการก็คือ extension, trigger, function, view, index และ data เหล่านั้นนั่นเอง

  • สงสัยว่าแค่ใช้ Docker แล้วมีฐานข้อมูลทดสอบแยกต่างหากไม่ได้หรือ
    Elixir ทำแบบนั้น และ test framework จะครอบแต่ละ test ด้วย transaction แล้ว rollback เพื่อแยก isolation น่าจะน่าสนใจถ้าได้รู้ว่าข้อดีของแนวทางนี้คืออะไร