เมื่อใดที่จะใช้ Mockito.verify ()


201

ฉันเขียนกรณีทดสอบ jUnit สำหรับ 3 จุดประสงค์:

  1. เพื่อให้แน่ใจว่ารหัสของฉันเป็นไปตามฟังก์ชันการทำงานที่จำเป็นทั้งหมดภายใต้การรวมกัน / ค่า (หรือส่วนใหญ่) ของอินพุต
  2. เพื่อให้แน่ใจว่าฉันสามารถเปลี่ยนแปลงการใช้งานและพึ่งพากรณีทดสอบ JUnit เพื่อบอกฉันว่าการทำงานทั้งหมดของฉันยังคงเป็นที่น่าพอใจ
  3. เป็นเอกสารประกอบการใช้งานทุกกรณีรหัสของฉันจัดการและทำหน้าที่เป็นข้อมูลจำเพาะสำหรับการปรับโครงสร้างใหม่ - หากรหัสจำเป็นต้องถูกเขียนใหม่ (ปรับเปลี่ยนรหัสและหากการทดสอบ jUnit ของฉันล้มเหลว - คุณอาจพลาดกรณีการใช้งานบางอย่าง)

ฉันไม่เข้าใจว่าทำไมหรือMockito.verify()ควรใช้ เมื่อฉันเห็นverify()การถูกเรียกมันกำลังบอกฉันว่า jUnit ของฉันเริ่มตระหนักถึงการใช้งาน (ดังนั้นการเปลี่ยนการใช้งานของฉันจะทำลาย jUnits ของฉันแม้ว่าการทำงานของฉันจะไม่ได้รับผลกระทบ)

ฉันกำลังหา:

  1. สิ่งที่ควรเป็นแนวทางสำหรับการใช้งานที่เหมาะสมของMockito.verify()?

  2. เป็นพื้นฐานที่ถูกต้องหรือไม่ที่ jUnits จะต้องตระหนักถึงหรือใช้ควบคู่กับการทดสอบในชั้นเรียนอย่างรัดกุมหรือไม่?


1
ฉันพยายามอยู่ห่างจากการใช้ Verify () ให้มากที่สุดเท่าที่จะทำได้ด้วยเหตุผลเดียวกับที่คุณสัมผัส (ฉันไม่ต้องการให้หน่วยทดสอบของฉันตระหนักถึงการใช้งาน) แต่มีกรณีเมื่อฉันไม่มีทางเลือก - โมฆะ stubbed- วิธีการ โดยทั่วไปการพูดเนื่องจากไม่ส่งคืนสิ่งใดก็ตามที่พวกเขาไม่ได้มีส่วนร่วมในการส่งออก 'จริง' ของคุณ แต่ถึงกระนั้นคุณต้องรู้ว่ามันถูกเรียกว่า แต่ฉันเห็นด้วยกับคุณมันไม่มีเหตุผลที่จะใช้การตรวจสอบเพื่อตรวจสอบการไหลของการดำเนินการ
Legna

คำตอบ:


78

หากสัญญาของคลาส A รวมถึงความจริงที่ว่ามันเรียกเมธอด B ของวัตถุชนิด C คุณควรทดสอบสิ่งนี้โดยการจำลองประเภท C และตรวจสอบว่ามีการเรียกเมธอด B

นี่หมายความว่าสัญญาของคลาส A มีรายละเอียดเพียงพอที่จะพูดถึงประเภท C (ซึ่งอาจเป็นส่วนต่อประสานหรือคลาส) ใช่แล้วเรากำลังพูดถึงระดับของสเปคที่นอกเหนือไปจาก "ความต้องการของระบบ" และอธิบายวิธีการใช้งาน

นี่เป็นเรื่องปกติสำหรับการทดสอบหน่วย เมื่อคุณทำการทดสอบหน่วยคุณต้องการให้แน่ใจว่าแต่ละหน่วยกำลังทำ "สิ่งที่ถูกต้อง" และโดยปกติจะรวมการโต้ตอบกับหน่วยอื่น ๆ "หน่วย" ที่นี่อาจหมายถึงคลาสหรือชุดย่อยขนาดใหญ่ของแอปพลิเคชันของคุณ

ปรับปรุง:

ฉันรู้สึกว่าสิ่งนี้ไม่ได้นำไปใช้กับการตรวจสอบ แต่รวมถึงการขัดด้วยเช่นกัน ทันทีที่คุณเริ่มต้นวิธีการเรียนของผู้ทำงานร่วมกันการทดสอบหน่วยของคุณจะกลายเป็นขึ้นอยู่กับการใช้งาน เป็นลักษณะของการทดสอบหน่วยที่จะเป็นเช่นนั้น เนื่องจาก Mockito ให้ความสำคัญกับการขัดถูมากเช่นเดียวกับการตรวจสอบข้อเท็จจริงความจริงที่ว่าคุณกำลังใช้ Mockito นั้นหมายความว่าคุณจะต้องวิ่งผ่านการพึ่งพาแบบนี้

จากประสบการณ์ของฉันถ้าฉันเปลี่ยนการใช้งานคลาสฉันมักจะต้องเปลี่ยนการใช้งานการทดสอบหน่วยเพื่อให้ตรงกับ โดยปกติแม้ว่าฉันจะไม่ได้มีการเปลี่ยนแปลงสินค้าคงคลังของสิ่งที่ทดสอบหน่วยมีอยู่สำหรับการเรียนนั้น เว้นแต่แน่นอนเหตุผลสำหรับการเปลี่ยนแปลงคือการมีอยู่ของเงื่อนไขที่ฉันไม่สามารถทดสอบก่อนหน้านี้

ดังนั้นนี่คือการทดสอบหน่วยเกี่ยวกับ การทดสอบที่ไม่ได้รับผลกระทบจากการพึ่งพาประเภทนี้กับวิธีการใช้คลาสผู้ทำงานร่วมกันนั้นเป็นการทดสอบระบบย่อยหรือการทดสอบการรวมระบบ แน่นอนว่าสิ่งเหล่านี้มักเขียนด้วย JUnit เช่นกันและบ่อยครั้งเกี่ยวข้องกับการใช้การเยาะเย้ย ในความคิดของฉัน "JUnit" เป็นชื่อที่แย่มากสำหรับผลิตภัณฑ์ที่ช่วยให้เราสามารถทำการทดสอบประเภทต่างๆได้ทั้งหมด


8
ขอบคุณเดวิด หลังจากสแกนชุดรหัสบางชุดดูเหมือนว่าเป็นการปฏิบัติทั่วไป - แต่สำหรับฉันแล้วสิ่งนี้เอาชนะจุดประสงค์ของการสร้างการทดสอบหน่วยและเพิ่มค่าใช้จ่ายในการบำรุงรักษาให้เหลือน้อยมาก ฉันเข้าใจว่าทำไมต้องมี mocks และทำไมการพึ่งพาสำหรับการดำเนินการทดสอบจึงต้องมีการตั้งค่า แต่การตรวจสอบวิธีการที่พึ่งพา dependencyA.XYZ () ถูกดำเนินการทำให้การทดสอบมีความเปราะมาก
รัสเซล

@Russell แม้ว่า "type C" เป็นอินเตอร์เฟสสำหรับ wrapper รอบ ๆ ไลบรารีหรือรอบ ๆ ระบบย่อยที่แตกต่างกันของแอปพลิเคชันของคุณ
Dawood ibn Kareem

1
ฉันจะไม่บอกว่ามันไร้ประโยชน์อย่างสมบูรณ์เพื่อให้แน่ใจว่าระบบย่อยหรือบริการบางอย่างถูกเรียกใช้ - เพียงว่าควรมีแนวทางบางอย่างอยู่รอบตัว (การกำหนดเป็นสิ่งที่ฉันต้องการจะทำ) ตัวอย่างเช่น: (ฉันอาจจะเข้าใจง่ายเกินไป) บอกว่าฉันใช้ StrUtil.equals () ในรหัสของฉันและตัดสินใจที่จะเปลี่ยนไปใช้ ) การทดสอบของฉันอาจล้มเหลวแม้ว่าการใช้งานจะถูกต้อง การยืนยันการโทร IMO นี้เป็นแนวปฏิบัติที่ไม่ดีแม้ว่าจะใช้สำหรับไลบรารี / ระบบย่อย ในทางกลับกันการใช้การตรวจสอบเพื่อให้แน่ใจว่าการโทรไปยัง closeDbConn อาจเป็นชื่อผู้ใช้ที่ถูกต้อง
รัสเซล

1
ฉันเข้าใจคุณและเห็นด้วยกับคุณอย่างสมบูรณ์ แต่ฉันก็รู้สึกว่าการเขียนแนวทางที่คุณอธิบายสามารถขยายไปสู่การเขียนตำราเรียนทั้ง TDD หรือ BDD ในการยกตัวอย่างการโทรequals()หรือequalsIgnoreCase()จะไม่เป็นสิ่งที่ระบุไว้ในข้อกำหนดของชั้นเรียนดังนั้นจะไม่มีการทดสอบหน่วยต่อ se อย่างไรก็ตาม "การปิดการเชื่อมต่อฐานข้อมูลเมื่อเสร็จสิ้น" (ไม่ว่าสิ่งนี้หมายถึงอะไรในแง่ของการใช้งาน) อาจเป็นข้อกำหนดของชั้นเรียนแม้ว่ามันจะไม่ใช่ สำหรับฉันแล้วสิ่งนี้เกิดขึ้นกับความสัมพันธ์ระหว่างสัญญา ...
Dawood ibn Kareem

... ของคลาสตามที่แสดงในข้อกำหนดทางธุรกิจและชุดวิธีการทดสอบที่ทดสอบหน่วยนั้น การกำหนดความสัมพันธ์นี้จะเป็นหัวข้อสำคัญในหนังสือใด ๆ บน TDD หรือ BDD ในขณะที่ใครบางคนในทีม Mockito สามารถเขียนโพสต์ในหัวข้อนี้สำหรับ wiki ของพวกเขาฉันไม่เห็นว่ามันจะแตกต่างจากวรรณกรรมอื่น ๆ ที่มีอยู่มากมาย หากคุณเห็นว่ามันแตกต่างกันอย่างไรโปรดแจ้งให้เราทราบและบางทีเราสามารถทำงานร่วมกันได้
Dawood ibn Kareem

60

แน่นอนคำตอบของเดวิดนั้นถูกต้อง แต่ก็ไม่ได้อธิบายว่าทำไมคุณถึงต้องการ

โดยทั่วไปเมื่อทดสอบหน่วยคุณกำลังทดสอบหน่วยการทำงานแยก คุณทดสอบว่าอินพุตสร้างเอาต์พุตที่ต้องการหรือไม่ บางครั้งคุณต้องทดสอบผลข้างเคียงเช่นกัน สรุปการตรวจสอบช่วยให้คุณทำ

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

หรือคุณอาจเยาะเย้ย DAO และตรวจสอบว่ามันถูกเรียกในแบบที่คุณคาดหวัง ด้วย mockito คุณสามารถตรวจสอบว่ามีการเรียกสิ่งใดความถี่การเรียกใช้และแม้แต่ใช้ตัวจับคู่กับพารามิเตอร์เพื่อให้แน่ใจว่าได้รับการเรียกในรูปแบบเฉพาะ

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


29

นี่เป็นคำถามที่ยอดเยี่ยม! ฉันคิดว่าสาเหตุของมันคือต่อไปนี้เราใช้ JUnit ไม่เพียง แต่สำหรับการทดสอบหน่วย ดังนั้นคำถามควรแยกออกเป็น:

  • ฉันควรใช้ Mockito.verify () ในของฉันบูรณาการ (หรือการทดสอบอื่น ๆ สูงกว่าหน่วย) การทดสอบ?
  • ฉันควรใช้ Mockito.verify () ในการทดสอบหน่วยกล่องดำของฉันหรือไม่
  • ฉันควรใช้ Mockito.verify () ในการทดสอบหน่วยกล่องขาวหรือไม่

ดังนั้นหากเราจะเพิกเฉยต่อการทดสอบที่สูงกว่าหน่วยคำถามสามารถใช้ถ้อยคำใหม่ได้ " การใช้การทดสอบหน่วยกล่องสีขาวกับ Mockito.verify () สร้างคู่ที่ยอดเยี่ยมระหว่างการทดสอบหน่วยกับการใช้งานของฉันฉันสามารถสร้างกล่องสีเทา "การทดสอบหน่วยและกฎง่ายๆที่ฉันควรใช้ในการนี้ "

ทีนี้มาดูทีละขั้นตอนกัน

* - ฉันควรใช้ Mockito.verify () ในการทดสอบการรวม (หรือการทดสอบที่สูงกว่าหน่วยอื่น ๆ) ของฉันหรือไม่ * ฉันคิดว่าคำตอบนั้นชัดเจนไม่มากไปกว่านั้นคุณไม่ควรใช้ mocks สำหรับสิ่งนี้ การทดสอบของคุณควรใกล้เคียงกับแอปพลิเคชันจริงมากที่สุด คุณกำลังทดสอบกรณีการใช้งานที่สมบูรณ์ไม่ใช่ส่วนหนึ่งของแอปพลิเคชัน

* กล่องดำ VS กล่องสีขาวหน่วยทดสอบ * ถ้าคุณกำลังใช้กล่องดำวิธีการสิ่งที่คุณกำลังทำจริงๆคุณจัดหา (ทุกชั้นสมมูล) นำเข้าเป็นรัฐการส่งออกและการทดสอบที่คุณคาดว่าจะได้รับ ในวิธีการใช้ mocks โดยทั่วไปนี้เป็นสิ่งที่ชอบธรรม (คุณแค่เลียนแบบว่าพวกเขากำลังทำสิ่งที่ถูกต้องคุณไม่ต้องการทดสอบพวกเขา) แต่การโทร Mockito.verify () นั้นไม่จำเป็น

หากคุณใช้วิธีการแบบกล่องขาวในสิ่งที่คุณทำจริงๆคุณกำลังทดสอบพฤติกรรมของหน่วยของคุณ ในวิธีนี้การโทรไปที่ Mockito.verify () เป็นสิ่งสำคัญคุณควรตรวจสอบว่าหน่วยของคุณทำงานตามที่คุณคาดหวัง

กฎของหัวแม่มือสำหรับการทดสอบกล่องสีเทา ปัญหากับการทดสอบกล่องขาวคือมันสร้างการมีเพศสัมพันธ์สูง ทางออกหนึ่งที่เป็นไปได้คือทำการทดสอบกล่องสีเทาไม่ใช่การทดสอบกล่องขาว นี่คือการรวมกันของการทดสอบกล่องขาวดำ คุณจริงๆทดสอบพฤติกรรมของหน่วยของคุณเช่นในการทดสอบกล่องสีขาว แต่โดยทั่วไปจะทำให้การดำเนินงานไม่เชื่อเรื่องพระเจ้าเมื่อเป็นไปได้ เมื่อเป็นไปได้คุณจะทำการตรวจสอบเช่นในกรณีกล่องดำเพียงแค่ยืนยันว่าการส่งออกเป็นสิ่งที่คุณคาดหวัง ดังนั้นสาระสำคัญของคำถามของคุณคือเมื่อเป็นไปได้

มันยากจริงๆ ฉันไม่มีตัวอย่างที่ดี แต่ฉันสามารถยกตัวอย่างให้คุณได้ ในกรณีที่กล่าวถึงข้างต้นด้วย equals () และ vs equalsIgnoreCase () คุณไม่ควรเรียก Mockito.verify () เพียงแค่ยืนยันเอาต์พุต หากคุณไม่สามารถทำได้ให้แบ่งรหัสของคุณเป็นหน่วยย่อย ๆ จนกว่าคุณจะสามารถทำได้ ในทางกลับกันสมมติว่าคุณมี @Service บางส่วนและคุณกำลังเขียน @ Web-Service ที่เป็นตัวห่อหุ้มที่ @Service ของคุณ - มันจะมอบหมายการเรียกทั้งหมดไปยัง @Service (และทำให้เกิดข้อผิดพลาดพิเศษบางอย่าง) ในกรณีนี้การโทรไปที่ Mockito.verify () เป็นสิ่งสำคัญคุณไม่ควรทำซ้ำการตรวจสอบทั้งหมดที่คุณทำกับ @Serive เพื่อยืนยันว่าคุณกำลังโทรหา @Service ด้วยรายการพารามิเตอร์ที่ถูกต้องก็เพียงพอแล้ว


การทดสอบกล่องสีเทาเป็นเรื่องของความผิดพลาด ฉันมักจะ จำกัด ให้สิ่งต่าง ๆ เช่น DAO ฉันอยู่ในบางโครงการที่มีการสร้างช้ามากเนื่องจากมีการทดสอบกล่องสีเทามากมายการขาดการทดสอบหน่วยที่ใกล้เคียงและวิธีการทดสอบ blackbox มากเกินไปที่จะชดเชยความไม่ไว้วางใจในการทดสอบ greybox ที่คาดคะเน
Jilles van Gurp

สำหรับฉันนี่เป็นคำตอบที่ดีที่สุดเนื่องจากตอบเมื่อใช้ Mockito เมื่อ () ในสถานการณ์ที่หลากหลาย ทำได้ดี.
Michiel

8

ฉันต้องบอกว่าคุณถูกต้องจากมุมมองของวิธีการแบบคลาสสิก:

  • หากคุณสร้าง (หรือเปลี่ยน) ตรรกะทางธุรกิจของแอปพลิเคชั่นของคุณก่อนและจากนั้นครอบคลุมด้วยการทดสอบ (ปรับใช้) (วิธีทดสอบสุดท้าย ) มันจะเจ็บปวดและอันตรายมากที่จะให้การทดสอบรู้ว่าซอฟต์แวร์ของคุณทำงานอย่างไร ตรวจสอบอินพุตและเอาต์พุต
  • หากคุณกำลังฝึกวิธีทดสอบด้วยการขับเคลื่อนการทดสอบของคุณเป็นครั้งแรกที่จะถูกเขียนเปลี่ยนแปลงและสะท้อนถึงกรณีการใช้งานของซอฟต์แวร์ของคุณ การใช้งานขึ้นอยู่กับการทดสอบ บางครั้งนั่นหมายความว่าคุณต้องการให้ซอฟต์แวร์ของคุณถูกนำไปใช้ในบางลักษณะเช่นพึ่งพาวิธีการของส่วนประกอบอื่น ๆ หรือแม้แต่เรียกมันว่าจำนวนครั้งที่เจาะจง นั่นคือสิ่งที่Mockito.verify () เข้ามามีประโยชน์!

เป็นสิ่งสำคัญที่ต้องจำไว้ว่าไม่มีเครื่องมือสากล ประเภทของซอฟต์แวร์ขนาดของ บริษัท เป้าหมายและสถานการณ์ตลาดทักษะของทีมและสิ่งอื่น ๆ อีกมากมายมีอิทธิพลต่อการตัดสินใจเลือกวิธีการใช้งานในกรณีเฉพาะของคุณ


0

อย่างที่บางคนพูด

  1. บางครั้งคุณไม่มีเอาต์พุตโดยตรงที่คุณสามารถยืนยันได้
  2. บางครั้งคุณเพียงแค่ต้องยืนยันว่าวิธีการทดสอบของคุณกำลังส่งเอาต์พุตทางอ้อมที่ถูกต้องไปยังผู้ทำงานร่วมกัน (ซึ่งคุณกำลังเยาะเย้ย)

เกี่ยวกับข้อกังวลของคุณเกี่ยวกับการทำลายการทดสอบของคุณเมื่อทำการเปลี่ยนโครงสร้างซึ่งคาดว่าจะค่อนข้างเมื่อใช้ mocks / stubs / spies ฉันหมายถึงโดยนิยามและไม่เกี่ยวกับการใช้งานเฉพาะเช่น Mockito แต่คุณสามารถคิดด้วยวิธีนี้ - ถ้าคุณจำเป็นต้องทำการปรับโครงสร้างใหม่ที่จะสร้างการเปลี่ยนแปลงครั้งใหญ่ในวิธีการทำงานของคุณมันเป็นความคิดที่ดีที่จะทำในแนวทาง TDD ซึ่งหมายความว่าคุณสามารถเปลี่ยนการทดสอบของคุณก่อนเพื่อกำหนด พฤติกรรมใหม่ (ที่จะล้มเหลวในการทดสอบ) จากนั้นทำการเปลี่ยนแปลงและทำการทดสอบผ่านอีกครั้ง


0

ในกรณีส่วนใหญ่เมื่อคนไม่ชอบใช้ Mockito.verify มันเป็นเพราะมันใช้ในการตรวจสอบทุกอย่างที่หน่วยการทดสอบทำและนั่นหมายความว่าคุณจะต้องปรับการทดสอบของคุณหากมีอะไรเปลี่ยนแปลงในมัน แต่ฉันไม่คิดว่าเป็นปัญหา หากคุณต้องการที่จะเปลี่ยนแปลงวิธีการที่ทำโดยไม่จำเป็นต้องเปลี่ยนการทดสอบนั่นหมายความว่าคุณต้องการเขียนการทดสอบที่ไม่ทดสอบทุกอย่างที่วิธีการของคุณทำเพราะคุณไม่ต้องการให้มันทดสอบการเปลี่ยนแปลงของคุณ . และนั่นคือวิธีคิดที่ผิด

สิ่งที่เป็นปัญหาคือถ้าคุณสามารถปรับเปลี่ยนวิธีการของคุณและการทดสอบหน่วยที่ควรจะครอบคลุมการทำงานทั้งหมดจะไม่ล้มเหลว นั่นหมายความว่าสิ่งที่คุณตั้งใจจะเปลี่ยนแปลงนั้นคือผลลัพธ์ของการเปลี่ยนแปลงนั้นไม่ครอบคลุมในการทดสอบ

ด้วยเหตุนี้ฉันจึงชอบที่จะล้อเลียนให้มากที่สุด: ก็จะล้อเลียนวัตถุข้อมูลของคุณด้วย เมื่อทำเช่นนั้นคุณไม่เพียง แต่สามารถใช้การตรวจสอบเพื่อตรวจสอบว่ามีการเรียกใช้วิธีการที่ถูกต้องของคลาสอื่น แต่ยังรวมถึงข้อมูลที่ถูกส่งผ่านทางวิธีการที่ถูกต้องของวัตถุข้อมูลเหล่านั้น และเพื่อให้เสร็จสมบูรณ์คุณควรทดสอบลำดับการโทรที่เกิดขึ้น ตัวอย่าง: หากคุณแก้ไขวัตถุเอนทิตี db แล้วบันทึกโดยใช้ที่เก็บข้อมูลนั้นไม่เพียงพอที่จะตรวจสอบว่า setters ของวัตถุนั้นถูกเรียกด้วยข้อมูลที่ถูกต้องและวิธีการบันทึกของที่เก็บเรียกว่า หากพวกเขาถูกเรียกในลำดับที่ผิดวิธีการของคุณยังคงไม่ทำในสิ่งที่ควรทำ ดังนั้นฉันไม่ใช้ Mockito.verify แต่ฉันสร้างวัตถุ inOrder กับ mocks ทั้งหมดและใช้ inOrder.verify แทน และถ้าคุณต้องการทำให้สมบูรณ์คุณควรโทร Mockito ตรวจสอบไม่มีการโต้ตอบในตอนท้ายและผ่านมัน mocks ทั้งหมด ไม่เช่นนั้นบางคนสามารถเพิ่มฟังก์ชันการทำงาน / พฤติกรรมใหม่โดยไม่ต้องทดสอบซึ่งอาจหมายถึงหลังจากนั้นในขณะที่สถิติการครอบคลุมของคุณอาจเป็น 100% และคุณยังคงกำลังรวบรวมรหัสที่ไม่ได้ยืนยันหรือยืนยัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.