TDD is not the f*ucking red, green and blue Part 4

Jassadakorn.ket
te<h @TDG
Published in
3 min readJun 13, 2022

มหากาพย์ TDD ในครั้งนี้ยังอีกยาวไกล สงครามยังไม่จบอย่าเพิ่งนับศพทหาร รู้เขารู้เรารบร้อยครั้งชนะร้อยครับ เริ่มไม่เกี่ยวแล้ว…พอ พอ พอ

  1. มึง…….กูรู้สึกว่างเปล่าเมื่ออยู่ในหน้าจอ test ( 2 สิ่งที่ต้องนึกถึง )
  2. ชิบหายใช้เวลาเยอะมากกว่าจะเขียนได้ 1 case ( ช้าไม่เป็นก็เร็วไม่ได้ )
  3. What the fuck รันกี่ครั้งทำไมแม่งไม่เหมือนเดิม ( 3 สิ่งต้องห้าม )

จาก Part ที่แล้ว
ถ้าไม่ให้ใส่ Singleton ลงไป
ถ้าไม่ให้ใส่ API ลงไป

แล้วจะทำยังไงให้ดึงข้อมูลได้ล่ะเรามาดูกัน

จะทดสอบอะไร ก็ให้สิ่งนั้นเป็นของจริง ที่เหลือเป็นของเก๊ อย่าใส่ของจริงมาทั้งหมดโว้ยย ( Protocol is Hero )

จากภาพด้านบนเรามีการ Initial ProductAPI() ไว้ใน ViewModel ซึ่งเป็นวิธีที่ไม่ควรทำ เพราะมันจะทำให้ผลการ test ขึ้นอยู่กับ server ซึ่งมันไม่ใช่ logic ของ sut เพียงอย่างเดียว แถมอาจจะใช้เวลานานในการ test อีกด้วย

ซึ่งสิ่งที่จะมาช่วยตรงนี้ก็คือ Protocol
ซึ่ง Protocol เนี่ยเมื่อมารวมกับสิ่งที่เรียกว่า Dependency Injection แล้ว
ก็จะทำให้เราสร้างของเก๊ขึ้นมาเพื่อมาทดแทนของจริง เพื่อที่จะได้ไม่ต้องไปเกี่ยวกับ server และทำให้การ run test รวดเร็ว fast and furious

แล้วของเก๊คืออะไรวะ ?

ก่อนจะไปดูหน้าตาของเก๊ที่ว่า เรามาทำความรู้จักกับ DI หรือ Dependency Injection กันสักเล็กน้อยก่อน

DI ( Dependency Injection ) แปลตรงตัวง่ายๆก็คือการเอา Dependency
ที่ initial
อยู่ข้างใน class ออกไป แล้ว initial dependency นั้นจากภายนอก แล้วส่ง
( inject ) เข้ามาแทน ซึ่งมีช่องทางการ inject ได้ 3 อย่างนี้

  1. Initializer ( Constructor ) Injection
  2. Property Injection
  3. Parameter Injection

ซึ่งในบทความนี้จะใช้ข้อ 1 Initializer ( Constructor ) Injection

จากรูปข้างบน dependency ที่ initial อยู่ข้างใน ProductViewModel คือ ProductAPI ก็ลบมันออกไปซะรอไรอ่ะ

เหตุผลที่ต้องลบมันออกไปเพราะอย่างที่เคยไปด้านบนว่า

เพราะมันจะทำให้ผลการ test ขึ้นอยู่กับ server ซึ่งมันไม่ใช่ logic ของ sut เพียงอย่างเดียว แถมอาจจะใช้เวลานานในการ test อีกด้วย

จากรูปด้านบนเปลี่ยนจาก initial ProductAPI() เป็น Protocol ที่สร้างขึ้นมา
แต่ที่เห็นยังมี Error เพราะว่าเรายังไม่ได้ทำการ Injection dependency
ผ่านทาง initializer

จากภาพเราทำการเปิดช่องให้ inject class อะไรก็ได้ที่ไป inherit ProductAPIProtocol ซึ่ง ProductAPI ตัวจริงก็ inherit เรียบร้อย

จากนั้นก็ set default initial ให้มันด้วยเผื่อลืม inject ตอน code ตัวจริง

แล้ว API ตัวปลอมล่ะ

เราสร้าง ProductAPISpy ขึ้นมาเพื่อ return stub ของ ProductList ออกไปทาง completion

เรื่อง Spy สามารถหาอ่านได้เพิ่มเติมในหัวข้อ Test Double ทั่วไปใน internet

จะสังเกตุได้ว่านอกจาก stub แล้วจะมีตัวอื่นอีกด้วยดังนี้

  1. invokedGetProduct : เอาไว้เพื่อให้เรามั่นใจว่า API ได้ถูก call จริงๆใน ViewModel ของเรานะจาก Boolean
  2. invokedGetProductCount : เอาไว้ให้เรามั่นใจว่า API ถูก call ไปตามจำนวนที่เราต้องการ เช่น ยิงครั้งเดียวพอไม่เบิ้ลจากจำนวน count
  3. invokedGetProductParameters: เอาไว้ให้เรามั่นใจว่า parameter ถูกส่งผ่านไปยัง API ของเราจริงๆนะ
  4. invokedGetProductParametersList : เอาไว้ให้เรามั่นใจว่าหาก API ของเรายิงได้หลายครั้งสามารถไปเรียกดู History ได้ว่ายิงไปด้วย parameter อะไรบ้าง
  5. StubbedGetProductCompletionResult : เอาไว้ให้เรา return ProductList เพื่อจำลอง API

ซึ่งสามารถใช้ Xcode Plugin เพื่อ Generate auto มาให้เลยไม่ต้องเขียนเองนะเมื่อยมือ

จากนั้นเราก็ทำการ set stub ด้วย productList ที่เราทำเอาไว้ตอนแรก จากนั้น inject เข้าไปที่ ProductViewModel ของเรา แต่ตอนของจริงไม่ต้อง initial ProductAPI แล้ว inject นะเพราะเราทำ default initial ไว้แล้ว

จากนั้นลอง run test ดูผลที่ได้ก็คือ fail ก็เพราะเรายังไม่ทำอะไรกับ logic ของ function getProduct เลย

จากภาพด้านบนตาม spec ที่เราได้ออกแบบไว้ test_SetProduct20Items_getProduct_ShouldShowOnly10ItemWithoutExpire

ตอนนี้เราก็ทำสำเร็จแล้ว แต่… ยังไม่จบนะเพราะว่ายังไม่ได้ filter expire เลย
เอาไว้เป็นตอนหน้าละกันนะ บทความเริ่มยาวเกินไปละ

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

--

--