Tag Archives: Testing

Guideline ในการเขียนเทสของผม

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

แนวทางในการเขียนเทสให้ได้แบบนั้น มีดังนี้

  • เทสต้องรันได้เร็ว ถ้ามันช้าเราจะไม่อยากรัน ไม่อยากเขียนเพิ่ม
  • เทสต้องดูแลได้ง่าย ซึ่งหมายถึง
    • เขียนง่าย การ setup ต้องไม่ซับซ้อน
    • อ่านง่าย ชัดเจนว่าต้องการทดสอบอะไร
    • faill เมื่อควรจะ fail จริงๆ
    • ไม่ fail แบบ random
  • เขียนให้น้อยที่สุดที่ตราบที่เรายังมั่นใจเวลาเราจะแก้โค้ด ตราบที่เรายังมั่นใจเวลาเราจะ deploy
    • ยิ่งปริมาณเทสเยอะยิ่งมีแต่วันที่มันจะรันช้าลง
    • ถ้าเราไม่เทสตรงนี้ ความเสียหายคืออะไร เรายอมรับได้มั้ย ถ้าเราพบภายหลังสามารถแก้ไขได้ยากหรือง่ายเพียงใด
    • บางจุดที่ถ้ามันมี error หรือ bug เราจะรู้หรือเห็นได้ทันที ผมก็ไม่เขียนเทส
Advertisements

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 ก็ตามความสะดวกและเหมาะสมเลยครับ

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

สรุป presentation: The Deep Synergy Between Testability and Good Design by Michael Feathers

The Deep Synergy Between Testability and Good Design.

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

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

มาเริ่มกันจาก quote เท่ๆ กันก่อน

ดีไซน์ดีทำให้เทสง่าย เทสง่ายไม่ได้แปลว่าดีไซน์ดี

อีกอัน

เมื่อมันเทสยาก อย่าแก้เทสให้เทสง่าย ให้มองหาปัญหาของดีไซน์ แล้วแก้ดีไซน์ เทสก็จะง่ายไปเอง

ต่อมาๆ พูดถึงปัญหาในการเทสกัน

  • อยากจะ access  local variable ในเทส – method ทำหลายอย่างเกินไป
  • setup เทสยาก – coupling เยอะเกินไป
  • เทสตายแบบรันไม่เสร็จ – คลาสไม่ดูแลสิ่งที่ตนควรทำ (เช่น ไม่ release memory)
  • รันเทสเดี่ยวๆ ผ่าน, รันหลายๆ เทสพร้อมกัน ไม่ผ่าน – มี global state
  • เฟรมเวิร์คทำให้เทสยาก – โดเมนของงานเราปนอยู่ในเฟรมเวิร์คมากเกินไป
  • ต้อง stub ผลลัพธ์ของ stub ซ้อนกันหลายชั้น – คลาสรู้จักส่วน private  collaborator ของคลาสอื่น
  • stub หรือ mock dependency ยาก – code อิงกับ implementation ของคลาสอื่นมากเกินไป
  • เทสคลาสแล้วมีผลลัพธ์อื่นที่คาดไม่ถึง (เช่น อยู่ดีๆ ก็ส่งอีเมล์ออกไปจริงๆ) – คลาสใหญ่ไปเกินที่เราจะรู้ว่ามันทำอะไรบ้าง
  • ไม่มีช่องให้ใส่ parameter เพื่อจะทดสอบกรณีต่างๆ ง่ายๆ – คลาสทำหลายอย่างเกินไป
  • เวลาจะเทสต้องเตรียม parameter เยอะเกินไป – คลาสหรือเมท็อด ทำหลายอย่างเกินไป
  • อยากจะเขียนเทส private method – คลาสทำหลายอย่างเกินไป
  • แก้ code นิดหน่อย unit test พังเยอะ – แปลว่าเราดีไซน์ในลักษณะที่เข้าไปแก้คลาสง่ายกว่าเขียนต่อยอด

ตอนท้ายผู้พูดได้เสนอ แนวคิดว่าทำไมดีไซน์ดีถึงทำให้เทสง่าย ทำไมมันถึงเกี่ยวข้องกัน ว่าดังนี้

ดีไซน์ที่ดี คือ ดีไซน์ที่คนเข้าใจมันได้ง่าย การเขียนเทสในมุมหนึ่งคือการทำความเข้าใจโค้ด ซึ่งก็คือการทำความเข้าใจดีไซน์ ถ้าดีไซน์เข้าใจได้ง่ายแล้ว นั่นทำให้การทำความเข้าใจด้วยการเขียนเทสง่ายตามไปด้วย