10
ENABLE FAST AND RELIABLE AUTOMATED TESTING (เปิดใช้งานการทดสอบอัตโนมัติที่รวดเร็วและเชื่อถือได้)
A t this point, Development และ QA ใช้สภาพแวดล้อมที่ใกล้เคียงกับ production ในการทำงานประจำวัน และเราสามารถ integrate และ run โค้ดของเราในสภาพแวดล้อมที่เหมือน production สำหรับทุก feature ที่ได้รับการยอมรับ โดยการเปลี่ยนแปลงทั้งหมดถูก check เข้าสู่ version control อย่างไรก็ตาม เรามักจะได้ผลลัพธ์ที่ไม่พึงประสงค์หากเรายังค้นหาและแก้ไขข้อผิดพลาดในช่วงการทดสอบที่แยกออกมา ซึ่งดำเนินการโดยแผนก QA ที่แยกต่างหากหลังจาก development ทั้งหมดเสร็จสมบูรณ์เท่านั้น และถ้าการทดสอบทำเพียงปีละไม่กี่ครั้ง นักพัฒนาจะรู้ถึงความผิดพลาดของตัวเองในอีกหลายเดือนหลังจากที่ตนเปลี่ยนแปลงโค้ดที่ทำให้เกิดข้อผิดพลาด เมื่อถึงเวลานั้น ความเชื่อมโยงระหว่างสาเหตุและผลกระทบก็เลือนหายไป การแก้ปัญหาต้องใช้การดับไฟ (firefighting) และการขุดคุ้ยหลักฐาน (archaeology) และที่แย่ที่สุดคือ ความสามารถในการเรียนรู้จากความผิดพลาดและนำมาปรับปรุงงานในอนาคตของเราลดลงอย่างมาก
Continuous Learning (การเรียนรู้อย่างต่อเนื่อง)
Does better observability mean less testing? (การสังเกตการณ์ที่ดีขึ้นหมายถึงการทดสอบที่น้อยลงหรือไม่)
ในยุคที่ระบบแบบ distributed systems กลายเป็นเรื่องปกติ หลายองค์กรลงทุนกับการทำ observability ที่ดีขึ้นสำหรับระบบ production ของตนอย่างถูกต้องแล้ว สิ่งนี้ทำให้บางคนเข้าใจไปว่า observability ที่ดีขึ้นหมายถึงความจำเป็นในการตรวจสอบซอฟต์แวร์ก่อน deploy นั้นลดน้อยลง ซึ่งเป็นความเข้าใจที่ผิด: incidents ใน production นั้นมีต้นทุนสูงและแก้ไขได้ยาก แม้จะมีเครื่องมือวัดและ tooling ที่ยอดเยี่ยมก็ตาม Distributed systems มีความซับซ้อนมากพอที่หากจะว่ากันแล้ว การทดสอบ service แต่ละตัวเพื่อความถูกต้องก่อน deploy นั้นยิ่งมีความสำคัญมากขึ้นไปอีก
การทดสอบอัตโนมัติยังช่วยแก้ปัญหาที่สำคัญและน่ากังวลอีกประการหนึ่ง Gary Gruver ตั้งข้อสังเกตว่า “หากไม่มีการทดสอบอัตโนมัติ ยิ่งเราเขียนโค้ดมากเท่าไหร่ ก็ยิ่งต้องใช้เวลาและเงินในการทดสอบโค้ดมากขึ้นเท่านั้น — ในกรณีส่วนใหญ่ นี่เป็น business model ที่ไม่สามารถ scale ได้เลยสำหรับองค์กรเทคโนโลยีใดๆ” 1
The Story of Google's Web Server (2005) (เรื่องราวของ Web Server ของ Google ปี 2005)
แม้ว่าปัจจุบัน Google จะเป็นตัวอย่างที่ชัดเจนของวัฒนธรรมที่ให้คุณค่ากับการทดสอบอัตโนมัติในระดับ scale แต่ก็ไม่ได้เป็นเช่นนั้นมาโดยตลอด ในปี 2005 เมื่อ Mike Bland เข้าร่วมองค์กร การ deploy ไปยัง Google.com มักจะมีปัญหาอย่างมาก โดยเฉพาะอย่างยิ่งสำหรับทีม Google Web Server (GWS)
ดังที่ Bland อธิบายไว้
ทีม GWS ตกอยู่ในสถานการณ์ในช่วงกลางยุค 2000 ที่ยากอย่างยิ่งที่จะเปลี่ยนแปลงอะไรกับ web server ซึ่งเป็นแอปพลิเคชัน C++ ที่จัดการทุก request ไปยังหน้าแรกของ Google และหน้าเว็บอื่นๆ ของ Google อีกมากมาย แม้ว่า Google.com จะสำคัญและโดดเด่นเพียงใด การอยู่ในทีม GWS ก็ไม่ใช่ตำแหน่งงานที่มีเกียรติ — มันมักเป็นที่ทิ้งขยะของทีมต่างๆ ที่สร้างฟังก์ชันการค้นหามากมาย ซึ่งทั้งหมดพัฒนาโค้ดแยกจากกันโดยอิสระ พวกเขามีปัญหาเช่น builds และ tests ที่ใช้เวลานานเกินไป โค้ดถูกนำขึ้น production โดยไม่ผ่านการทดสอบ และ teams ต่างๆ check in การเปลี่ยนแปลงขนาดใหญ่ที่ไม่บ่อยนักซึ่งขัดแย้งกับการเปลี่ยนแปลงของทีมอื่น 2
ผลที่ตามมามีมากมาย — ผลการค้นหาอาจมีข้อผิดพลาดหรือช้าจนยอมรับไม่ได้ ส่งผลกระทบต่อคำค้นหาหลายพันรายการบน Google.com ผลลัพธ์ที่อาจเกิดขึ้นไม่ใช่เพียงการสูญเสียรายได้ แต่รวมถึงความไว้วางใจของลูกค้าด้วย
Bland อธิบายว่ามันส่งผลต่อนักพัฒนาที่ deploy การเปลี่ยนแปลงอย่างไร: “ความกลายเป็นนักฆ่าความคิด (Fear became the mind-killer) ความกลัวหยุดสมาชิกใหม่ในทีมไม่ให้เปลี่ยนแปลงอะไร เพราะพวกเขาไม่เข้าใจระบบ แต่ความกลัวก็หยุดคนที่มีประสบการณ์ไม่ให้เปลี่ยนแปลงอะไร เพราะพวกเขาเข้าใจมันดีเกินไป” 3 * Bland เป็นส่วนหนึ่งของกลุ่มที่มุ่งมั่นจะแก้ปัญหานี้
Bharat Mediratta หัวหน้าทีม GWS เชื่อว่าการทดสอบอัตโนมัติจะช่วยได้ ดังที่ Bland อธิบายว่า
พวกเขาสร้างกฎที่ชัดเจน: จะไม่รับการเปลี่ยนแปลงใดๆ เข้าสู่ GWS โดยไม่มีการทดสอบอัตโนมัติประกอบ พวกเขาตั้งค่า continuous build และรักษาให้มันผ่านตลอดเวลาอย่างเคร่งครัด พวกเขาตั้งค่า monitoring สำหรับ test coverage และมั่นใจว่าระดับ test coverage เพิ่มขึ้นเรื่อยๆ เมื่อเวลาผ่านไป พวกเขาเขียน policy และคู่มือการทดสอบ และยืนกรานให้ผู้มีส่วนร่วมทั้งในและนอกทีมปฏิบัติตาม 5
ผลลัพธ์นั้นน่าทึ่ง ดังที่ Bland กล่าวไว้ว่า
GWS กลายเป็นหนึ่งในทีมที่ productive ที่สุดในบริษัทอย่างรวดเร็ว สามารถ integrate การเปลี่ยนแปลงจำนวนมากจากทีมต่างๆ ในทุกสัปดาห์ ในขณะที่ยังคงตารางการ release ที่รวดเร็ว สมาชิกใหม่ของทีมสามารถมีส่วนร่วมอย่างมีประสิทธิภาพกับระบบที่ซับซ้อนนี้ได้อย่างรวดเร็ว ต้องขอบคุณ test coverage ที่ดีและ code health ที่แข็งแรง ท้ายที่สุด นโยบายที่รุนแรงนี้ทำให้หน้าแรกของ Google.com สามารถขยายความสามารถได้อย่างรวดเร็วและเติบโตในภูมิทัศน์เทคโนโลยีที่แข่งขันสูงและเปลี่ยนแปลงอย่างรวดเร็วได้อย่างน่าอัศจรรย์ 6
แต่ GWS ก็ยังเป็นทีมที่ค่อนข้างเล็กในบริษัทขนาดใหญ่ที่กำลังเติบโต ทีมต้องการขยายแนวปฏิบัติเหล่านี้ไปทั่วทั้งองค์กร ดังนั้น testing grouplet จึงถือกำเนิดขึ้น ซึ่งเป็นกลุ่มวิศวกรที่ไม่เป็นทางการที่ต้องการยกระดับแนวปฏิบัติการทดสอบอัตโนมัติไปทั่วทั้งองค์กร ในช่วงห้าปีถัดมา พวกเขาช่วยกระจายวัฒนธรรมการทดสอบอัตโนมัตินี้ไปทั่วทั้ง Google 7 †
ดังที่ Rachel Potvin และ Josh Levenberg อธิบาย ระบบของ Google ได้พัฒนาไปจนสามารถทดสอบ commits นับพันจากนักพัฒนานับพันคนได้โดยอัตโนมัติทุกวัน:
Google มีโครงสร้างพื้นฐานการทดสอบอัตโนมัติที่เริ่ม rebuild dependencies ที่ได้รับผลกระทบทั้งหมดในเกือบทุกการเปลี่ยนแปลงที่ commit เข้าสู่ repository หากการเปลี่ยนแปลงทำให้เกิด build breakage ในวงกว้าง ระบบจะถูกตั้งค่าให้ยกเลิกการเปลี่ยนแปลงนั้นโดยอัตโนมัติ เพื่อลดโอกาสที่โค้ดเสียจะถูก commit ตั้งแต่แรก โครงสร้างพื้นฐาน “presubmit” ที่ปรับแต่งได้สูงของ Google จะทำการทดสอบและวิเคราะห์การเปลี่ยนแปลงโดยอัตโนมัติก่อนที่จะถูกเพิ่มเข้าไปใน codebase ชุดการวิเคราะห์ presubmit ระดับโลกจะถูก run สำหรับการเปลี่ยนแปลงทั้งหมด และเจ้าของโค้ดสามารถสร้างการวิเคราะห์แบบกำหนดเองที่ run เฉพาะบน directories ที่พวกเขากำหนดภายใน codebase 8
Eran Messeri วิศวกรในกลุ่ม Google Developer Infrastructure กล่าวว่า “ความล้มเหลวขนาดใหญ่เกิดขึ้นเป็นครั้งคราว คุณจะได้รับข้อความ instant message มากมายและวิศวกรมาเคาะประตูคุณ [เมื่อ deployment pipeline พัง] เราต้องแก้ไขมันทันทีเพราะนักพัฒนาไม่สามารถ commit โค้ดได้อีกต่อไป ดังนั้น เราจึงต้องการให้การ roll back ทำได้ง่ายมาก” 9
สิ่งที่ทำให้ระบบนี้ทำงานได้ที่ Google คือความเป็นมืออาชีพทางวิศวกรรม (engineering professionalism) และวัฒนธรรมที่ไว้วางใจซึ่งกันและกันสูง ที่ถือว่าทุกคนต้องการทำงานที่ดี รวมถึงความสามารถในการตรวจจับและแก้ไขปัญหาได้อย่างรวดเร็ว Messeri อธิบายว่า
ไม่มีนโยบายที่เข้มงวดที่ Google เช่น “ถ้าคุณทำ production เสียมากกว่าสิบโปรเจกต์ คุณมี SLA ที่ต้องแก้ไขปัญหาภายในสิบนาที” แต่กลับมีความเคารพซึ่งกันและกันระหว่างทีมและข้อตกลงโดยนัยที่ว่าทุกคนจะทำทุกวิถีทางเพื่อให้ deployment pipeline ทำงานต่อไป เราทุกคนรู้ว่าวันหนึ่งฉันอาจทำโปรเจกต์คุณพังโดยไม่ได้ตั้งใจ และวันถัดไปคุณอาจทำโปรเจกต์ฉันพัง 10
สิ่งที่ทีมต่างๆ ที่ Google ทำได้สำเร็จทำให้บริษัทกลายเป็นหนึ่งในองค์กรเทคโนโลยีที่ productive ที่สุดในโลก ภายในปี 2016 การทดสอบอัตโนมัติและ continuous integration ที่ Google ทำให้ทีมขนาดเล็กกว่าสี่พันทีมสามารถทำงานร่วมกันและคงความ productive ไว้ได้ โดยทั้งหมดพัฒนา integrate ทดสอบ และ deploy โค้ดของตนขึ้น production พร้อมกัน โค้ดส่วนใหญ่ของ Google อยู่ใน shared repository เดียว ประกอบด้วยไฟล์หลายพันล้านไฟล์ ซึ่งทั้งหมดถูก build และ integrate อย่างต่อเนื่อง และในปี 2014 มีการเปลี่ยนแปลงโค้ดประมาณ 15 ล้านบรรทัดในไฟล์ประมาณ 250,000 ไฟล์ใน repository ของ Google ในแต่ละสัปดาห์ 11 สถิติที่น่าประทับใจอื่นๆ เกี่ยวกับโครงสร้างพื้นฐานโค้ดของพวกเขา (ณ ปี 2016) ได้แก่: 12
• โดยทั่วไปมี code commits 40,000 ครั้งต่อวัน (16,000 ครั้งจากวิศวกรและ 24,000 ครั้งจากระบบอัตโนมัติ)
• builds 50,000 ครั้งต่อวัน (ในวันธรรมดาอาจเกิน 90,000 ครั้ง)
• ชุดทดสอบอัตโนมัติ 120,000 ชุด
• ทดสอบเคส 75 ล้านเคสต่อวัน
• กว่า 99% ของไฟล์ที่เก็บใน version control ของ Google สามารถมองเห็นได้โดยวิศวกรประจำของ Google ทุกคน
• codebase ประกอบด้วยไฟล์ประมาณหนึ่งพันล้านไฟล์และมีประวัติ commits ประมาณ 35 ล้านครั้ง
• repository มีโค้ดประมาณสองพันล้านบรรทัดในไฟล์ source code ต้นฉบับเก้าล้านไฟล์ และมีขนาดข้อมูล 86 TB
แม้ว่าการทดสอบอัตโนมัติในระดับ Google อาจไม่ใช่เป้าหมายของหลายๆ บริษัท แต่ประโยชน์ของการทดสอบอัตโนมัตินั้นเป็นไปได้สำหรับทุกคน ในส่วนที่เหลือของบทนี้ เราจะพูดถึงแนวปฏิบัติ continuous integration ที่จำเป็นเพื่อให้ได้ผลลัพธ์ที่คล้ายคลึงกัน
Continuously Build, Test, and Integrate Our Code and Environments (การ Build ทดสอบ และ Integrate โค้ดและสภาพแวดล้อมของเราอย่างต่อเนื่อง)
เป้าหมายของเราคือการสร้างคุณภาพให้เป็นส่วนหนึ่งของผลิตภัณฑ์ตั้งแต่ช่วงแรกที่สุด โดยให้นักพัฒนาสร้าง automated tests เป็นส่วนหนึ่งของงานประจำวันของพวกเขา สิ่งนี้สร้าง feedback loop ที่รวดเร็วซึ่งช่วยให้นักพัฒนาค้นหาปัญหาได้ตั้งแต่เนิ่นๆ และแก้ไขได้อย่างรวดเร็วในช่วงที่มีข้อจำกัดน้อยที่สุด (เช่น เวลา ทรัพยากร)
ในขั้นตอนนี้ เราสร้างชุดทดสอบอัตโนมัติที่เพิ่มความถี่ของการ integration และการทดสอบโค้ดและสภาพแวดล้อมของเราจากแบบเป็นระยะ (periodic) เป็นแบบต่อเนื่อง (continuous) เราทำสิ่งนี้โดยการสร้าง deployment pipeline ซึ่งจะทำการ integration โค้ดและสภาพแวดล้อมของเรา และ trigger ชุดการทดสอบทุกครั้งที่มีการเปลี่ยนแปลงใหม่ถูก check เข้าสู่ version control ‡ (ดู Figure 10.1 .)
Figure 10.1: The Deployment Pipeline (Deployment Pipeline)
ที่มา: Humble and Farley, Continuous Delivery , 3.
Deployment pipeline ซึ่งถูกนิยามครั้งแรกโดย Jez Humble และ David Farley ในหนังสือ Continuous Delivery: Reliable Software Releases Through Build, Test, and Deployment Automation ทำให้มั่นใจว่าโค้ดทั้งหมดที่ check เข้าสู่ version control จะถูก build และทดสอบโดยอัตโนมัติในสภาพแวดล้อมที่เหมือน production 14 การทำเช่นนี้ทำให้เราค้นหาข้อผิดพลาดในการ build การทดสอบ หรือ integration ได้ทันทีที่การเปลี่ยนแปลงถูกนำเข้า ทำให้เราแก้ไขได้ทันที หากทำอย่างถูกต้อง จะทำให้เรามั่นใจได้เสมอว่าเราอยู่ในสถานะที่สามารถ deploy และ ship ได้
เพื่อให้บรรลุเป้าหมายนี้ เราต้องสร้างกระบวนการ build และทดสอบอัตโนมัติที่ run ในสภาพแวดล้อมเฉพาะ (dedicated environments) ซึ่งเป็นสิ่งสำคัญด้วยเหตุผลดังต่อไปนี้:
• กระบวนการ build และทดสอบของเราสามารถ run ได้ตลอดเวลา โดยไม่ขึ้นอยู่กับนิสัยการทำงานของวิศวกรแต่ละคน
• กระบวนการ build และทดสอบที่แยกออกมาช่วยให้เราเข้าใจ dependencies ทั้งหมดที่จำเป็นในการ build, package, run, และทดสอบโค้ดของเรา (กล่าวคือ กำจัดปัญหา “มันทำงานบน laptop ของ developer แต่พังใน production”)
• เราสามารถ package แอปพลิเคชันของเราเพื่อให้สามารถติดตั้งโค้ดและการกำหนดค่าเข้าไปในสภาพแวดล้อมได้ซ้ำๆ (เช่น บน Linux ใช้ RPM, yum, npm; บน Windows ใช้ OneGet; หรือใช้ระบบ packaging เฉพาะ framework เช่น EAR และ WAR files สำหรับ Java, gems สำหรับ Ruby ฯลฯ)
• แทนที่จะใส่โค้ดใน packages เราอาจเลือกที่จะ package แอปพลิเคชันของเราลงใน containers ที่สามารถ deploy ได้ (เช่น Docker, Rkt, LXD, AMIs)
• สภาพแวดล้อมสามารถทำให้ใกล้เคียง production มากขึ้นในลักษณะที่สอดคล้องและทำซ้ำได้ (เช่น compiler ถูกลบออกจากสภาพแวดล้อม debugging flags ถูกปิด ฯลฯ)
Deployment pipeline ของเราทำการตรวจสอบหลังจากทุกการเปลี่ยนแปลงว่าโค้ดของเราสามารถ integrate เข้าสู่สภาพแวดล้อมที่เหมือน production ได้สำเร็จ มันกลายเป็นแพลตฟอร์มที่ผู้ทดสอบใช้ขอและรับรอง builds ระหว่างการทดสอบการยอมรับ (acceptance testing) และการทดสอบการใช้งาน (usability testing) และมันจะ run การตรวจสอบประสิทธิภาพและความปลอดภัยโดยอัตโนมัติ
นอกจากนี้ มันยังจะถูกใช้เพื่อสร้าง builds แบบ self-service สำหรับสภาพแวดล้อม UAT (user acceptance testing), integration testing, และ security testing ในขั้นตอนถัดๆ ไป เมื่อเรา evolution deployment pipeline มันจะถูกใช้เพื่อจัดการกิจกรรมทั้งหมดที่จำเป็นในการนำการเปลี่ยนแปลงของเราจาก version control ไปสู่การ deploy
มีเครื่องมือหลากหลายที่ถูกออกแบบมาเพื่อให้ความสามารถของ deployment pipeline หลายตัวเป็น open source (เช่น Jenkins, Go.cd, Concourse, Bamboo, Microsoft Team Foundation Server, TeamCity, Gitlab CI รวมถึงโซลูชันบนคลาวด์อย่าง CircleCi และ TravisCI) §
เราเริ่ม deployment pipeline ด้วยการ run commit stage ซึ่ง build และ package ซอฟต์แวร์ run unit tests อัตโนมัติ และทำการตรวจสอบเพิ่มเติม เช่น static code analysis, การวิเคราะห์ duplication และ test coverage และการตรวจสอบ style ¶ หากสำเร็จ ขั้นตอนนี้จะ trigger acceptance stage ซึ่งจะ deploy packages ที่สร้างใน commit stage โดยอัตโนมัติเข้าสู่สภาพแวดล้อมที่เหมือน production และ run acceptance tests อัตโนมัติ
เมื่อการเปลี่ยนแปลงได้รับการยอมรับเข้าสู่ version control เราต้องการ package โค้ดของเราเพียงครั้งเดียว เพื่อให้ packages เดียวกันถูกใช้ในการ deploy โค้ดตลอดทั้ง deployment pipeline ของเรา การทำเช่นนี้จะทำให้โค้ดถูก deploy เข้าสู่สภาพแวดล้อม integrated test และ staging ในลักษณะเดียวกับที่ deploy ขึ้น production ซึ่งช่วยลดความแตกต่างที่อาจทำให้เกิดข้อผิดพลาด downstream ที่ยากต่อการวินิจฉัย (เช่น การใช้ compiler, compiler flags, library versions, หรือ configurations ที่ต่างกัน) **
เป้าหมายของ deployment pipeline คือการให้ feedback ที่เร็วที่สุดเท่าที่เป็นไปได้แก่ทุกคนใน value stream โดยเฉพาะนักพัฒนา ว่าการเปลี่ยนแปลงใดๆ ทำให้เราหลุดออกจากสถานะที่พร้อม deploy ซึ่งอาจเป็นการเปลี่ยนแปลงโค้ด สภาพแวดล้อมใดๆ ของเรา การทดสอบอัตโนมัติของเรา หรือแม้แต่โครงสร้างพื้นฐานของ deployment pipeline (เช่น การตั้งค่า configuration ของ Jenkins)
ด้วยเหตุนี้ โครงสร้างพื้นฐานของ deployment pipeline จึงมีความสำคัญต่อกระบวนการพัฒนาของเราไม่แพ้โครงสร้างพื้นฐานของ version control Deployment pipeline ของเรายังเก็บประวัติของแต่ละ code build รวมถึงข้อมูลว่าการทดสอบใดถูก run บน build ใด build ใดถูก deploy ไปยังสภาพแวดล้อมใด และผลการทดสอบเป็นอย่างไร เมื่อรวมกับข้อมูลในประวัติ version control เราสามารถระบุได้อย่างรวดเร็วว่าอะไรทำให้ deployment pipeline ของเราพัง และมีแนวโน้มว่าจะแก้ไขข้อผิดพลาดนั้นอย่างไร ข้อมูลนี้ยังช่วยให้เราตอบสนองข้อกำหนดด้านหลักฐานสำหรับการตรวจสอบ (audit) และการปฏิบัติตามข้อกำหนด (compliance) โดยหลักฐานถูกสร้างขึ้นโดยอัตโนมัติซึ่งเป็นส่วนหนึ่งของงานประจำวัน
เมื่อเรามีโครงสร้างพื้นฐานของ deployment pipeline ที่ใช้งานได้แล้ว เราต้องสร้างแนวปฏิบัติ continuous integration ซึ่งต้องมีความสามารถสามประการ:
• ชุดการทดสอบอัตโนมัติที่ครอบคลุมและเชื่อถือได้ ซึ่งตรวจสอบว่าเราอยู่ในสถานะที่พร้อม deploy
• วัฒนธรรมที่ “หยุดสายการผลิตทั้งหมด” เมื่อการทดสอบตรวจสอบของเราล้มเหลว
• นักพัฒนาทำงานใน batches ขนาดเล็กบน trunk แทนที่จะใช้ long-lived feature branches
ในส่วนถัดไป เราจะอธิบายว่าทำไมการทดสอบอัตโนมัติที่รวดเร็วและเชื่อถือได้จึงจำเป็น และวิธีสร้างมันขึ้นมา
Build a Fast and Reliable Automated Validation Test Suite (สร้างชุดทดสอบตรวจสอบอัตโนมัติที่รวดเร็วและเชื่อถือได้)
ในขั้นตอนก่อนหน้านี้ เราเริ่มสร้างโครงสร้างพื้นฐานการทดสอบอัตโนมัติที่ตรวจสอบว่าเรามี green build (กล่าวคือ สิ่งที่อยู่ใน version control อยู่ในสถานะที่สามารถ build และ deploy ได้) เพื่อเน้นย้ำว่าทำไมเราจึงต้องทำขั้นตอนการ integration และการทดสอบนี้อย่างต่อเนื่อง ลองพิจารณาว่าจะเกิดอะไรขึ้นเมื่อเราทำการดำเนินการนี้เป็นระยะเท่านั้น เช่น ในระหว่างกระบวนการ nightly build
สมมติว่าเรามีทีมนักพัฒนาสิบคน ทุกคน check โค้ดของตนเข้าสู่ version control ทุกวัน และนักพัฒนาคนหนึ่งนำการเปลี่ยนแปลงที่ทำให้ nightly build และ test job ของเราเสีย ในสถานการณ์นี้ เมื่อเราค้นพบในวันถัดไปว่าเราไม่มี green build อีกต่อไป มันจะใช้เวลาเป็นนาที หรือน่าจะเป็นชั่วโมง ที่ทีมพัฒนาของเราจะหาว่าการเปลี่ยนแปลงใดทำให้เกิดปัญหา ใครเป็นคนนำเข้ามา และวิธีแก้ไข
ที่แย่กว่านั้น สมมติว่าปัญหาไม่ได้เกิดจากการเปลี่ยนแปลงโค้ด แต่เกิดจากปัญหาของ test environment (เช่น การตั้งค่า configuration ที่ไม่ถูกต้องที่ใดที่หนึ่ง) ทีมพัฒนาอาจเชื่อว่าแก้ไขปัญหาแล้วเพราะ unit tests ทั้งหมดผ่าน เพียงแต่มาค้นพบว่าการทดสอบจะยังคงล้มเหลวในคืนนั้น
ยิ่งทำให้ปัญหาซับซ้อนขึ้นไปอีก การเปลี่ยนแปลงอีกสิบรายการจะถูก check เข้าสู่ version control โดยทีมในวันนั้น แต่ละการเปลี่ยนแปลงเหล่านี้มีโอกาสที่จะนำข้อผิดพลาดเพิ่มเติมที่อาจทำให้การทดสอบอัตโนมัติของเราเสีย ทำให้การวินิจฉัยและแก้ไขปัญหาสำเร็จนั้นยากยิ่งขึ้น
กล่าวโดยสรุป feedback ที่ช้าและเป็นระยะเป็นสิ่งที่อันตราย โดยเฉพาะอย่างยิ่งสำหรับทีมพัฒนาขนาดใหญ่ ปัญหายิ่งน่ากังวลมากขึ้นเมื่อเรามีนักพัฒนาหลายสิบ หลายร้อย หรือหลายพันคนที่ check การเปลี่ยนแปลงของตนเข้าสู่ version control ในแต่ละวัน ผลลัพธ์คือ builds และการทดสอบอัตโนมัติของเราพังบ่อยครั้ง และนักพัฒนาก็หยุด check การเปลี่ยนแปลงของตนเข้าสู่ version control (“จะไปทำไม ในเมื่อ builds และ tests ก็พังอยู่ตลอด”) แต่พวกเขากลับรอที่จะ integrate โค้ดของตนเมื่อสิ้นสุดโปรเจกต์ ซึ่งนำไปสู่ผลลัพธ์ที่ไม่พึงประสงค์ทั้งหมดของ batch size ขนาดใหญ่ big-bang integrations และการ deploy ขึ้น production ††
เพื่อป้องกันสถานการณ์นี้ เราต้องมีการทดสอบอัตโนมัติที่รวดเร็วซึ่ง run ภายในสภาพแวดล้อม build และทดสอบของเราเมื่อใดก็ตามที่มีการเปลี่ยนแปลงใหม่ถูกนำเข้าสู่ version control ด้วยวิธีนี้ เราสามารถค้นหาและแก้ไขปัญหาใดๆ ได้ทันที ดังที่ตัวอย่างของ Google Web Server แสดงให้เห็น การทำเช่นนี้ทำให้เรามั่นใจได้ว่า batches ของเรายังคงมีขนาดเล็ก และ ณ เวลาใดๆ เรายังคงอยู่ในสถานะที่พร้อม deploy
โดยทั่วไป การทดสอบอัตโนมัติแบ่งออกเป็นประเภทต่างๆ ดังต่อไปนี้ โดยเรียงจากเร็วที่สุดไปช้าที่สุด:
• Unit tests: โดยทั่วไปแล้วจะทดสอบ method, class, หรือ function เดียวๆ แบบแยกส่วน (isolation) เพื่อให้ความมั่นใจแก่นักพัฒนาว่าโค้ดของตนทำงานตามที่ออกแบบไว้ ด้วยเหตุผลหลายประการ รวมถึงความจำเป็นในการทำให้การทดสอบของเราเร็วและไม่มีสถานะ (stateless) unit tests มักจะ “stub out” ฐานข้อมูลและ dependencies ภายนอกอื่นๆ (เช่น functions ถูกปรับให้ return ค่าสถิตที่กำหนดไว้ล่วงหน้า แทนที่จะเรียกฐานข้อมูลจริง) ‡‡
• Acceptance tests: โดยทั่วไปแล้วจะทดสอบแอปพลิเคชันโดยรวมเพื่อให้ความมั่นใจว่าฟังก์ชันการทำงานระดับสูงทำงานตามที่ออกแบบไว้ (เช่น เกณฑ์การยอมรับทางธุรกิจสำหรับ user story, ความถูกต้องของ API) และไม่มี regression errors เกิดขึ้น (กล่าวคือ เราไม่ได้ทำให้ฟังก์ชันที่ทำงานได้ถูกต้องก่อนหน้านี้เสียไป) Humble และ Farley นิยามความแตกต่างระหว่าง unit และ acceptance testing ไว้ว่า “เป้าหมายของ unit test คือการแสดงว่าส่วนใดส่วนหนึ่งของแอปพลิเคชันทำสิ่งที่โปรแกรมเมอร์ตั้งใจให้ทำ... วัตถุประสงค์ของ acceptance tests คือการพิสูจน์ว่าแอปพลิเคชันของเราทำสิ่งที่ลูกค้าตั้งใจให้ทำ ไม่ใช่เพียงว่ามันทำงานในแบบที่โปรแกรมเมอร์คิดว่าควรจะทำงาน” 15 หลังจาก build ผ่าน unit tests ของเรา deployment pipeline จะนำไป run กับ acceptance tests ของเรา build ใดก็ตามที่ผ่าน acceptance tests จะถูกนำไปให้สำหรับการทดสอบด้วยมือ (manual testing) เช่น exploratory testing, UI testing ฯลฯ รวมถึง integration testing
• Integration tests: Integration tests คือการทดสอบที่เรามั่นใจว่าแอปพลิเคชันของเราสามารถโต้ตอบกับแอปพลิเคชันและ services ใน production จริงได้อย่างถูกต้อง แทนที่จะเรียกใช้ interfaces ที่ถูก stub ไว้ ดังที่ Humble และ Farley ตั้งข้อสังเกตว่า “งานส่วนใหญ่ในสภาพแวดล้อม SIT [system integration testing] เกี่ยวข้องกับการ deploy เวอร์ชันใหม่ของแต่ละแอปพลิเคชันจนกว่าพวกมันทั้งหมดจะทำงานร่วมกันได้ ในสถานการณ์นี้ smoke test มักจะเป็นชุด acceptance tests ที่สมบูรณ์ซึ่ง run กับทั้งแอปพลิเคชัน” 16 Integration tests จะถูกดำเนินการบน builds ที่ผ่าน unit และ acceptance tests ของเราแล้ว เนื่องจาก integration tests มักจะเปราะบาง (brittle) เราจึงต้องการลดจำนวน integration tests และค้นหาข้อบกพร่องให้มากที่สุดเท่าที่จะทำได้ในช่วง unit และ acceptance testing ความสามารถในการใช้ virtual หรือ simulated versions ของ remote services เมื่อ run acceptance tests กลายเป็นข้อกำหนดทางสถาปัตยกรรมที่จำเป็น
เมื่อเผชิญกับแรงกดดันด้านกำหนดส่งงาน นักพัฒนาอาจหยุดสร้าง unit tests เป็นส่วนหนึ่งของงานประจำวัน ไม่ว่าเราจะนิยาม “done” ไว้อย่างไรก็ตาม เพื่อตรวจจับสิ่งนี้ เราอาจเลือกที่จะวัดและทำให้ test coverage มองเห็นได้ (เป็นฟังก์ชันของจำนวน classes, บรรทัดของโค้ด, permutations ฯลฯ) หรือแม้กระทั่งทำให้ validation test suite ล้มเหลวเมื่อมันลดต่ำกว่าระดับที่กำหนด (เช่น เมื่อน้อยกว่า 80% ของ classes ของเรามี unit tests) §§
Martin Fowler สังเกตว่า โดยทั่วไปแล้ว
build [และกระบวนการทดสอบ] ที่ใช้เวลาสิบนาทีนั้นอยู่ในเกณฑ์ที่สมเหตุสมผลอย่างสมบูรณ์... [ขั้นแรกเรา] ทำ compilation และ run tests ที่เป็น localized unit tests โดยที่ database ถูก stub อย่างสมบูรณ์ การทดสอบเช่นนี้สามารถ run ได้เร็วมาก โดยอยู่ในแนวทางสิบนาที แต่ bugs ใดๆ ที่เกี่ยวข้องกับการโต้ตอบในระดับใหญ่ โดยเฉพาะอย่างยิ่งที่เกี่ยวข้องกับ database จริง จะไม่ถูกพบ การ build ในขั้นตอนที่สองจะ run ชุดการทดสอบที่แตกต่างกัน [acceptance tests] ซึ่งเข้าถึง database จริงและเกี่ยวข้องกับพฤติกรรม end-to-end มากขึ้น ชุดทดสอบนี้อาจใช้เวลาสองสามชั่วโมงในการ run 17
Catch Errors as Early in Our Automated Testing as Possible (จับข้อผิดพลาดให้เร็วที่สุดเท่าที่จะทำได้ในการทดสอบอัตโนมัติของเรา)
เป้าหมายการออกแบบเฉพาะของชุดทดสอบอัตโนมัติของเราคือการค้นหาข้อผิดพลาดให้เร็วที่สุดในขั้นตอนการทดสอบเท่าที่เป็นไปได้ นี่คือเหตุผลที่เราจึง run การทดสอบอัตโนมัติที่เร็วกว่า (เช่น unit tests) ก่อนการทดสอบอัตโนมัติที่ช้ากว่า (เช่น acceptance และ integration tests) ซึ่งทั้งคู่จะถูก run ก่อนการทดสอบด้วยมือใดๆ
ข้อสรุปอีกประการหนึ่งของหลักการนี้คือ ข้อผิดพลาดใดๆ ควรถูกค้นพบด้วยการทดสอบประเภทที่เร็วที่สุดเท่าที่เป็นไปได้ หากข้อผิดพลาดส่วนใหญ่ของเราถูกพบใน acceptance และ integration tests feedback ที่เราให้แก่นักพัฒนาจะช้ากว่า unit tests เป็นอันดับมาก — และ integration testing ต้องใช้สภาพแวดล้อมการทดสอบ integration ที่ scarce และซับซ้อน ซึ่งสามารถใช้ได้ทีละทีมเท่านั้น ยิ่งทำให้ feedback ล่าช้าไปอีก
ยิ่งไปกว่านั้น ไม่เพียงแต่ข้อผิดพลาดที่ตรวจพบระหว่าง integration testing จะยากและใช้เวลานานสำหรับนักพัฒนาในการจำลอง (reproduce) แม้แต่การตรวจสอบว่ามันได้รับการแก้ไขแล้วก็ยังทำได้ยาก (เช่น นักพัฒนาสร้าง fix แต่ต้องรอสี่ชั่วโมงเพื่อรู้ว่า integration tests ผ่านหรือไม่)
ดังนั้น เมื่อใดก็ตามที่เราพบข้อผิดพลาดด้วย acceptance หรือ integration test เราควรสร้าง unit test ที่สามารถค้นหาข้อผิดพลาดได้เร็วกว่า ก่อนหน้า และถูกกว่า Martin Fowler อธิบายแนวคิดของ “ideal testing pyramid” ที่เราสามารถจับข้อผิดพลาดส่วนใหญ่ได้โดยใช้ unit tests ของเรา 18 (ดู Figure 10.2 .) ในทางตรงกันข้าม ในหลายๆ โปรแกรมการทดสอบ สิ่งที่ตรงกันข้ามคือความจริง โดยที่การลงทุนส่วนใหญ่อยู่ในการทดสอบด้วยมือและการทดสอบ integration
Figure 10.2: The Ideal and Non-Ideal Automated Testing Pyramids (พีระมิดการทดสอบอัตโนมัติในอุดมคติและไม่เป็นอุดมคติ)
ที่มา: Martin Fowler, “TestPyramid,” MartinFowler.com .
ถ้าเราพบว่า unit หรือ acceptance tests นั้นยากและมีค่าใช้จ่ายสูงในการเขียนและบำรุงรักษา ก็เป็นไปได้ว่าเรามีสถาปัตยกรรมที่ tightly coupled มากเกินไป ซึ่งการแยกส่วนระหว่าง module boundaries ของเรานั้นไม่มีอยู่อีกต่อไป (หรืออาจไม่เคยมีมาก่อน) ในกรณีนี้ เราจะต้องสร้างระบบที่ loosely coupled มากขึ้นเพื่อให้ modules สามารถถูกทดสอบอย่างอิสระโดยไม่ต้องพึ่งพาสภาพแวดล้อม integration Acceptance test suites สำหรับแม้แต่แอปพลิเคชันที่ซับซ้อนที่สุดที่ run ในเวลาไม่กี่นาทีก็เป็นไปได้
Ensure Tests Run Quickly (In Parallel, If Necessary) (ทำให้การทดสอบ run เร็ว (แบบขนานถ้าจำเป็น))
เนื่องจากเราต้องการให้การทดสอบของเรา run อย่างรวดเร็ว เราจึงต้องออกแบบการทดสอบให้ run แบบขนาน (parallel) ซึ่งอาจครอบคลุมเซิร์ฟเวอร์ต่างๆ มากมาย เราอาจต้องการ run การทดสอบประเภทต่างๆ แบบขนานด้วย ตัวอย่างเช่น เมื่อ build ผ่าน acceptance tests ของเรา เราอาจ run performance testing พร้อมกับ security testing ดังที่แสดงใน Figure 10.3 เราอาจอนุญาตหรือไม่อนุญาตให้ทำ manual exploratory testing จนกว่า build จะผ่านการทดสอบอัตโนมัติทั้งหมดของเรา — ซึ่งช่วยให้ feedback เร็วขึ้น แต่ก็อาจทำให้มีการทดสอบด้วยมือบน builds ที่สุดท้ายแล้วจะล้มเหลว
Figure 10.3: Running Automated and Manual Tests in Parallel (การ run การทดสอบอัตโนมัติและการทดสอบด้วยมือแบบขนาน)
ที่มา: Humble and Farley, Continuous Delivery , Kindle edition, ตำแหน่ง 3868
เราทำให้ build ใดๆ ที่ผ่านการทดสอบอัตโนมัติทั้งหมดของเราพร้อมใช้งานสำหรับ exploratory testing รวมถึงการทดสอบด้วยมือรูปแบบอื่นๆ หรือการทดสอบที่ใช้ทรัพยากรมาก (เช่น performance testing) เราต้องการทำการทดสอบทั้งหมดนี้บ่อยเท่าที่เป็นไปได้และปฏิบัติได้ ไม่ว่าจะอย่างต่อเนื่องหรือตามตารางเวลา
ผู้ทดสอบใดๆ (ซึ่งรวมถึงนักพัฒนาทั้งหมดของเรา) ควรใช้ build ล่าสุดที่ผ่านการทดสอบอัตโนมัติทั้งหมด แทนที่จะรอให้นักพัฒนาระบุว่า build ใดพร้อมทดสอบ การทำเช่นนี้ทำให้เรามั่นใจว่าการทดสอบเกิดขึ้นเร็วที่สุดในกระบวนการเท่าที่เป็นไปได้
Write Our Automated Tests before We Write the Code (“Test-Driven Development”) (เขียนการทดสอบอัตโนมัติก่อนเขียนโค้ด — Test-Driven Development)
วิธีที่มีประสิทธิภาพที่สุดวิธีหนึ่งในการทำให้เรามีการทดสอบอัตโนมัติที่เชื่อถือได้คือการเขียนการทดสอบเหล่านั้นเป็นส่วนหนึ่งของงานประจำวัน โดยใช้เทคนิคเช่น test-driven development (TDD) และ acceptance test-driven development (ATDD) นั่นคือการเริ่มต้นทุกการเปลี่ยนแปลงในระบบโดยการเขียน automated test ก่อนที่ตรวจสอบว่าพฤติกรรมที่คาดหวังล้มเหลว จากนั้นจึงเขียนโค้ดเพื่อให้การทดสอบผ่าน
เทคนิคนี้ถูกพัฒนาโดย Kent Beck ในช่วงปลายทศวรรษ 1990 ซึ่งเป็นส่วนหนึ่งของ Extreme Programming และมีสามขั้นตอนดังนี้: 19
• ทำให้แน่ใจว่าการทดสอบล้มเหลว “เขียน test สำหรับฟังก์ชันถัดไปที่คุณต้องการเพิ่ม” Check in
• ทำให้แน่ใจว่าการทดสอบผ่าน “เขียน functional code จนกว่าการทดสอบจะผ่าน” Check in
• “Refactor ทั้งโค้ดใหม่และเก่าเพื่อให้มีโครงสร้างที่ดี” ทำให้แน่ใจว่าการทดสอบผ่าน Check in อีกครั้ง
ชุดทดสอบอัตโนมัติเหล่านี้ถูก check เข้าสู่ version control ควบคู่ไปกับโค้ดของเรา ซึ่งเป็นข้อกำหนดของระบบที่ยังมีชีวิตและทันสมัยอยู่เสมอ (living, up-to-date specification) นักพัฒนาที่ต้องการเข้าใจวิธีการใช้ระบบสามารถดูชุดทดสอบนี้เพื่อค้นหาตัวอย่างการทำงานของ API ของระบบ ¶¶
Automate as Many of Our Manual Tests as Possible (ทำให้การทดสอบด้วยมือของเราเป็นอัตโนมัติให้มากที่สุดเท่าที่เป็นไปได้)
เป้าหมายของเราคือการค้นหาข้อผิดพลาดของโค้ดให้ได้มากที่สุดผ่านชุดทดสอบอัตโนมัติของเรา เพื่อลดการพึ่งพาการทดสอบด้วยมือ ในการนำเสนอของเธอที่ Flowcon ปี 2013 หัวข้อ “On the Care and Feeding of Feedback Cycles” Elisabeth Hendrickson กล่าวว่า “ถึงแม้การทดสอบจะสามารถทำเป็นอัตโนมัติได้ แต่การสร้างคุณภาพนั้นไม่สามารถทำได้ การให้มนุษย์ execute การทดสอบที่ควรจะเป็นอัตโนมัตินั้นเป็นการสิ้นเปลืองศักยภาพของมนุษย์” 21
การทำเช่นนี้ทำให้เราสามารถให้ผู้ทดสอบทั้งหมดของเรา (ซึ่งรวมถึงนักพัฒนา) ทำงานในกิจกรรมที่มีมูลค่าสูงที่ไม่สามารถทำเป็นอัตโนมัติได้ เช่น exploratory testing หรือการปรับปรุงกระบวนการทดสอบเอง
อย่างไรก็ตาม การทำให้การทดสอบด้วยมือทั้งหมดของเราเป็นอัตโนมัติเพียงอย่างเดียวอาจสร้างผลลัพธ์ที่ไม่พึงประสงค์ — เราไม่ต้องการการทดสอบอัตโนมัติที่ไม่น่าเชื่อถือหรือสร้าง false positives (กล่าวคือ การทดสอบที่ควรจะผ่านเพราะโค้ดถูกต้องตามฟังก์ชัน แต่ล้มเหลวเนื่องจากปัญหาเช่นประสิทธิภาพช้าทำให้ timeout, สถานะเริ่มต้นที่ไม่ถูกควบคุม, หรือสถานะที่ไม่ตั้งใจเนื่องจากการใช้ database stubs หรือสภาพแวดล้อมการทดสอบที่ใช้ร่วมกัน) 22
การทดสอบที่ไม่น่าเชื่อถือซึ่งสร้าง false positives ก่อให้เกิดปัญหาสำคัญ: การเสียเวลาอันมีค่า (เช่น บังคับให้นักพัฒนาต้องรันการทดสอบซ้ำเพื่อดูว่ามีปัญหาจริงหรือไม่) เพิ่มความพยายามโดยรวมในการ run และตีความผลการทดสอบ และมักส่งผลให้นักพัฒนาที่เครียดเพิกเฉยต่อผลการทดสอบทั้งหมดหรือปิดการทดสอบอัตโนมัติเพื่อมุ่งเน้นไปที่การเขียนโค้ด
ผลลัพธ์ก็เหมือนเดิมเสมอ: เราตรวจพบปัญหาช้าลง ปัญหานั้นแก้ไขได้ยากขึ้น และลูกค้าของเรามีผลลัพธ์ที่แย่ลง ซึ่งในทางกลับกันก็สร้างความเครียดไปทั่วทั้ง value stream
เพื่อบรรเทาปัญหานี้ การทดสอบอัตโนมัติจำนวนน้อยที่เชื่อถือได้นั้นมักจะดีกว่าการทดสอบด้วยมือหรือการทดสอบอัตโนมัติที่ไม่น่าเชื่อถือจำนวนมาก ดังนั้น เราจึงมุ่งเน้นไปที่การทำให้เฉพาะการทดสอบที่ตรวจสอบเป้าหมายทางธุรกิจที่เราพยายามทำให้สำเร็จเป็นอัตโนมัติเท่านั้น หากการละทิ้งการทดสอบส่งผลให้เกิดข้อบกพร่องใน production เราควรเพิ่มมันกลับเข้าไปในชุดทดสอบด้วยมือของเรา โดยมีเป้าหมายสูงสุดคือการทำให้เป็นอัตโนมัติในที่สุด
ดังที่ Gary Gruver อดีตรองประธานฝ่าย Quality Engineering, Release Engineering, และ Operations ของ Macys.com กล่าวไว้ว่า “สำหรับเว็บไซต์ e-commerce ของร้านค้าปลีกขนาดใหญ่ เราเปลี่ยนจากการรันการทดสอบด้วยมือ 1,300 รายการที่เรารันทุกสิบวัน มาเป็นการรันการทดสอบอัตโนมัติเพียงสิบรายการในทุก code commit — การรันการทดสอบจำนวนน้อยที่เราไว้วางใจนั้นดีกว่าการรันการทดสอบที่ไม่น่าเชื่อถือ เมื่อเวลาผ่านไป เราเติบโตชุดทดสอบนี้จนมี automated tests หลายแสนรายการ” 23
กล่าวอีกนัยหนึ่ง เราเริ่มต้นด้วยการทดสอบอัตโนมัติที่เชื่อถือได้จำนวนน้อยและเพิ่มเข้ามาเมื่อเวลาผ่านไป สร้างความมั่นใจที่เพิ่มขึ้นเรื่อยๆ ว่าเราจะตรวจพบการเปลี่ยนแปลงใดๆ ต่อระบบที่ทำให้เราหลุดออกจากสถานะที่พร้อม deploy ได้อย่างรวดเร็ว
Continuous Learning (การเรียนรู้อย่างต่อเนื่อง)
บางคนอาจคิดว่าการเขียน automated tests เป็นไปไม่ได้เมื่อต้องเขียน features ใน sprint สองสัปดาห์ แต่ Angie Jones, Java champion และผู้เชี่ยวชาญด้าน test automation ชี้ให้เห็นอย่างถูกต้องว่าหากไม่มี automation ทีมที่ ship เฉพาะ features โดยไม่มี test automation กำลัง ship ความเสี่ยงและสะสม technical debt 24
เธอสรุปสามกลยุทธ์สำหรับทีมในการ ship ทั้ง features และ test automation ใน sprints ของพวกเขา:
• Collaborate (ทำงานร่วมกัน): การทำงานร่วมกันกับฝ่ายธุรกิจ ผู้ทดสอบ และนักพัฒนา ช่วยให้คุณมั่นใจว่าคุณทำให้สิ่งที่ถูกต้องเป็นอัตโนมัติและให้ผู้อื่นมีส่วนร่วมแบบขนาน
• Automate strategically (ทำอัตโนมัติเชิงกลยุทธ์): การใช้แนวทางแบบ hybrid สามารถช่วยให้ทีมคิดเกี่ยวกับ test coverage โดยใช้ APIs และการออกแบบที่ชาญฉลาดเพื่อให้ครอบคลุมสถานการณ์ต่างๆ
• Build incrementally (สร้างแบบเพิ่มทีละน้อย): เริ่มต้นด้วยการสร้างสิ่งที่คุณต้องการ เมื่อคุณสร้าง features เพิ่มเติม การเพิ่มการทดสอบมากขึ้นโดยใช้ framework TDD (test-driven development) จะช่วยให้คุณคิดเหมือนทั้งผู้ทดสอบและนักพัฒนา และจะช่วยให้คุณเขียนโค้ดที่ทดสอบได้ง่ายขึ้น
Integrate Performance Testing into Our Test Suite (รวม Performance Testing เข้ากับชุดทดสอบของเรา)
บ่อยครั้งเกินไปที่เราค้นพบว่าแอปพลิเคชันของเราทำงานได้ไม่ดีในช่วง integration testing หรือหลังจากที่มันถูก deploy ขึ้น production ปัญหาด้านประสิทธิภาพ เช่น เมื่อสิ่งต่างๆ ช้าลงเมื่อเวลาผ่านไป มักตรวจจับได้ยาก โดยไม่มีใครสังเกตจนกว่าจะสายเกินไป (เช่น ตาราง database ที่ไม่มี index) และปัญหามากมายนั้นแก้ไขได้ยาก โดยเฉพาะเมื่อเกิดจากการตัดสินใจทางสถาปัตยกรรมที่เราทำ หรือข้อจำกัดที่ไม่คาดคิดของระบบเครือข่าย ฐานข้อมูล พื้นที่จัดเก็บ หรือระบบอื่นๆ
เป้าหมายของเราคือการเขียนและ run การทดสอบประสิทธิภาพอัตโนมัติที่ตรวจสอบประสิทธิภาพของเราทั่วทั้ง stack ของแอปพลิเคชัน (โค้ด, database, storage, network, virtualization ฯลฯ) ซึ่งเป็นส่วนหนึ่งของ deployment pipeline เพื่อให้เราตรวจพบปัญหาได้ตั้งแต่เนิ่นๆ เมื่อการแก้ไขมีต้นทุนต่ำที่สุดและเร็วที่สุด
การทำความเข้าใจว่าแอปพลิเคชันและสภาพแวดล้อมของเราทำงานอย่างไรภายใต้โหลดที่ใกล้เคียง production จะช่วยให้เราวางแผนความจุ (capacity planning) ได้ดีขึ้น รวมถึงตรวจจับเงื่อนไขต่างๆ เช่น:
• เมื่อเวลาในการ query database ของเราเติบโตแบบไม่เป็นเชิงเส้น (non-linearly) (เช่น เราลืมสร้าง database indexes และหน้าโหลดจากหนึ่งร้อยวินาทีกลายเป็นนาที)
• เมื่อการเปลี่ยนแปลงโค้ดทำให้จำนวนการเรียก database, การใช้พื้นที่จัดเก็บ, หรือ traffic บนเครือข่ายเพิ่มขึ้นเป็นสิบเท่า
เมื่อเรามี acceptance tests ที่สามารถ run แบบขนาน เราสามารถใช้มันเป็นพื้นฐานของ performance tests ของเรา ตัวอย่างเช่น สมมติว่าเราทำเว็บไซต์ e-commerce และระบุว่า “search” และ “checkout” เป็นสอง operations ที่มีมูลค่าสูงซึ่งต้องทำงานได้ดีภายใต้โหลด เพื่อทดสอบสิ่งนี้ เราอาจ run acceptance tests การค้นหาหลายพันชุดแบบขนานพร้อมกับ checkout tests หลายพันชุด
เนื่องจากต้องใช้ทรัพยากร compute และ I/O จำนวนมากในการ run performance tests การสร้างสภาพแวดล้อมการทดสอบประสิทธิภาพอาจซับซ้อนกว่าการสร้างสภาพแวดล้อม production สำหรับแอปพลิเคชันเองเสียอีก ด้วยเหตุนี้ เราจึงควรสร้างสภาพแวดล้อมการทดสอบประสิทธิภาพตั้งแต่เริ่มต้นของโปรเจกต์ และมั่นใจว่าเราทุ่มเททรัพยากรที่จำเป็นในการสร้างมันตั้งแต่เนิ่นๆ และอย่างถูกต้อง เพื่อค้นหาปัญหาด้านประสิทธิภาพแต่เนิ่นๆ เราควรบันทึกผลลัพธ์ด้านประสิทธิภาพและประเมินผลการ run แต่ละครั้งเทียบกับผลลัพธ์ก่อนหน้า ตัวอย่างเช่น เราอาจทำให้ performance tests ล้มเหลวหากประสิทธิภาพเบี่ยงเบนมากกว่า 2% จากการ run ก่อนหน้า
Integrate Non-Functional Requirements Testing into Our Test Suite (รวมการทดสอบ Non-Functional Requirements เข้ากับชุดทดสอบของเรา)
นอกเหนือจากการทดสอบว่าโค้ดของเราทำงานตามที่ออกแบบและมีประสิทธิภาพภายใต้โหลดที่ใกล้เคียง production แล้ว เรายังต้องการตรวจสอบคุณลักษณะอื่นๆ ทุกอย่างของระบบที่เราใส่ใจด้วย ซึ่งมักเรียกว่า non-functional requirements ซึ่งรวมถึง availability, scalability, capacity, security และอื่นๆ
ข้อกำหนดเหล่านี้จำนวนมากสำเร็จได้ผ่านการกำหนดค่าที่ถูกต้องของสภาพแวดล้อมของเรา ดังนั้นเราจึงต้องสร้าง automated tests เพื่อตรวจสอบว่าสภาพแวดล้อมของเราถูก build และกำหนดค่าอย่างถูกต้อง ตัวอย่างเช่น เราต้องการบังคับใช้ความสอดคล้องและความถูกต้องของสิ่งต่อไปนี้ ซึ่ง non-functional requirements จำนวนมากพึ่งพา (เช่น security, performance, availability):
• แอปพลิเคชันสนับสนุน, databases, libraries ฯลฯ
• language interpreters, compilers ฯลฯ
• ระบบปฏิบัติการ (เช่น เปิดใช้งาน audit logging ฯลฯ)
• dependencies ทั้งหมด
เมื่อเราใช้เครื่องมือจัดการการกำหนดค่าแบบ infrastructure as code (เช่น Terraform, Puppet, Chef, Ansible, Salt, Bosh) เราสามารถใช้ framework การทดสอบเดียวกับที่ใช้ทดสอบโค้ดของเราเพื่อทดสอบว่าสภาพแวดล้อมของเราถูกกำหนดค่าและทำงานอย่างถูกต้อง (เช่น การเข้ารหัสการทดสอบสภาพแวดล้อมเป็น cucumber หรือ gherkin tests) เราควร run การตรวจสอบ security hardening ใดๆ ซึ่งเป็นส่วนหนึ่งของการทดสอบอัตโนมัติของเราเพื่อให้แน่ใจว่าทุกอย่างถูกกำหนดค่าอย่างปลอดภัยและถูกต้อง (เช่น server-spec)
ณ เวลาใดๆ การทดสอบอัตโนมัติของเราสามารถตรวจสอบได้ว่าเรามี green build และเราอยู่ในสถานะที่พร้อม deploy ตอนนี้เราต้องสร้าง Andon cord เพื่อว่าเมื่อมีคนทำให้ deployment pipeline เสีย เราจะดำเนินการทุกขั้นตอนที่จำเป็นเพื่อกลับสู่สถานะ green build
Pull Our Andon Cord When the Deployment Pipeline Breaks (ดึง Andon Cord เมื่อ Deployment Pipeline พัง)
เมื่อเรามี green build ใน deployment pipeline เรามีความมั่นใจในระดับสูงว่าโค้ดและสภาพแวดล้อมของเราจะทำงานตามที่ออกแบบเมื่อเรา deploy การเปลี่ยนแปลงของเราขึ้น production
เพื่อรักษา deployment pipeline ของเราให้อยู่ในสถานะ green เราจะสร้าง Andon cord เสมือน ซึ่งคล้ายกับแบบจริงใน Toyota Production System เมื่อใดก็ตามที่มีคนนำการเปลี่ยนแปลงที่ทำให้ build หรือการทดสอบอัตโนมัติของเราล้มเหลว จะไม่อนุญาตให้งานใหม่เข้าสู่ระบบจนกว่าปัญหาจะได้รับการแก้ไข และถ้ามีคนต้องการความช่วยเหลือในการแก้ปัญหา พวกเขาสามารถขอความช่วยเหลือที่ต้องการได้
เมื่อ deployment pipeline ของเราเสีย อย่างน้อยที่สุดเราต้องแจ้งให้ทั้งทีมทราบถึงความล้มเหลว เพื่อให้ทุกคนสามารถแก้ไขปัญหาหรือ roll-back commit เราอาจกำหนดค่าระบบ version control เพื่อป้องกันการ commit โค้ดเพิ่มเติมจนกว่าขั้นตอนแรก (เช่น builds และ unit tests) ของ deployment pipeline จะกลับสู่สถานะ green หากปัญหาเกิดจากการทดสอบอัตโนมัติที่สร้าง false positive error การทดสอบที่ก่อปัญหาควรถูกเขียนใหม่หรือลบทิ้ง *** สมาชิกทุกคนในทีมควรมีอำนาจในการ roll back commit เพื่อกลับสู่สถานะ green
Randy Shoup อดีต engineering director ของ Google App Engine เขียนเกี่ยวกับความสำคัญของการนำ deployment กลับสู่สถานะ green
เราให้ความสำคัญกับเป้าหมายของทีมมากกว่าเป้าหมายของแต่ละบุคคล — เมื่อใดก็ตามที่เราช่วยใครสักคนทำงานของพวกเขาต่อไป เรากำลังช่วยทั้งทีม สิ่งนี้ใช้ได้ไม่ว่าเราจะช่วยใครแก้ไข build หรือ automated test หรือแม้แต่ทำ code review ให้พวกเขา และแน่นอน เรารู้ว่าพวกเขาจะทำแบบเดียวกันให้เราเมื่อเราต้องการความช่วยเหลือ ระบบนี้ทำงานได้โดยไม่ต้องมีระเบียบการหรือนโยบายมากมาย — ทุกคนรู้ว่างานของเราไม่ใช่แค่ “เขียนโค้ด” แต่คือ “run service” นี่คือเหตุผลที่เราให้ความสำคัญกับปัญหาด้านคุณภาพทั้งหมด โดยเฉพาะที่เกี่ยวข้องกับ reliability และ scaling ในระดับสูงสุด ปฏิบัติต่อมันเป็นปัญหา Priority 0 “show-stopper” ในมุมมองของระบบ แนวปฏิบัติเหล่านี้ป้องกันไม่ให้เราถอยหลัง 25
เมื่อขั้นตอนหลังของ deployment pipeline ล้มเหลว เช่น acceptance tests หรือ performance tests แทนที่จะหยุดงานใหม่ทั้งหมด เราจะมีนักพัฒนาและผู้ทดสอบที่อยู่เวรรับผิดชอบในการแก้ไขปัญหาเหล่านี้ทันที พวกเขาควรสร้างการทดสอบใหม่ที่ run ในขั้นตอนก่อนหน้าของ deployment pipeline เพื่อจับ regressions ในอนาคต ตัวอย่างเช่น ถ้าเราค้นพบข้อบกพร่องใน acceptance tests เราควรเขียน unit test เพื่อจับปัญหา ในทำนองเดียวกัน ถ้าเราค้นพบข้อบกพร่องในการ exploratory testing เราควรเขียน unit หรือ acceptance test
เพื่อเพิ่มการมองเห็นของความล้มเหลวในการทดสอบอัตโนมัติ เราควรสร้างตัวบ่งชี้ที่มองเห็นได้ชัดเจนเพื่อให้ทั้งทีมเห็นเมื่อ build หรือการทดสอบอัตโนมัติของเราล้มเหลว หลายทีมได้สร้าง build lights ที่มองเห็นได้ชัดเจนติดตั้งบนผนัง แสดงสถานะ build ปัจจุบัน หรือวิธีสนุกๆ อื่นๆ ในการบอกทีมว่า build เสีย รวมถึง lava lamps, การเล่นเสียงตัวอย่างหรือเพลง, klaxons, ไฟจราจร และอื่นๆ
ในหลายแง่มุม ขั้นตอนนี้ท้าทายกว่าการสร้าง build และ test servers ของเรา — สิ่งเหล่านั้นเป็นกิจกรรมทางเทคนิคล้วนๆ ในขณะที่ขั้นตอนนี้ต้องการการเปลี่ยนแปลงพฤติกรรมและแรงจูงใจของมนุษย์ อย่างไรก็ตาม continuous integration และ continuous delivery ต้องการการเปลี่ยนแปลงเหล่านี้ ดังที่เราจะสำรวจในหัวข้อถัดไป
Why We Need to Pull the Andon Cord (ทำไมเราต้องดึง Andon Cord)
ผลที่ตามมาของการไม่ดึง Andon cord และไม่แก้ไขปัญหา deployment pipeline ทันทีคือปัญหาที่คุ้นเคยกันดีซึ่งการนำแอปพลิเคชันและสภาพแวดล้อมกลับสู่สถานะที่พร้อม deploy นั้นยากขึ้นเรื่อยๆ ลองพิจารณาสถานการณ์ต่อไปนี้:
• มีคน check in โค้ดที่ทำให้ build หรือการทดสอบอัตโนมัติของเราเสีย แต่ไม่มีใครแก้ไข
• มีคนอื่น check in การเปลี่ยนแปลงอีกครั้งบน build ที่เสีย ซึ่งก็ไม่ผ่านการทดสอบอัตโนมัติของเราเช่นกัน — แต่ไม่มีใครเห็นผลการทดสอบที่ล้มเหลวซึ่งจะช่วยให้เราเห็นข้อบกพร่องใหม่ นับประสาอะไรกับการแก้ไขมัน
• การทดสอบที่มีอยู่ของเราไม่ run อย่างน่าเชื่อถือ ดังนั้นเราจึงมีโอกาสน้อยมากที่จะสร้างการทดสอบใหม่ (จะไปทำไม ในเมื่อการทดสอบปัจจุบันยัง run ไม่ได้เลย)
เมื่อสิ่งนี้เกิดขึ้น การ deploy ของเราไปยังสภาพแวดล้อมใดๆ ก็ไม่น่าเชื่อถือพอๆ กับตอนที่เราไม่มีการทดสอบอัตโนมัติหรือใช้วิธี waterfall ซึ่งปัญหาส่วนใหญ่ของเราถูกค้นพบใน production ผลลัพธ์ที่หลีกเลี่ยงไม่ได้ของวงจรอุบาทว์นี้คือเราจบลงที่จุดเริ่มต้น ด้วย “stabilization phase” ที่คาดเดาไม่ได้ซึ่งกินเวลาหลายสัปดาห์หรือหลายเดือน ซึ่งทั้งทีมของเราจมดิ่งสู่วิกฤต พยายามทำให้การทดสอบทั้งหมดผ่าน ตัดมุมเพราะแรงกดดันด้านกำหนดส่งงาน และเพิ่ม technical debt †††
Continuous Learning (การเรียนรู้อย่างต่อเนื่อง)
ข้อมูลสนับสนุนความสำคัญของการทดสอบอัตโนมัติ งานวิจัยจาก DORA's 2019 State of DevOps Report แสดงให้เห็นว่าทีมที่ใช้การทดสอบอัตโนมัติมี continuous integration ที่เหนือกว่า การลงทุนอย่างชาญฉลาดในการทดสอบอัตโนมัติช่วยปรับปรุง continuous integration ของเรา รายงานระบุว่า “การทดสอบอัตโนมัติสามารถเป็น force-multiplier ที่สำคัญเมื่อใช้ในหลายทีมภายในองค์กร” และสามารถมีส่วนช่วยต่อประสิทธิภาพระดับสูง (elite performance) 27
องค์ประกอบสำคัญของการทดสอบอัตโนมัติรวมถึงการทดสอบที่:
• Reliable (น่าเชื่อถือ): ความล้มเหลวบ่งชี้ถึงข้อบกพร่องจริง และเมื่อการทดสอบผ่าน นักพัฒนามั่นใจว่าโค้ดจะทำงานได้สำเร็จใน production
• Consistent (สม่ำเสมอ): แต่ละ code commit ควร trigger ชุดการทดสอบ เพื่อให้ feedback แก่นักพัฒนา
• Fast and reproducible (รวดเร็วและทำซ้ำได้): การทดสอบควรเสร็จภายในสิบนาทีหรือน้อยกว่า เพื่อให้นักพัฒนาสามารถจำลองและแก้ไขความล้มเหลวในสภาพแวดล้อมส่วนตัวได้อย่างรวดเร็ว
• Inclusive (ครอบคลุม): การทดสอบไม่ควรจำกัดเฉพาะผู้ทดสอบเท่านั้น และผลลัพธ์ที่ดีที่สุดจะเกิดขึ้นเมื่อนักพัฒนาปฏิบัติตาม test-driven development
ความสำคัญของการ exploratory และ manual testing ก็ได้รับการสนับสนุนจากงานวิจัยเช่นกัน DORA's 2018 State of DevOps Report พบว่าการทดสอบตลอดวงจรการส่งมอบซอฟต์แวร์ (software delivery life cycle) มีส่วนช่วยให้เกิดผลลัพธ์ continuous delivery และประสิทธิภาพระดับสูง นอกเหนือจากการทดสอบอัตโนมัติแล้ว สิ่งนี้รวมถึง: 28
• การทบทวนและปรับปรุงชุดทดสอบอย่างต่อเนื่องเพื่อค้นหาข้อบกพร่องได้ดีขึ้นและควบคุมความซับซ้อน/ต้นทุน
• การให้ผู้ทดสอบทำงานเคียงข้างนักพัฒนาตลอดกระบวนการพัฒนาและส่งมอบซอฟต์แวร์
• การดำเนินกิจกรรมการทดสอบด้วยมือ เช่น exploratory testing, usability testing, และ acceptance testing ตลอดกระบวนการส่งมอบ
Conclusion (บทสรุป)
ในบทนี้ เราได้สร้างชุดการทดสอบอัตโนมัติที่ครอบคลุมเพื่อยืนยันว่าเรามี green build ที่ยังอยู่ในสถานะผ่านและพร้อม deploy เราได้จัดระเบียบชุดทดสอบและกิจกรรมการทดสอบของเราเป็น deployment pipeline นอกจากนี้เรายังสร้างบรรทัดฐานทางวัฒนธรรมในการทำทุกวิถีทางเพื่อกลับสู่สถานะ green build ถ้ามีคนนำการเปลี่ยนแปลงที่ทำให้การทดสอบอัตโนมัติของเราเสีย
การทำเช่นนี้เป็นการปูทางสำหรับการนำ continuous integration ไปใช้ ซึ่งช่วยให้ทีมขนาดเล็กจำนวนมากสามารถพัฒนา ทดสอบ และ deploy โค้ดขึ้น production ได้อย่างอิสระและปลอดภัย ส่งมอบคุณค่าให้แก่ลูกค้า
Bland อธิบายว่าที่ Google ผลที่ตามมาอย่างหนึ่งของการมีนักพัฒนาที่มีความสามารถมากมายคือการสร้าง “imposter syndrome” ซึ่งเป็นคำที่นักจิตวิทยาบัญญัติขึ้นเพื่ออธิบายคนที่ไม่สามารถซึมซับความสำเร็จของตนเองได้ Wikipedia ระบุว่า “แม้จะมีหลักฐานภายนอกที่แสดงถึงความสามารถของพวกเขา คนที่แสดงอาการของ syndrome นี้ยังคงเชื่อว่าพวกเขาเป็นคนหลอกลวงและไม่สมควรได้รับความสำเร็จที่ได้รับ หลักฐานของความสำเร็จถูกมองว่าเป็นโชค จังหวะเวลา หรือผลของการหลอกลวงผู้อื่นให้คิดว่าพวกเขาฉลาดและมีความสามารถมากกว่าที่ตนเองเชื่อ” 4 | |
พวกเขาสร้างโปรแกรมฝึกอบรม ผลักดัน newsletter ชื่อดัง Testing on the Toilet (ซึ่งติดไว้ในห้องน้ำ) สร้าง Test Certified roadmap และโปรแกรมการรับรอง และนำ “fix-it” days (การปรับปรุงครั้งใหญ่) หลายครั้ง ซึ่งช่วยให้ทีมปรับปรุงกระบวนการทดสอบอัตโนมัติของตน เพื่อให้สามารถทำซ้ำผลลัพธ์ที่น่าทึ่งที่ทีม GWS ทำได้ | |
ใน Development continuous integration มักหมายถึงการรวมสาขาโค้ดหลายสาขาเข้าสู่ trunk และทำให้แน่ใจว่ามันผ่าน unit tests อย่างไรก็ตาม ในบริบทของ continuous delivery และ DevOps continuous integration ยังกำหนดให้ต้อง run บนสภาพแวดล้อมที่เหมือน production และผ่าน acceptance และ integration tests Jez Humble และ David Farley แยกความแตกต่างของสิ่งนี้โดยเรียกอย่างหลังว่า CI+ ในหนังสือเล่มนี้ continuous integration จะหมายถึงแนวปฏิบัติ CI+ เสมอ 13 | |
ถ้าเราสร้าง containers ใน deployment pipeline และมีสถาปัตยกรรมแบบ microservices เราสามารถให้ developer แต่ละคนสร้าง immutable artifacts ที่ developers สามารถประกอบและ run components ทั้งหมดของ service ในสภาพแวดล้อมที่เหมือน production บน workstation ของตนเอง สิ่งนี้ช่วยให้ developers สร้างและ run การทดสอบบน workstation ของตนได้มากขึ้นแทนที่จะพึ่งพา test servers ทำให้ได้รับ feedback ที่เร็วขึ้นในงานของพวกเขา | |
เราอาจกำหนดให้เครื่องมือเหล่านี้ run ก่อนที่การเปลี่ยนแปลงจะถูกยอมรับเข้าสู่ version control (เช่น ใช้ pre-commit hooks) เราอาจ run เครื่องมือเหล่านี้ภายใน IDE (integrated development environment; ที่ developer แก้ไข compile และ run โค้ด) ของ developers ซึ่งสร้าง feedback loop ที่เร็วขึ้นอีก | |
เรายังสามารถใช้ containers เช่น Docker เป็นกลไกในการ packaging ได้ Containers ทำให้มีความสามารถในการเขียนครั้งเดียว run ได้ทุกที่ (write once, run anywhere) containers เหล่านี้ถูกสร้างขึ้นเป็นส่วนหนึ่งของกระบวนการ build ของเรา และสามารถ deploy และ run ในสภาพแวดล้อมใดๆ ได้อย่างรวดเร็ว เนื่องจาก container เดียวกันถูก run ในทุกสภาพแวดล้อม เราจึงช่วยบังคับใช้ความสอดคล้องของ build artifacts ทั้งหมดของเรา | |
ปัญหานี้เองที่นำไปสู่การพัฒนาแนวปฏิบัติ continuous integration | |
มีเทคนิคทางสถาปัตยกรรมและการทดสอบมากมายที่ใช้จัดการกับปัญหาของการทดสอบที่ต้องอาศัยข้อมูลจาก external integration points รวมถึง “stubs,” “mocks,” “service virtualization” และอื่นๆ สิ่งนี้ยิ่งมีความสำคัญมากขึ้นสำหรับ acceptance และ integration testing ซึ่งพึ่งพาสถานะภายนอกมากกว่า | |
เราควรทำสิ่งนี้เมื่อทีมของเราให้คุณค่ากับการทดสอบอัตโนมัติอยู่แล้วเท่านั้น — metric ประเภทนี้ถูกโกงได้ง่ายโดยนักพัฒนาและผู้จัดการ | |
Nachi Nagappan, E. Michael Maximilien, และ Laurie Williams (จาก Microsoft Research, IBM Almaden Labs, และ North Carolina State University ตามลำดับ) ดำเนินการศึกษาที่แสดงให้เห็นว่าทีมที่ใช้ TDD ผลิตโค้ดที่ดีขึ้น 60%–90% ในแง่ของความหนาแน่นของข้อบกพร่อง (defect density) เมื่อเทียบกับทีมที่ไม่ใช้ TDD โดยใช้เวลาเพิ่มขึ้นเพียง 15%–35% 20 | |
ถ้ากระบวนการสำหรับ roll back โค้ดไม่เป็นที่รู้จักดี มาตรการแก้ไขที่เป็นไปได้คือการกำหนดเวลา pair programmed rollback เพื่อให้สามารถบันทึกกระบวนการได้ดีขึ้น | |
บางครั้งสิ่งนี้เรียกว่า water-Scrum-fall anti-pattern ซึ่งหมายถึงเมื่อองค์กรอ้างว่าใช้แนวปฏิบัติแบบ Agile แต่ในความเป็นจริงแล้ว การทดสอบและการแก้ไขข้อบกพร่องทั้งหมดจะดำเนินการเมื่อสิ้นสุดโปรเจกต์ 26 |