Dependency injection

posted on 03 Aug 2008 02:47 by wonam in softdev

ในการทำ unit test สิ่งที่น่ากลัวและสร้างปัญหาที่สุดก็คือการที่ส่วนย่อยของเราขึ้นกับส่วนอื่น ๆ

พิจารณาตัวอย่างโค้ด ruby ด้านล่างนี้

class TestA
  def initialize(x,y)   
    @my_b = TestB.new(x,y)
  end
  def
my_private_work(z)     # do something   end
  def work(z)     y = my_private_work(z)     return @my_b.execute(y)   end end

สำหรับคนที่ไม่ได้เขียน Ruby  เมท็อด initialize คือ constructor ของคลาส เวลาเราสั่ง TestA.new(10,20) มันจะสร้าง object ขึ้นมาแล้วเรียก initialize(10,20) ที่ object นั้น จากนั้นจึงค่อยคืน object ให้เราอีกที

สังเกตว่า object ของคลาส TestA มี attribute อันหนึ่งคือ my_b ที่อ้างไปถึง object ของคลาส TestB  ดังนั้นเวลาเราจะ test คลาส TestA เราก็จำเป็นจะต้องมีคลาส TestB มาก่อน ไม่เช่นนั้นเราก็จะสร้าง object ของคลาส TestA ไม่ได้ 

ปัญหาเกิดขึ้นเมื่อ TestB เขียนได้ยุ่งยาก หรือเรายังไม่อยากเขียน  นอกจากนี้ TestB ที่เราเขียนนั้นอาจมีปัญหาได้ทำให้เวลาทดสอบเราไม่รู้ว่าเวลาเกิดข้อผิดพลาดขึ้นแล้วปัญหาเกิดจาก TestA ที่เรากำลังทดสอบ หรือว่าเกิดจากคลาส TestB ที่มันใช้

วิธีการจัดการกับ dependency ในลักษณะนี้ก็มีสองแบบ คือการใช้ stub หรือไม่ก็ใช้ mock (ต่างกันอย่างไร? อ่านบทความ Mocks Aren't Stubs ของ Martin Fowler)  หลักการคร่าว ๆ ก็คือจัดการกับ dependency โดยสร้างของปลอมหรือของเทียมขึ้นมา "ตัดหน้า" การทำงานของคลาสจริง

แต่ blog นี้ไม่ได้เกี่ยวกับว่าจะใช้ mock หรือจะใช้ stub  สังเกตดี ๆ เราจะพบว่าใน code ด้านบนไม่ว่าจะ mock หรือว่าจะ stub ก็คงจะใช้ไม่ได้  เพราะว่าคำสั่งที่สร้าง dependency ดันไปอยู่ใน constructor ที่ใช้สร้าง object

อีกตัวอย่างของ code ที่ test ยากมาก ก็คือ code ที่ใช้ singleton เพราะว่าเราจะเอา mock กับ stub ไปตัดหน้าการทำงานของ singleton class นั้นได้ยาก

แล้วจะทำอย่างไร?

เราก็ต้องแยก (refactor) ส่วนที่สร้าง dependency ออกจาก code ที่เราจะ test แล้วก็ "ฉีด" (inject) มันเข้าไปแทนด้วย code ส่วนอื่น (เช่นส่วนที่เรียกใช้ หรือส่วนที่สร้าง object นั้น) จากตัวอย่างข้างบนเราคงจะแก้ส่วน constructor เป็น

class TestA
  def initialize(b)
    @my_b = b
  end
  ....
end

ทีนี้เราก็สามารถปรับเปลี่ยนคลาส TestB ได้ตามใจชอบ โดยจะเขียนคลาส TestBStub หรือจะสร้าง Mock object ของคลาส TestB ก็ได้ (สังเกตว่าเราเอา argument x และ y ทิ้งไปพร้อม ๆ กันเลย เพราะว่าจริง ๆ แล้วใน code เก่าเราไม่ได้ใช้มันนอกจากเอาไว้สร้าง my_b)

การฉีด dependency ในลักษณะแบบนี้เรียกว่า Dependency Injection (ดูวิกิพีเดีย) หรือในชื่อเก่า (ที่งงมาก) ว่า Inversion of Control  ซึ่งเทคนิคดังกล่าวพัฒนามาจากแวดวง J2EE

เมื่อเราแยก dependency ออกมาจาก code ในส่วนกำหนดค่าเริ่มต้นของ object แล้ว  สิ่งที่เหลือคือส่วนต่าง ๆ ของระบบซอฟต์แวร์ที่แยกจากกันเป็นก้อน ๆ  ดังนั้น code ในส่วนที่สร้างวัตถุทั้งหมดก็จะมีแต่การเรียก constructor แล้วก็ส่งวัตถุไปมาเท่านั้นเอง

เนื่องจากขั้นตอนสุดท้ายที่เราประกอบร่าง object ต่าง ๆ เข้าด้วยกัน (พี่ป๊อก เรียกว่า "ร้อย" วัตถุเข้าด้วยกัน)มันเหมือนแค่ระบุว่าใครจะร้อยเข้ากันอย่างไร ก็เลยมีการทำ framework ของ dependency injection ขึ้นมามากมาย ที่ดัง ๆ หน่อยจากทางฝั่ง Java น่าจะเป็น (เอาตามที่ผมเคยได้ยินมา) Spring, PicoContainer, หรือ Guice ของ google (ดูรายชื่อได้จากบทความวิกิพีเดีย)

หมายเหตุ: เมื่อประมาณอาทิตย์ก่อนอ่านบล็อก Top 10 Things I do on Every Project ของ Misko Hevery เห็นเขาพูดถึง DI framework ก็เลยเห็นว่าน่าจะเอามาเขียนถึง

เพิ่มเติม: เพิ่งเห็นว่า Misko (คนเดิม) เขียนถึงกรณีที่การขึ้นต่อกันเป็นวงรอบ

Comment

Comment:

Tweet

กำลังเทรนเรื่อง unit test อยู่พอดีเลยครับอาจารย์

ยังดี ของผมแค่ภาษา C...

#4 By โอ๊ะโอ๋ (58.137.97.178) on 2008-08-04 10:17

ขอบคุณคุณ deans4j มากครับ ที่เข้ามาช่วยอธิบาย

ขอเสริมด้วยนะครับ

ตรง mock นี่มันจะโยงไปถึงการทดสอบแบบที่เรียกว่า interaction-based testing

คือปกติเวลาเรา test เราจะเรียกใช้งาน class แล้วก็เขียน assert ๆ ว่า state ของคลาสมันเปลี่ยนไปตามที่เราต้องการหรือเปล่า อันนี้เค้าเรียกว่าเป็น state-based testing

ส่วนการ test ที่ดูเฉพาะการติดต่อระหว่างวัตถุ จะเรียกว่า interaction-based testing เช่น ที่ใช้ mock นี่แหล่ะครับ

ปกติพอเราทำ testing ที่เป็น state-based เนี่ยะ บางทีเราต้องไปล้วงลูกดูว่าวัตถุมันเปลี่ยนสถานะถูกหรือเปล่า ซึ่งทำให้ test code เราขึ้นกับตัว code ของคลาสมาก

มีลิงก์เกี่ยวกับเรื่องนี้ที่
http://nat.truemesh.com/archives/000342.html กับ http://codebetter.com/blogs/jeremy.miller/pages/129544.aspx

แล้วก็ผมเคยทำ slide เรื่องนี้เอาไว้ http://www.cpe.ku.ac.th/~jtf/219343-49/lect4-unit2.pdf เรื่องพวกนี้ มีตัวอย่างการใช้ easymock อยู่ด้วย อาจจะพออ่านได้นะครับ

#3 By wonam on 2008-08-04 09:42

ในบรรดา DI ผมชอบ Guice ที่สุดแล้วครับ เพราะมัน config อยู่ใน environment ของ Java เอง :) เป็นอีกหนึ่งเหตผลดีๆ ที่ยังทำให้อยู่กับ static type language ต่อไปครับ จะต่างกับ Spring ที่ใช้ xml ถ้าเขียนผิดนี่รอ runtime/deploytime ถึงจะรู้

@veer

mock จะตรวจสอบ behavior ครับ stub จะตรวจสอบ state เอาครับ

mock เลยจะออกแนว testing แบบ record/playback คือเรา record พฤติกรรมที่เราคาดหวังเอาไว้

พอเราทดสอบ unit ใดๆ เนื่องจาก unit นั้นไป depend กับ object ที่เรา mock ไป ปุ่ม record ที่ 2 ก็จะทำงานอัตโนมัติ

เสร็จแล้วเราก็มา playback ว่า record แรก (อันที่เรา expect) ตรงกับ record ที่ใช้จริงๆ หรือเปล่า (actual)

ส่วน stub นี่ง่ายกว่า mock ครับ แต่ก็หยาบกว่าเหมือนกัน เราจะ extends class จริงๆ มา แล้วเขียน code ให้โง่ๆ ง่ายที่สุดให้มันเหมือนว่าทำงานได้ อย่างตัวอย่างที่ M.Fowler ยกมาคือ mail stub แทนที่จะส่ง mail จริงๆ มันก็สมมติว่าส่ง แล้วคืนค่าสำเร็จออกไป (กรณีที่ method เป็น void อาจจะเพิ่ม method ใหม่อย่างที่ Fowler ใช้ method count ก็ได้ หรืออาจจะไม่จำเป็นต้องมี แค่มันไม่ throw exception ออกก็อนุมานได้แล้วว่ามันทำงานสำเร็จ)

ปกติเค้าจะพูดรวมๆ เอา stub+mock เป็น mock ทั้งๆที่จริงๆ บางทีเป็นแค่ stub เท่านั้นเอง (เป็นที่มาของ article นี้ของ M.Fowler)

ตัวอย่างเช่น document ของ guice พูดถึง mock แต่จริงๆ มันคือ stub :P

http://docs.google.com/View?docid=dd2fhx4z_5df5hw8

#2 By deans4j (124.120.141.245) on 2008-08-04 04:20

ตามไปอ่านเรื่อง Mocks Aren't Stubs มา ... ทำให้เข้าใจขึ้นอีกมาก. แต่ก็เหมือนพอใจเข้าใจ mock บ้างๆ เพราะดูจากตัวอย่าง มีอะไรคล้ายๆ expect. แต่งงๆ ว่า stub อะไร sad smile

#1 By veer on 2008-08-03 14:12