สรุปคลาส Microservices day 2/4

สรุปคลาส Microservices day 2/4

ต่อไปนี้คือบทความที่เกี่ยวข้องในชุดนี้ครับ

หลังจากวันที่ 1 พี่ปุ๋ยได้ทำการให้ลองออกแบบระบบแล้ว พี่ปุ๋ยได้ปรับโจทย์ให้เป็นเพียง Feature hello world ซึ่งรูปแบบคือเขียน API ให้สามารถเชื่อมต่อกันกับ Datastore เพื่อนำเอาข้อมูลออกมาแสดงผ่านหน้า Front-end ซึ่งเทคโนโลยีที่ใช้เหมือนกันคือฝั่งของ API คือ Spring Boot 2.0 ส่วนฝั่ง Front-end แล้วแต่ทีมจะเลือกใช้งาน โดยแต่ละทีมเลือก Angular และ React

รูปด้านซ้ายนี้คือการอธิบายโจทย์ในมุมของ Architecture ซึ่งแบ่งออกเป็น 3-tiers นั้นคือ ชั้นแรกสุดคือชั้นของหน้าจอที่ผู้ใช้งานเห็น (Front-end) มีการไปเรียก REST API เพื่อให้ได้ข้อมูลนำมาแสดงผล ซึ่งตัว API นี้ได้ทำการเชื่อมต่อไปยังชั้นของ Database เพื่อเอาข้อมูลมาใช้งาน

จะเห็นว่า Feature นี้ไม่ได้มีความยากในการพัฒนามากนัก แต่สิ่งที่ทำให้เราได้ลองทดสอบนั่นก็คือการนำเอา Build pipeline ที่ได้ออกแบบไว้ มาแปลงให้เป็นคำสั่ง automate ต่าง ๆ เพื่อได้พบปัญหาจริง รวมทั้งทำให้ทีมสามารถเตรียมพร้อมได้ว่าก่อนลงมือทำงานจริง ต้องเตรียมอะไรก่อนบ้าง

ก่อนเริ่มไปยังส่วนต่อไป ผู้อ่านสามารถทดลองเอาไปเล่นและทำเองได้จาก Github repo นี้ ครับ >> https://github.com/nitipatl/springboot-hello-project

Build pipeline

ก่อนที่จะเริ่มลงมือทำจริง สิ่งที่ทีมควรทำคือ

  1. กำหนดเทคโนโลยีและเครื่องมือที่จะนำมาใช้
  2. ออกแบบ Build pipeline ในการทำ Project นี้ร่วมกัน
  3. เรียนรู้และวางโครงสร้างของ Project ร่วมกัน ซึ่งจากตัวอย่างที่พี่ปุ๋ยพาทำได้นำเอารูป Service structure นี้มาอ้างอิงในการออกแบบ

Service structure

Resource คือ ช่องทางที่ภายนอก Service มาต่อ ถ้าดูจากใน Code จะเห็นว่าคือส่วนของ Controller ที่ระบุว่า Resoure ไหนเชื่อมกับ Service Layer ไหน

@GetMapping คือการระบุ Resource

method sayHi คือการกำหนด Service Layer

ส่วน Hello คือ Domain ของเราที่แยกออกมาจากส่วนของ Controller เพื่อให้ง่ายต่อการแก้ไขและดูแล ซึ่งตัวนี้ทำหน้าที่ในการจัดการ Format ของข้อมูลที่จะแสดงออกมาเท่านั้น

โดยไฟล์ HelloController.java นี้จะอยู่ในโฟลเดอร์ Controller

เมื่อเราเขียน Code ขึ้นมาแล้ว สิ่งที่ต้องคำนึงถึงคือเราจะทำการทดสอบมันอย่างไร?

Testing pyramid

สำหรับ Testing pyramid นี้สามารถอ่านเพิ่มเติมได้ที่นี้ครับ ซึ่งเราจะใช้อ้างอิงชนิดของการทดสอบต่อ ๆ ไป ภายในบทความนี้

ทันทีที่เราทำเสร็จ เราก็ทำการรันขึ้นมา แล้ว เข้าไปยัง URL นั้น อาจจะใช้งานผ่าน Postman เพื่อทดสอบดูว่าใช้งานได้ไหม ซึ่งการทดสอบนี้เมื่อดูเทียบจาก Testing pyramid เราเรียกกันว่า Integration test ซึ่งการทดสอบนี้คือการที่เราต้องการทดสอบว่าในแต่ละ Method ที่เขียนขึ้นมานั้นเมื่อมาอยู่รวมกันมันสามารถทำงานร่วมกันได้ไหม จากตัวอย่างนี้คือการที่เราลองเข้าไปที่ Resoure นั้นเพื่อดูผลของ Response ที่ออกมาว่าถูกต้องไหม

ต่อไปนี้เราจะลองเปลี่ยนกิจกรรมพวกนี้ให้มาเป็นแบบ automate ดูครับ โดยใน workshop นี้เรามีการทดสอบโดยใช้ MockMvc เพื่อทำหน้าที่ในการจำลองการเปิด Service ขึ้นมาดังนี้ โดยโฟลเดอร์ที่นำมาไว้ test/Controller และกำหนดชื่อให้มีความสอดคล้องกันกับ Controller ในที่นี้คือ HelloControllerTest.java ซึ่งตรงนี้ก็ต้องอยู่ที่ทีมตกลงกันว่าจะจัดวาง Test script ไว้ตรงไหน เพราะ ตัวโปรแกรมที่รันทดสอบสามารถกำหนดได้ว่าจะให้รันไฟล์ไหน หรือ โฟลเดอร์ไหนได้อยู่แล้ว

เมื่อเรารันทดสอบ ผลที่ตามมาก็มีการแสดงว่าถูกต้องไหม ถ้าไม่ถูกก็แจ้งเตือนว่าผิดที่ไฟล์ไหน method ไหน แต่ถ้าสามารถทำได้อย่างถูกต้องหมดก็ Build ออกมาเป็น jar file เพื่อนำเอาไป deploy ต่อไป

สรุป ตอนนี้สิ่งที่เราทำไปแล้วคือมี Controller ขึ้นมาใหม่ชื่อ HelloController ซึ่งมีการระบุ Resource ไว้ว่าให้สามารถ GET ไปที่ /hello/{name} (name คือค่าที่ส่งไปกับ url) ซึ่งมีการระบุด้วยว่าใช้งานได้คู่กับ Service Layer ใด ในที่นี้คือ method sayHi ซึ่งมีการเรียกใช้งาน class Hello เพื่อจัดการเรื่อง Formate ของข้อมูลจาก Domain และในทุกไฟล์ที่เราเพิ่มเข้าไปต้องมีการทดสอบประกบไปด้วยดังนี้

ที่นี้สิ่งที่เราต้องทำคือการเชื่อมต่อกับ Datastore ซึ่งผมได้ใช้ Mongodb ผ่านการใข้งานร่วมกันกับ Spring Data โดยต้องเพิ่ม dependency เพิ่มขึ้นมานั้นก็คือ spring-data-mongodb ซึ่งสามารถดูตัวอย่างการเพิ่มได้ที่นี่ จากนั้นในไฟล์ resources/application.properties เพิ่มข้อมูลในส่วนของ host name และ database name ที่สามารถเชื่อมต่อกับ mongodb server ได้

จากนั้นเราจะทำการสร้าง Resource ขึ้นมาใหม่ นั้นคือ GET /hello/data/{name} โดยมีการไปค้นหา name จาก Datastore มาแสดง โดยสร้างไว้ใน Controller ชื่อไฟล์ HelloControllerWithRepository.java ซึ่งภายในจะมี Resoure และ Service Layer คล้ายคลึงกลับไฟล์แรกที่เราทำขึ้นมา

โดยสิ่งที่แตกต่างก็คือมีการเรียก PersonRepository ขึ้นมา ซึ่งไม่ได้ผ่านไปยังชั้นของ Datastore ตรง ๆ ซึ่งไฟล์นี้จะไปอยู่ในโฟลเดอร์ repository เราเลือกทำแบบนี้เพื่อให้การปรับเปลี่ยนเทคโนโลยีของ Datastore ไม่ต้องเกิดผลกระทบกับ Controller นี้

โดย PersonRepository ก็จะมีการเรียกใช้งาน Person ซึ่งเป็นตัวกำหนด Schema ของ Model นี้

จะเห็นความสัมพันธ์ตาม Service structure ที่เราวางไว้ตอนแรกนั้นก็คือ Resource -> Service Layer -> Repository / Domain

ซึ่งตัว PersonRepository ถ้าดูจากโค๊ดภายในก็จะมีการ extends ความสามารถมาจาก MongoRepository ซึ่งมองได้เทียบเคียงว่าคือ (Data mapper / ORM) ที่ทำหน้าที่แปลงเป็น query syntax เพื่อเรียกใช้งาน Datastore ต่อไป

ถ้าถามว่าทำไมถึงต้องมีการวางโครงสร้างของ Service ที่ชัดเจนเช่นนี้เพราะอย่างแรกเลย เราสามารถดูแลต่อได้ง่าย ไม่มีการผูกกันที่มั่วซั่ว ที่สำคัญคือเราสามารถกำหนดการทดสอบได้ทุกส่วน ว่าในแต่ละส่วนต้องทดสอบอย่างไร เพราะการวางโครงสร้างที่ไม่ดีนั้นจะส่งผลร้ายทำให้การทดสอบยาก แต่ละส่วนผูกกันจนเราไม่สามารถเขียน automate script มาทดสอบ ต้องมีการทำ Test double มากมายเพราะผูกกันเกินไป

ตอนนี้มีคำใหม่เกิดขึ้นมาในบทความนี้นั้นคือ Test double มันคือเทคนิคที่จะช่วยเราทดสอบแต่ละส่วนได้ง่ายขึ้น เพราะ เราจะสามารถทำการจำลอง Dependency ทุกตัวที่เกี่ยวข้องได้ (ถ้ามีการวางโครงสร้างที่ดี)

จากรูปนี้เราสนใจที่การทำงานในส่วนของ Controller ที่มี Service Layer อยู่ (Method sayHi) แต่จะพบว่าจากโค๊ดนี้ว่ามันมีการไปดึง Repository (PersonRepository) มาใช้งานเพื่อเชื่อมต่อข้อมูลจาก Datastore ด้วย ทีนี้ถ้าเราต้องการทดสอบแค่ Controller เราจะทำอย่างไร?

ลองย้อนกลับไปดู Testing pyramid จะพบว่าการทดสอบที่เราจะทำต่อไปนี้เราเรียกว่า Unit test การทดสอบลักษณะนี้มีความหมายที่สามารถแยกออกจากแบบอื่นได้ชัดเจนที่สุดนั้นก็คือ การทดสอบที่เราสามารถตัด Dependency ของจริงจากตัวที่ต้องการทดสอบได้ทั้งหมด เช่น ถ้ามันมีการเรียก Datastore สิ่งที่ทำได้คือเราสามารถทดสอบได้แม้จะไม่มีการเปิด Database server

จากตัวอย่างเราต้องการทดสอบ Method sayHi ที่อยู่ภายใน Controller ซึ่งเราต้องการตัด Dependency ของมัน ซึ่งนั้นก็คือ Repository ซึ่งจาก Framework ที่เราใช้ สามารถใช้เครื่องมือที่ชื่อว่า mockito เข้ามาช่วย

โดยไฟล์ HelloControllerWithRepositoryTest.java จะถูกวางไว้ที่ test/controller ตามตัวอย่างข้างต้น

จะเห็นว่าเรามีการสร้าง PersonRepository ขึ้นมา และ มีการสร้าง HelloControllerWithRepository ขึ้นมาด้วย จากนั้นเราลองมาดูใน method init ครับ จะเห็นว่ามีการระบุ initMocks ซึ่งอันนี้เป็นคำสั่งของ mockito ครับ ส่วนสำคัญคือบรรทัดถัดมานั้นคือการประกาศสร้างคลาส HelloControllerWithRepository มีการโยนค่าของ PersonRepository ที่เราต้องการจะใช้แก้ไขข้อมูลภายหลังเข้าไปด้วย เทคนิคนี้เราเรียกว่า Dependency Injection เพื่อให้เกิดการ Dependency Invasion ขึ้นผลก็คือเราสามารถจัดการ Dependency ได้หมดว่าอยากให้เป็นค่าไหน

แน่นอนครับว่าถ้าย้อนกลับไปดูโค๊ดของ HelloControllerWithRepository จะพบว่ามีการกำหนดให้สามารถโยนค่าเข้ามาได้ใน construct method ได้ ถ้าเราไม่เขียนในลักษณะนี้ก็ไม่สามารถทำให้ทดสอบโดยใช้ท่านี้ได้ ลองศึกษาต่อได้จากเทคนิคการออกแบบโดยหลักการของ SOLID ที่จะข่วยชีวิตเรามากยิ่งขึ้น

ต่อไปเรามาดูกันที่ method ของ shouldBeReturnHelloNitipat จะพบว่าในบรรทัดที่ 33–34 มีการเตรียมข้อมูลที่เราต้องการให้เป็นของ personRepository ส่วนนี้นิยมเรียกกันว่า Arrange และ บรรทัดที่ 36 คือการสั่งไปที่ HelloControllerWithRepository method sayHi และโยนค่าเข้าไปเพื่อเรียกใข้งานสิ่งที่เราต้องการทดสอบ ส่วนนี้จะเรียกว่า Action และ สุดท้าย บรรทัดที่ 38 คือการเปรียบเทียบค่าว่าตรงกับที่เราคาดหวังไหม ซึ่งเรียกว่า Assert

สรุป การเขียนการทดสอบในแต่ละ 1 Method นั่นคือ 1 Test case

ซึ่งมีโครงสร้างที่จำง่าย คือ 3A นั่นคือ Arrange (ตระเตรียมสิ่งที่ต้องใช้), Action (เรียกการทดสอบ) และ Assert (ตรวจสอบผลลัพธ์)

ที่นี้ถ้าเราทำงานกันหลายคน คำถามคือเราทดสอบมันมากพอไหม?

เราจะรู้ได้ยังไงว่าเพียงพอแล้ว?

แน่นอนทุกอย่างมีหลักฐาน และ วัดผลได้ครับ ซึ่งเมื่อเรามีการเขียน automate test สิ่งที่ตามมาคือเราตรวจสอบได้ว่ามันครบถ้วนแค่ไหน ซึ่งนิยมเรียกกันว่า Code coverage หรือ Test coverage ซึ่งการที่จะนำเอาค่านั้นออกมาได้ จะต้องมี Tool ที่สามารถอ่านไปยัง Executable file และ วิเคราะห์ร่วมกับไฟล์ automate test ว่ามีการทดสอบผ่านไปที่ส่วนใดของโค๊ดบ้าง ซึ่งถ้าเป็นภาษา Java ที่แนะนำคือ cobertura

cobertura report

มาถึงตอนนี้เรามี Build pipeline ของ API ดังนี้

  • Build + Test => mvn clean package
  • Run => java -jar program.jar

ซึ่งเราสามารถทดลองสั่งรันโดย Manual ได้แล้ว สิ่งต่อไปคือเราจะลองทำการแปลงให้คำสั่งนี้อยู่ในโลกของ Container โดยใช้ Docker และ ทำให้ automate ด้วยการใช้ Jenkins มาทำให้เกิดระบบ Continuous Integration ขึ้น