มารู้จักและสร้าง Contract Testing ด้วย Jest.JS กันเถอะ

Traitanit Huangsri
LINE Developers Thailand
4 min readAug 26, 2019

สวัสดีครับ ผมเชื่อว่าหลายคนที่เปิดเข้ามาอ่านบทความนี้ น่าจะเคยได้ยินหรือรู้จักการพัฒนา Software ด้วยการออกแบบในลักษณะที่เป็น Microservice Architecture กันมาบ้างใช่มั้ยครับ แต่ถ้าใครที่ยังไม่รู้จักก็ลองอ่านบทความนี้เบื้องต้นก่อนได้ครับ

เมื่อเรามี Component ที่รันอยู่เป็น Microservice หลายๆ ตัวก็มักจะเจอปัญหาเรื่อง Compatibility ระหว่าง Component ที่มีการเปลี่ยนแปลงอยู่บ่อยๆ โดยเฉพาะในช่วงเริ่ม Development Process ใหม่ๆ ก็จะต้องมีการสื่อสารเรื่อง Interface หรือ Spec ที่แต่ละ Component จะใช้สื่อสารกันค่อนข้างมาก ซึ่งแน่นอนว่าถ้าใครที่ทำเทส Project ที่เป็น Microservice เยอะๆ แบบนี้ก็มักจะเจอปัญหาเรื่อง Failed Tests ที่เกิดจากการเปลี่ยน Interface หรือ Spec ของ ​Microservice บางตัวอยู่บ่อยๆ ใช่มั้ยครับ

วันนี้ผมจะขอยกหนึ่งใน Practice ของการทำ Test (Automation) ที่ช่วย Detect ปัญหาที่เกิดขึ้นจากการเปลี่ยนแปลงของ Microservice แต่ละตัว ซึ่ง Practice ที่ว่านั้นเราจะเรียกว่าการทำ “Contract Testing” นั่นเอง

รู้จัก Contract Testing

เคยมั้ยครับ? ที่ App ที่เราต้องการจะเทสต้องมีการไปต่อกับ External Service หรือ Component อื่นๆ ที่ทีมของเราไม่ได้เป็นคนดูแล ซึ่งบางครั้ง Service เหล่านั้นก็อาจจะทำงานได้ค่อนข้างช้าหรือไม่ก็มีการเปลี่ยนแปลงโดยที่เราไม่รู้ ทำให้ Test ที่เรารันอยู่นั้น Failed ได้ง่ายๆ โดยที่ App เราไม่ได้มีการแก้ไขโค้ดแต่อย่างใด ทำให้เกิด Flaky Test หรือ Test ที่ไม่น่าเชื่อถือ (Unreliable) เกิดขึ้น

Contract Testing with Test Doubles

วิธีการแก้ปัญหาอย่างหนึ่งก็คือการนำ Test Doubles เข้ามาใช้ ซึ่ง Test Double คือตัวช่วยที่เราใช้ในการสร้าง Mock หรือ Stub External Service ที่เราไปต่อเพื่อตัด Dependency ที่ไม่เกี่ยวข้องกับการเทสของเราออกไป ทำให้ Test ของเราสามารถ Control Behavior รวมถึง Expected Result ที่เราต้องการได้ ผมเชื่อว่าหลายคนก็อาจจะเคยใช้พวก Tools ต่างๆ ที่ทำ Test Doubles ได้เช่น Wiremock, NodeRed, Karate เป็นต้น

Test Doubles นั้นก็ทำหน้าที่ของตัวมันเองได้ดีครับ ทำให้ Test ของเรารันได้เร็วขึ้น เกิด Flaky Tests น้อยลง แต่ก็มีข้อเสียคือ Low Fidelity คือมันเป็นการเทสที่ไม่เหมือน Real World Scenarios เลยจริงๆ

นั่นล่ะครับคือปัญหา เราจะรู้ได้ยังไงว่าถ้าสมมติวันนึง External Service ตัวนั้นมีการ เปลี่ยนแปลง Interface การเรียกใช้งานใหม่โดยที่เค้าไม่ได้มีการสื่อสารบอกกับทีมเรา ซึ่งก็อาจจะเป็นผลให้ App ของเราทำงานผิดพลาดเมื่อ Deploy ขึ้น Production ไปแล้ว (กว่าจะรู้ตัวก็สายไปเสียแล้ว)

วิธีการหนึ่งที่จะช่วยแก้ปัญหานี้ตั้งแต่ช่วง Development ก็การสร้าง Set ของ Test Cases แยกออกมาเพื่อมาทำ Contract Testing เพื่อเช็คดูว่า Interface ที่ใช้ในการคุยกันระหว่าง App ของเรากับ External Service ยังเป็นเหมือนเดิมอยู่มั้ย เมื่อ Contract Test รันแล้วเกิด Failure ขึ้นเราก็มีหน้าที่ทำการสอบถามไปยังคนที่ดูแล External Service นั้นและคอยทำการ Update Test Doubles ของเราให้มีความเหมือนจริงและเป็นปัจจุบันให้มากที่สุด

Contract Testing ต่างกับ API Integration Test ยังไง?

โดยสรุป การทำ Contract Testing ก็คือการเทสเพื่อ Check การสื่อสารระหว่าง App ของเรากับ External Service ว่ายังเหมือนเดิมอยู่มั้ย​ ซึ่งการทำ Contract Testing นั้นจะไม่ได้ Check ว่า Data ที่ Return มาจาก External Service นั้นมีค่าเป็นอะไร แต่จะสนใจเพียงแค่ว่า Data ที่ได้กลับมานั้นมีโครงสร้างหรือ Spec ที่เคยได้ตกลงกันไว้ตั้งแต่แรกหรือไม่ ซึ่งจะแตกต่างกับ API Functional Test ที่จะเทสว่า Data ที่ได้กลับมาจะต้องมีค่า(Values)ถูกต้องตามที่ Expect ไว้

การรัน Contract Testing นั้นอาจจะไม่ต้องรันบ่อยมากเหมือนกับการรัน API Integration Test ทั่วไปครับ โดยเราอาจจะตั้งเวลาให้มันรันเป็น Interval ตามความเหมาะสม เช่น Daily, Weekly ก็น่าจะเพียงพอแล้วครับ

Contract Testing with Jest.JS

วันนี้ผมจะขอแนะนำ Tool ที่จะช่วยทำ Contract Testing ได้ดีบนฝั่ง Node.js กันนั่นก็คือ Jest.JS นั่นเอง (ใน Platform ของภาษาอื่นๆ ก็มี Library ที่ทำ Contract Testing ได้ดีเช่นกัน แต่วันนี้ผมขอยกตัวอย่างที่ Javascript แล้วกันนะครับ)

Jest.JS คือ Open Source Javascript Testing Framework ที่ Maintain โดย Facebook ซึ่ง Jest เป็น Testing Library บน Node.js ที่ Powerful มากๆ ได้รับการยอมรับอย่างแพร่หลายและมีผู้ใช้ดาวน์โหลดไปใช้งานมากกว่า 10 ล้านครั้งในแต่ละเดือน

ซึ่ง Jest ก็มี Feature ที่ช่วยทำ Contract Testing ให้เราด้วย ซึ่ง Jest เรียกว่าการทำ Snapshot Testing ครับ ซึ่งตัว Snapshot Testing มันจะช่วย Snapshot Test Case ของเราพร้อมทั้ง Expected Result ต่างๆ เก็บไว้ในไฟล์ให้เราอัตโนมัติ เมื่อเราทำการรันเทสครั้งต่อไป มันก็จะเอา Snapshot Data ที่เก็บไว้มาเป็น Expected Result ในการรันครั้งปัจจุบันให้โดยที่ไม่ต้องแก้โค้ดอะไรเลย สะดวกสุดๆ ไปเลยครับ

ซึ่งเราก็สามารถกำหนดรูปแบบของ Snapshot File ของเราได้ด้วยนะครับ โดยการใช้ Matchers ที่ Jest เตรียมมาไว้ให้ เช่น การกำหนด Pattern ของ JSON Object ตามโครงสร้าง JSON Schema ที่เราต้องการ, การเช็ค Types ของ Property ใน Object ต่างๆ เป็นต้น ซึ่งก็ตอบโจทย์การทำ Contract Testing ที่เราต้องการได้เป็นอย่างดีครับ

เริ่มต้นเขียน Contract Testing

ผมได้สร้าง RESTful Web Service API ขึ้นมาอันนึงเพื่อเป็นตัวอย่างในการทำ Contract Testing ในบทความนี้ครับ ซึ่ง API นี้เป็น API ที่ใช้ Get ข้อมูลเที่ยวบินจากกรุงเทพฯ (BKK) ไปยังกรุงโซล (ICN) เกาหลีใต้ ซึ่งเราจะสมมติว่า External Service ของเราคือ Web Service ที่ Provide Flight Data พวกนี้ให้เราครับ

sh$ curl -X GET https://api-flights.demo.com/flights/routes/BKK/ICN

Flight API ก็ Return Data กลับมาเป็นตัวอย่างแบบนี้ครับ

ตัวอย่าง JSON Response Body

และนี่ก็คือ Interface ของ API ที่ใช้สื่อสารกันระหว่าง Application ของเรากับ External Service ดังนั้นเราก็จะเขียน Contract Testing ตาม Spec นี้ครับ

Install Dependencies

sh$ yarn add jest jest-extended axios

ผมทำการสร้าง Contract Test Spec File ขึ้นมาแบบนี้ครับ

ผมเขียนโค้ดเพื่อ Call API โดยใช้ Axios ซึ่งเป็น HTTP client ยอดนิยมของ Javascript ครับ หลังจากที่ได้ Response กลับมาก็ทำการ loop เข้าไปในแต่ละ Object ของ Array ของ Property data ใน Response Body เพื่อทำการ Snapshot Response Body เก็บไว้แบบนี้ครับ

expect(flightData).toMatchSnapshot()

โดยปกติ ถ้าเราไม่ Pass Parameter ใดๆ เข้าไปในฟังก์ชัน toMatchSnapshot() Jest จะมองว่าเป็นการทำ Exact Match 100% เลยนะครับ กล่าวคือมันจะ Check ว่าต้องเหมือนเป๊ะทั้งในแง่โครงสร้างของ JSON และ Values ของทุกๆ Property เลย

ซึ่งในแง่ของการทำ Contract Testing นั้น เราคงไม่ได้ต้องการความแม่นยำขนาดนั้น เพียงแต่เราต้องการ ​Check โครงสร้างของ JSON และ Values ของมันว่าตรงตาม Spec ที่ได้ตกลงกันไว้หรือไม่

ยกตัวอย่างเช่น Property ชนิด Date String อย่าง depart และ arrive เราก็อาจจะเช็คแค่ว่า Value ที่ได้มาจะต้องเป็น Pattern ที่เราต้องการเท่านั้น หรืออย่าง Property flightStatus ซึ่งจะมี Values เป็นได้เพียงค่าที่เราตกลงกันไว้เท่านั้น เช่น SCHEDULED|DELAYED|DIVERTED เป็นต้นครับ

ซึ่ง Jest ก็ใจดีเปิดช่องให้การทำ Snapshot Testing สามารถระบุ Pattern ของ Values ที่เราต้องการจะเช็คได้ โดยการเซ็ตค่าเข้าไปในฟังก์ชัน toMatchSnapshot() โดยใช้ Matchers ที่ Jest ให้มา ยกตัวอย่างเช่น

expect(flightData).toMatchSnapshot({
arrive: expect.stringMatching(/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}\+[0-9]{2}:[0-9]{2}/),
flightStatus: expect.toBeOneOf(['SCHEDULED', 'DELAYED', 'DIVERTED']),
airline: expect.any(String)
})

ผมได้ใช้ jest-extended ซึ่งเป็น Additional Matchers เพิ่มเติมที่ช่วยทำให้เราเขียนโค้ดเพื่อเช็ค Values ได้ง่ายขึ้น เช่น expect.toBeOneOf() ลองดูตัวอย่าง Matchers อื่นๆ เพิ่มเติมได้ที่นี่เลยครับ

เมื่อรันเทสเสร็จแล้ว Jest จะทำการสร้างไฟล์ flight.contract.spec.js.snap ขึ้นมาเพื่อเก็บ Snapshot Data ให้เราอัตโนมัติแบบนี้ครับ

ผมจะลองเทสดูว่า Contract Testing ของเราจะ Detect Changes ได้จริงๆ ไหมโดยการให้ API Return flightStatus แบบใหม่ที่ยังไม่เคย Map ไว้เป็น Values ที่เรารู้จักมาก่อน เช่น DEPARTED แล้วลองดูว่า ​Test ของเราจะ Failed มั้ยนะ ?

เมื่อรันแล้วก็พบว่า Test Failed ครับเพราะ Jest Detect เจอว่ามี Object หนึ่งที่มี flightStatus ไม่ใช่หนึ่งใน status ที่เคย Map ไว้อย่าง <SCHEDULED|DELAYED|DIVERTED> นั่นก็แสดงให้เห็นว่า Jest สามารถช่วย Detect Changes ของ API Spec ที่เปลี่ยนไปให้เราได้จริงๆ ดีงามมากๆ ครับ

สรุป

จะเห็นได้ว่าเราสามารถเขียน Contract Testing ได้ง่ายๆ ไว้เป็นตัวช่วยให้เราสามารถ Detect Changes ที่อาจจะเกิดขึ้นระหว่างการ Develop Microservice หลายๆ ตัวพร้อมๆ กันหรือเอาไว้ใช้สำหรับการเช็ค Interface ระหว่าง Internal กับ External Service ได้ด้วย

หวังว่าบทความนี้จะมีประโยชน์กับหลายๆ คนนะครับ ผมได้สร้าง GitHub Project ตัวอย่างโค้ดทั้งหมดไว้ สามารถเข้าไปดูได้ในลิ้งค์ข้างล่างนี้ได้เลยครับ Happy Testing!

Reference: https://martinfowler.com/bliki/ContractTest.html, https://jestjs.io/docs/en/snapshot-testing#snapshot-testing-with-jest

--

--