Tag Archives: POODR

Test อะไรบ้าง?

มาเปลี่ยนเรื่องคุยกันหน่อยหลังจากไปอยู่ทาง Functional programming 2 บล๊อกติดๆ คราวนี้จะมาพูดกันถึงเรื่องที่ผมรู้สึกว่าบางทีผมก็รู้ บางทีผมก็รู้สึกว่าไม่รู้ แปลกๆยังไงชอบกลกันบ้าง เรื่องนี้คือ testing ครับ

testing ที่จะมาพูดในวันนี้ ผมได้ปัญหามาจากบทสุดท้ายในหนังสือ Practical Object-Oriented Design in Ruby ครับ มีประโยคนึงที่ผมทำ highlight ไว้เพราะคิดว่ามันน่าสนใจ สามารถนำไปเป็น guideline ในการเขียน test ได้อย่างดี แต่ก็ยังไม่ค่อยเข้าใจเท่าไหร่ ขอคัดมาบางส่วนตามนี้ครับ

Guidelines for what to test: Incoming messages should be tested for the state they return. Outgoing command messages should be tested to ensure they get sent. Outgoing query messages should not be tested.

(คนเขียนเค้าเป็น OO สาย smalltalk เค้ามอง method, function เป็น message ที่ส่งระหว่าง object ครับ  ซึ่งจากประโยคด้านบน message == method/fucntion)

ในหนังสือเล่มนี้เค้าพูดถึงแต่ unit และ isolation test ผมจึงตีความว่าประโยคข้างบนนี้พูดถึงแค่ unit และ isolation test เช่นกัน

เค้าบอกว่า ให้ test message ขาเข้า (incoming) ด้วยการตรวจผลลัพธ์ ส่วน message ขาออก(outgoing) ให้ทดสอบเฉพาะ message ที่เป็น command message คือ message ที่ไปสั่งให้ object อื่นทำงาน ไม่ได้ต้องการค่ากลับมา (หรือเรียกตามประสา functional ว่า side-effect นั่นเอง) ส่วน message ขาออกที่เป็น query message คือ message ที่ส่งไปเพื่อถามค่า คาดหวังค่ากลับมา ไม่ต้องทำการ test

ตัวอย่าง ข้างล่างนี้ calculate คือ incoming message, price_for คือ outgoing query message  และ decrease คือ outgoing command message

class Cashier
  def initialize(item_price, stock_control)
    @item_price = item_price
    @stock_control = stock_control
  end

  def calculate(items)
    prices = items.map { |id, amount| @item_price.price_for(id) * amount }
    items.each do |id, amount|
      @stock_control.decrease(id, amount)
    end
    prices.inject(0, :+)
  end
end

ผมสงสัยว่า ทำไมเค้าถึงบอกว่าอย่า test outgoing query message ในหนังสือก็ไม่ได้อธิบายอย่างชัดเจน ลอง search หาจากเน็ตก็หาไม่เจอ เลยกลับตัดสินใจกลับไปวนอ่านซ้ำอีกหลายรอบ จนในที่สุดก็นึกถึงคำอธิบายที่พอจะเข้าเค้าขึ้นมาได้

ผมเข้าใจว่าเราไม่ควรทดสอบ outgoing query message ในระดับ unit และ isolation แต่เราควรทดสอบมันในระดับที่สูงขึ้น เช่น integration, functional, acceptance, end-to-end เพราะว่าการทดสอบ outgoing query message ในระดับ unit และ isolation นั้น ทำให้เราผูกติดกับ dependency มากเกินไป เป็นการรู้มากเกินไปว่า dependency ของ class Cashier นั้นเป็นใคร มันไม่ใช่สิ่งที่ระดับ unit ควรจะรู้

การเขียน test นั้น  เปรียบเสมือนการจำลองตัวเองเป็นผู้เรียกใช้งาน ในฐานะผู้ใช้งานเราไม่ควรจะต้องรู้ว่า calculate มี dependency เป็นใคร เราควรรู้แค่ว่า calculate ทำอะไรให้กับระบบโดยรวมบ้าง ซึ่งถูกสะท้อนออกมาโดยการ test incoming message และ outgoing command message การที่รู้มากเกินไปในระดับ unit ทำให้เกิดปัญหาตามมา ทั้ง test พังง่าย แก้อะไรนิดหน่อยก็พัง และทำให้เรา reuse Cashier ได้ยากขึ้นด้วย เพราะความคิดจะติดอยู่กับว่า item_price จะต้องเป็น class ที่ return ค่านั้นค่านี้เสมอ แล้วเราจะเปลี่ยน implementation ของ item_price ได้อย่างไร

ในหนังสือผู้เขียนมีการเน้นย้ำบ่อยครั้งว่า เราควรเขียน code และ test ผูกกับสิ่งที่ไม่เปลี่ยนแปลง (interface) ในที่นี้คือ calculate และ decrease และปล่อยส่วนที่เหลือให้เป็นอิสระเปลี่ยนแปลงได้ จะทำให้ code เรา reuse ได้ง่ายขึ้น และเขียน test ได้ดีขึ้นด้วย

อย่างที่บอกไปว่าเราไม่ test ในระดับ unit แต่เราจะไป test ในระดับที่่สูงขึ้นแทน เพราะในระดับที่สูงขึ้นเรามีความรู้ในภาพรวมแล้วว่าระบบนี้ใช้ทำอะไร และต้องมี component อะไรเป็นส่วนประกอบบ้าง จะ test แค่การ integrate บางส่วน หรือจะทำ end-to-end ก็ตามความสะดวกและเหมาะสมเลยครับ

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

Advertisements