เมื่อวันเสาร์ที่ 27 ก.พ. 2016 ที่ผ่านมา ได้มีโอกาสเข้าร่วมงาน Code Mania ครั้งที่ 11 ซึ่งจัดโดยสมาคมโปรแกรมเมอร์ไทย
ทั้งนี้ต้องขอบคุณตั๋วฟรีจากทางสมาคม และน้อง Max (Issaret Max Prachitmutita) ที่เป็นธุระให้อย่างดี ขอขอบคุณมา ณ โอกาสนี้
เนื่องจากผมติดภาระกิจตอนเช้าจึงพลาด session ช่องแรกไป จึงขอเขียนบทความเฉพาะที่ผมคิดว่่าสามารถทอดผู้อ่านได้ให้รับความเข้าใจ และตนเองก็มีความมั่นใจที่จะถ่ายทอด
สำหรับผู้อ่านที่สนใจ session เช้า และสรุป session ต่างๆ ผมขอแนะนำบทความนี้เลยครับ
Code Mania 11: Raise the Bar ของคุณ Kan Ouivirach
โดยผมขอเริ่มบทความจาก session test double
##Test Double Session
ต้องขอขอบคุณวิทยากรสำหรับ session นี้คือคุณเก๋ Nattanicha Phatharamalai ที่ไขความกระจางเกี่ยวกับ test double
ภาพบรรยากาศในห้องบรรยาย
ก่อนถึงคำว่า test double ผมขออธิบายถึงหลักการง่ายๆ ของ unit test ก่อนนะครับ
unit test คือการเขียนคำสั่ง หรือ code ไปทดสอบการทำงานของ code หลักที่เราสร้างขึ้น หรือ production code นั่งเอง ว่าทำงานถูกต้องตามที่เราต้องการ
ลักษณะสำคัญของ unit test คือ
แต่แน่นอนว่าใน code ของเรา ย่อมมีส่วนที่ทำงานได้ช้ากว่าส่วนอื่นๆ เช่น access database, web service, sending email
ดังนั้น การสร้าง unit test ที่ถูกต้อง เราจึงต้องสร้างตัวแทน object จริงที่ทำงานได้ช้าหรือเราไม่ได้สนใจที่จะทดสอบผลลัพธ์ แต่มีความจำเป็นเพื่อทำให้เราสร้าง unit test ได้ ด้วยวิธการที่เรียกว่า test double
กล่าวคือ test double หมายถึง การสร้างตัวแทนของ object จริง เพื่อใช้งานใน unit test มีที่มาจากคำว่า stunt double ที่หมายถึงตัวแสดงแทนในภาพยนต์
test double เป็นเพียง technical term แต่เมื่อพูดถึงการนำไปใช้งานจริง การสร้างตัวแทนของ object ก็แบ่งแยกย่อยออกไปได้อีก ดังนี้
ดังนั้นเวลาจะที่อ้างถึง dummy, stub, mock, spy และ fake แบบรวมๆ ไม่เจาะจงก็เรียกว่า test double จริงๆ แล้ว test double มักถูกเรียกอีกชื่อว่า mock ถึงถือว่าไม่ผิด แต่ไม่เป็นทางการมากกว่า เพราะจริงๆ แล้ว mock เป็นเพียงวิธีการหนึ่งของ test double
###dummy สร้างขึ้นมาเพื่อช่วยในการ set up test แต่ไม่ได้สนใจ ใส่ใจการทำงานของ dummy เลย เช่นการสร้าง dummy object เพื่อใช้เป็น argument ของ method หรือ constructor
ใช้เมื่อต้องการทำให้ object ที่ถูกแทนที่ทำงานบางอย่าง เพื่อให้เราสามารถทำการทดสอบส่วนอื่นๆ ที่สนใจได้
เป็น stub ที่เพิ่มความสามารถบางอย่าง เช่น มีตัวแปรเก็บว่า method ถูกเรียกใช้ไปแล้วหรือไม่
สนใจพฤติกรรมของ method ว่ามีการ call หรื่อไม่ กี่ครั้ง หรือทดสอบการรับค่า argument ของ method
สร้าง object ใหม่ที่มีส่วน business behavior อยู่ด้วย มีส่วนการทำงานจริง
เพื่อความเข้าใจ ผมได้นำแนวคิดจาก session ของคุณเก๋ และบทความของพี่ปุ๋ยใช้เป็นแนวทาง เพื่อสร้างระบบ NotificationService ง่ายๆ เป็นรูปแบบการทำงานร่วมกันของ object ต่างๆ เพื่อทำการส่ง email ไปแจ้งผู้ใช้ ตัว code หลักเขียนด้วย Java แต่ unit test เขียนด้วย Groovy และ Spock ครับ หากใครสนใจรายละเอียดเบื้องต้นเกี่ยวกับ Spock สามารถไปอ่านเพิ่มเติมตามนี้ได้เลย
เลิก manual test แล้วมาเขียน unit test Java Project ด้วย Spock กันเถอะ
เรามาดู class หลักของระบบ NotificationService กันดีกว่าครับ
NotificationService.java
package com.codesanook.example;
public class NotificationService {
private EmailClient emailClient;
private NotificationInputValidator validator;
public NotificationService(EmailClient emailClient,
NotificationInputValidator validator) {
this.emailClient = emailClient;
this.validator = validator;
}
public String removeHtmlTag(String input) {
return input.replaceAll("<[^>]*>", "");
}
public Email composeEmail(String from, String to, String subject, String body) {
if (!validator.validateEmailInput(from, to, subject, body)) {
throw new IllegalStateException("invalid email input");
}
Email email = new Email();
email.setFrom(from);
email.setTo(to);
email.setSubject(subject);
email.setBody(body);
return email;
}
public boolean notifyByEmail(String from, String to, String subject, String body) {
subject = removeHtmlTag(subject);
body = removeHtmlTag(body);
Email email = composeEmail(from, to, subject, body);
return emailClient.sendEmail(email);
}
}
อธิบาย
ต่อไปเราก็จะมาทำการ test กันเพื่อความเข้าใจ test double ในแต่ละแบบ
สร้างขึ้นมาเพื่อช่วยในการ set up test แต่ไม่ได้สนใจ ใส่ใจการทำงานของ dummy เลย เช่นการสร้าง dummy object เพื่อใช้เป็น argument ของ method หรือ constructor
ถ้าเราจะ test removeHtmlTag method สังเกตว่า method นี้ ไม่ได้ต้องการใช้ EmailClient หรือ NotificationInputValidator object เลย แต่การสร้าง NotificationService object จำเป็นต้องใช้ EmailClient และ NotificationInputValidator ส่งเป็น constructor ดังนั้น เมื่อเกิดกรณีเช่นนี้ เราก็สร้าง EmailClient และ NotificationInputValidator เป็น dummy object ได้เลย
NotificationServiceRemoveHtmlTagSpec.groovy
package com.codesanook.example.test
import com.codesanook.example.Email
import com.codesanook.example.EmailClient
import com.codesanook.example.NotificationInputValidator
import com.codesanook.example.NotificationService
import spock.lang.Specification
class NotificationServiceRemoveHtmlTagSpec extends Specification {
def "HTML string should be remove"() {
given:
def emailClient = new DummyEmailClient()
def inputValidator = new DummyInputValidator()
NotificationService notificationService =
new NotificationService(emailClient, inputValidator)
def htmlString = "<h1>hello</h1> <p>world</p>"
when:
def removedHtmlTag = notificationService.removeHtmlTag(htmlString)
then:
removedHtmlTag == "hello world"
}
}
class DummyInputValidator extends NotificationInputValidator {
@Override
public boolean validateEmailInput(String from, String to, String subject, String body) {
throw new IllegalStateException("should not be called");
}
}
class DummyEmailClient implements EmailClient {
@Override
boolean sendEmail(Email email) {
throw new IllegalStateException("should not be called");
}
}
อธิบาย
ใช้เมื่อต้องการทำให้ object ที่ถูกแทนที่ทำงานบางอย่าง เพื่อให้เราสามารถทำการทดสอบส่วนอื่นๆ ที่สนใจได้
NotificationServiceComposeEmailSpec.groovy
package com.codesanook.example.test
import com.codesanook.example.Email
import com.codesanook.example.EmailClient
import com.codesanook.example.NotificationInputValidator
import com.codesanook.example.NotificationService
import spock.lang.Specification
class NotificationServiceComposeEmailSpec extends Specification {
def "valid email input, valid email object should return"() {
def emailClient = new DummyEmailClient()
NotificationInputValidator inputValidator = Stub();
inputValidator.validateEmailInput(_, _, _, _) >> true;
NotificationService notificationService =
new NotificationService(emailClient, inputValidator)
def from = "[email protected]"
def to = "[email protected]"
def subject = "Hello"
def body = "Hello World"
Email email = notificationService.composeEmail(from, to, subject, body)
expect:
email.from == from
email.to == to
email.subject == subject
email.body == body
}
อธิบาย
เป็น stub ที่เพิ่มความสามารถบางอย่าง เช่น มีตัวแปรเก็บว่า method ถูกเรียกใช้ไปแล้วหรือไม่
NotificationServiceComposeEmailSpec.groovy
package com.codesanook.example.test
import com.codesanook.example.Email
import com.codesanook.example.EmailClient
import com.codesanook.example.NotificationInputValidator
import com.codesanook.example.NotificationService
import spock.lang.Specification
class NotificationServiceComposeEmailSpec extends Specification {
def "validateEmailInput called once"() {
given:
EmailClient emailClient = new DummyEmailClient()
NotificationInputValidator inputValidator = Spy()
NotificationService notificationService = new NotificationService(emailClient, inputValidator)
def from = "[email protected]"
def to = "[email protected]"
def subject = "Hello"
def body = "Hello World"
when:
notificationService.composeEmail(from, to, subject, body)
then:
1 * inputValidator.validateEmailInput(_, _, _, _)
}
}
อธิบาย
สนใจพฤติกรรมของ method ว่ามีการ call หรื่อไม่ กี่ครั้ง ทดสอบการรับ parameter ของ method
NotificationServiceSendEmailSpec.groovy
package com.codesanook.example.test
import com.codesanook.example.Email
import com.codesanook.example.EmailClient
import com.codesanook.example.NotificationInputValidator
import com.codesanook.example.NotificationService
import spock.lang.Specification
class NotificationServiceSendEmailSpec extends Specification {
def "valid email input, sendEmail called once"() {
given:
EmailClient emailClient = Mock()
NotificationInputValidator inputValidator = Stub()
inputValidator.validateEmailInput(_, _, _, _) >> true
NotificationService notificationService =
new NotificationService(emailClient, inputValidator)
def from = "[email protected]"
def to = "[email protected]"
def subject = "Hello"
def body = "Hello World"
when:
notificationService.notifyByEmail(from, to, subject, body)
then:
1 * emailClient.sendEmail({ Email email ->
email.from == from
email.to == to
email.subject == subject
email.body == body
})
}
}
สร้าง object ใหม่ที่มีส่วน business behavior ด้วย มีส่วนการทำงานจริง
NotificationServiceSendEmailSpec.groovy
package com.codesanook.example.test
import com.codesanook.example.Email
import com.codesanook.example.EmailClient
import com.codesanook.example.NotificationInputValidator
import com.codesanook.example.NotificationService
import spock.lang.Specification
class NotificationServiceSendEmailSpec extends Specification {
def "valid email, send email return true"() {
given:
EmailClient emailClient = new FakeEmailClient();
NotificationInputValidator inputValidator = Stub()
inputValidator.validateEmailInput(_, _, _, _) >> true
NotificationService notificationService =
new NotificationService(emailClient, inputValidator)
def from = "[email protected]"
def to = "[email protected]"
def subject = "Hello"
def body = "Hello World"
when:
def sentResult = notificationService.notifyByEmail(from, to, subject, body)
then:
sentResult == true
}
class FakeEmailClient implements EmailClient {
@Override
boolean sendEmail(Email email) {
println "sent email from ${email.from} to ${email.to}\n" +
"subject ${email.subject} body ${email.body}";
return true;
}
}
}
อธิบาย
หวังว่าผู้อ่าน จะได้เข้าใจความหมายและเห็นวิธีการต่างๆ ของ test double มากขึ้นนะครับ
ถ้าใครมีคำถามข้อสงสัยใดๆ เขียน comment มาได้เลย
สุดท้ายต้องขอขอบคุณวิทยากรคุณเก๋ และพี่ปุ๋ยสำหรับข้อมูลที่นำมาประกอบเป็นบทความนี้ครับ