Unit “Testability”

เคยพูดไปเมื่อก่อนว่า โค๊ดที่ถือว่าเป็นโค๊ดมรดก หรือ Legacy Code นั้น ว่ากันว่าเป็นโค๊ดที่ไม่พึงประสงค์ เหมือนเป็นภาระที่สืบทอดกันมาเป็นรุ่น ๆ ในองค์กร ซึ่งเกิดจากการที่โค๊ดนั้นมันไม่มี Unit Test

ทีนี้ ก็ในเมื่อเรารู้อยู่แล้วว่าโค๊ดมันไม่มี Unit Test เลยมีแต่คนรังเกียจ แล้วเราจะเพิ่ม Unit Test เข้าไปในโค๊ดเดิมเพื่อให้มันไม่เป็น Legacy Code เลยไม่ได้เหรอ คำตอบก็คือทำได้ครับ และก็ควรทำด้วย เพียงแต่ว่าการเพิ่ม Unit Test เข้าไปในโค๊ดเดิมนั้นมันมีค่าใช้จ่ายสูง ใช้เวลามาก และต้องมีการแก้ไขโค๊ดเดิมบางส่วน (หรืออาจจะเป็นส่วนมาก)

ที่ว่าค่าใช้จ่ายสูง ใช้เวลามากนั้น น่าจะพอนึกภาพออก ยิ่งโค๊ดมีขนาดใหญ่มากก็ต้องเขียน Unit Test มากก็ไม่แปลกถูกไหมครับ

แต่ว่าโค๊ดเดิมต้องแก้ด้วยหรือ ?

คำตอบคือ แล้วแต่ว่าโค๊ดเดิมครับ แต่โดยทั่ว ๆ ไปมันก็มักจะมีโค๊ดที่เทสต์ได้ยากถ้าไม่มีการแก้ไขเลย

สมมติว่าผมมีคลาส Java ชื่อว่า Logger ละกัน มันไม่ได้ทำอะไรมากมาย แค่ว่าเขียน log ลงไปในไฟล์เท่านั้นเอง ... แบบนี้ (เขียนไม่เต็มนะครับ ขี้เกียจ 55)

class Logger {
  private FileWriter writer;
  public Logger(File file) {
    writer = new FileWriter(file);
  }
  public void log(String message) {
    Date date = new Date();
    writer.write(String.format("%D : %s"), date, message);
  }
}

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

เราก็ต้อง refactor code โค๊ดให้มันมีการ "ยึดติด" น้อยลง แบบนี้ครับ

class Logger {
  private Writer writer;
  public Logger(Writer writer) {
    this.writer = writer;
  }
  public Logger(File file) {
    this.writer = new FileWriter(file);
  }

  public void log(Date date, String message) {
    writer.write(String.format("%D : %s"), date, message);
  }

  public void log(String message) {
    log(new Date(), message);
  }
}

ผมใช้วิธีสองอย่างในการทำให้ "การยึดติด" ลดลง คือการใช้ polymorphism - ใช้คลาส Writer แทนที่จะเป็น FileWriter และการใช้ parameter passing จากข้างนอกแทนที่จะเป็นการสร้างใหม่จากภายในคลาส (และมีอีก method นึงทำหน้าที่เป็นเหมือนกับการกำหนด default parameter) วิธีนี้ทำให้ผมไม่จำเป็นจะต้องอ่านไฟล์หลังจากที่เขียนลงไปแล้ว เพราะว่าผมสามารถใช้ StringWriter แทน FileWriter ได้นั่นเอง นอกจากนี้วิธีนี้ทำให้โค๊ดที่ใช้งานคลาสนี้สามารถใช้งานได้เหมือนเดิม เพราะว่า interface เดิมยังอยู่ครบเลยครับ

ส่วนตัว Unit Test นั้นก็สามารถเขียนแบบนี้ :-

class LoggerTest {
  @Test
  void test(){
    Stringwriter writer = new StringWriter();
    Logger logger = new Loger(writer);
    Date date = new GregorianCalendar(2000, 3, 15).getTime();

    logger.log(date, "test message");
    writer.close();
    assertEquals(writer.toString(), "03/15/2000 : test message");
  }
}

ยาวกว่าที่คิดแฮะ 555 setup ยาวครับ แต่จริง ๆ ก็ไม่ได้ซับซ้อนอะไร เทียบกับการไปอ่านไฟล์กลับขึ้นมาแล้วลำบากกว่าเยอะ แถมต้องมา parse ว่าวันที่ที่ log ไปมันวันที่เท่าไหร่อีก ... ดูลำบากชีวิตว่ามั้ยครับ

ตรงนี้แหละครับที่เป็นการวัดว่า ความสามารถในการถูกทดสอบ หรือ Testability นั้นมีมากน้อยแค่ไหน

ทีนี้คงมีคำถามเพิ่มเข้ามาว่า method ที่เราเทสต์กับ method ที่เราใช้จริงนั้นมันเป็นคนละ method กัน ซึ่งก็จริงครับ แต่ตราบใดที่ method ที่ใช้งานจริงนั้นมีความเรียบง่ายพอ ผมว่าก็สามารถปล่อยมันไว้แบบนั้นได้โดยที่ไม่มีปัญหาอะไร โดยเฉพาะอย่างยิ่งถ้าเป็น method ที่แค่สร้างค่า default เข้ามาแบบนี้ก็ไม่ต้องต้องไปสนใจมันมากก็ได้ครับ

จุดที่ต้องระวังอีกอย่างคือเปิดให้เข้าถึง method นั้นจะทำให้ user เข้ามาใช้ method นั้น ๆ โดยที่เราไม่ได้ตั้งใจให้เขาใช้ เพราะว่าต้องเปิดให้เข้าถึงจากภายนอก เราก็ต้องทำการจำกัดการเข้าถึงโดยไม่อนุญาตให้คลาสอื่นนอกจากคลาสที่เป็น unit test เข้าถึงได้นั่นเอง ซึ่งก็เป็นต้องดูเป็นรายภาษาไป อย่างของ C++ ก็อาจจะใช้ friend class ช่วย ส่วน Java ก็อาจจะเป็นการใช้ default access modifier ที่ไม่อนุญาตให้คลาสอื่น ๆ นอกจากคลาสใน package เดียวกันเข้าถึงได้ครับ

หรือในกรณีที่แย่ที่สุดก็ comment ไปเลยว่า user ไม่ควรใช้ method นี้ตรง ๆ นะ (ถ้าใช้แล้วเกิดอะไรขึ้นไม่รับผิดชอบ) ก็ยังได้ครับผม

ปล. โค๊ดที่เขียนมานี่เขียนสดหมดเลย ไม่ได้เทสต์แม้แต่บรรทัดเดียว Exception ก็ไม่จับ ... ดังนั้นเอาไปใช้จริงไม่ได้นะครับ เอาไว้ดูเป็นไอเดียละกัน

Wutipong Wongsakuldej

Programmer, interested in frontend applications, music and multimedia.

Latest posts by Wutipong Wongsakuldej (see all)

Leave a Reply

Your email address will not be published. Required fields are marked *