เกริ่น นำรำวงมานานหลายตอนมาถึงตอนนี้เป็นตอนที่จะได้เห็นโค้ดจริงๆสักที โดยอ้างถึงตอนที่แล้วเราตั้งใจกันว่าจะเขียน สิ่งที่เรียกว่า template engine ดังนั้นเพื่อให้สอดคล้องกับแนวคิดเรื่อง TDD นั้นเราจะค่อยๆทำไปทีละนิดเราจะไม่ทำที เดียวหมด ดังนั้นสิ่งแรกที่เราจะทำคือการเน้นไปในเรื่องของ business logic ของตัว template emgine ก่อนโดยที่ให้ตัด ส่วนอื่นๆทิ้งไปก่อนแล้ว template engine คืออะไร
“The template engine we’re talking about needs to be capable of reading in a template,which is basically static text with an arbitrary number of variable placeholders mixed in. The variables are marked up using a specific syntax; before the template engine is asked to render the template, it needs to be given the values for the named variables.(ขี้เกียจแปล)”
น่าจะเคลียร์นะครับ ดังนั้นสิ่งที่เราต้องทำคือเปลี่ยนมันให้เป็นชุดของ test และหลังจากนั้นเลือกมาหนึ่งอันเพื่อ implement
2.2.1 Creating a list of tests
อย่างไรก็ตามก่อนที่เราจะมี list ของ test ได้เราต้องมี requirement ก่อนและไอ้ข้างล่างคือ requirement ที่เรามี
■ ระบบจะต้องแทนค่าตัวแปรต่างๆใน template เช่น ${firstname} และ ${lastname} ด้วยค่าที่เราต้องการ ณ จังหวะ runtime
■ ถ้าระบบพยายามส่ง email ออกไปโดยที่ยังมีตัวแปรบางตัวไม่ถูกแทนค่า จะต้องเกิด error ในบัดดล
■ ระบบจะไม่สนใจตัวแปรถูกส่งมาแต่ไม่สามารถหาได้ใน template file
■ ระบบรองรับ Latin-1 character set
■ ระบบรองรับ Latin-1 character set สำหรับการตั้งชื่อตัวแปร
■ และอื่นๆ
ไอ้ ที่อยู่ข้างบนนั่นเป็น requirement ครับไม่ใช่ test นะเพราะ test จริงๆหน้าตาไม่ใช่แบบนี้หน้าที่เราคือเปลี่ยน requirement ให้เป็น test ซึ่งเราสามารถเขียนออกมาได้แบบนี้ (test ต้องจับต้องได้)
■ ถ้าเรามี tempalte แบบนี้ “Hello, ${name}” แล้วเราแทนค่า ${name} ด้วย “Reader” เราจะได้ stringที่มีค่าเป็น “Hello, Reader”.
■ ถ้าเรามี template แบบนี้ “${greeting}, ${name}” wแล้วเราแทนค่า “Hi” และ “Reader”, ผลที่ได้ควรจะเป็น “Hi, Reader”.
■ ถ้าเรามี template แบบนี้ “Hello, ${name}” แต่ไม่มีการแทนค่า “name” จะส่งผลให้เกิด MissingValueError.
■ ถ้าเรามี template แบบนี้ “Hello, ${name}” แล้วเราแทนค่า “Hi” และ “Reader” ให้กับตัวแปรที่ชื่อ “doesnotexist” และ “name”, ผลที่จะได้คือ “Hello, Reader”.
■ และอื่นๆ
เห็น ความต่างไหมครัฟ ถ้าไม่เห็นก็ไปนอนก่อนนะครับเพราะมันโคตรจะต่างกันเลย เพราะ test จะเป็นสิ่งที่จับต้องได้มากๆ ภาษาที่ใช้ก็ชัดเจนจนเราไม่สงสัยว่าอะไรคือ “raise en error” เพราะมันคือการ throw exception นั่นเองและเราก็รู้ด้วยว่า message อะไรที่เราจะใส่ลงไปใน exception นั้นๆ ซึ่งจาก test เราจะใช้ “MessingValueError” แต่ไม่เจาะจงรายละเอียดของ message ดังนั้นด้วย test list นี้เรารู้ว่าเราต้องการอะไร ดังนั้นส่วนต่อไปคือเราจะต้องเลือกว่าอะไรคือสิ่งแรกที่เราจะเลือกทำ
2.2.2 Writing the first failing test
มาดูตัวอย่างกันโดยเราจะเลือก test ที่ง่ายที่สุดมาทำก่อนเพื่อความมั่นใจ
ถ้าเรามี tempalte แบบนี้ “Hello, ${name}” แล้วเราแทนค่า ${name} ด้วย “Reader” เราจะได้ stringที่มีค่าเป็น “Hello, Reader”.
และเพราะเราต้องการทำงานแบบ test first ดังนั้นสิ่งแรกที่เราจะเขียนคือ test ที่มีแค่โครงก่อนหน้าตาแบบนี้
Listing 2.1 Creating a skeleton for our tests
public class TestTemplate {
}
ต่อไปเราจะใส่ method ที่ทำหน้าที่แทนค่าหนึ่งค่า
Listing 2.2 Adding a test method
import org.junit.Test;
public class TestTemplate {
@Test
public void oneVariable() throws Exception {
}
}
ต่อ ไปเป็นจุดเริ่มต้นที่สำคัญมากๆ เราต้องย้อนกลับไปคิดเรื่อง “Programming by Intention” คือเราต้อง คิดก่อนว่าสิ่งที่เรากำลังเขียนลงไปใน test นี้นั้นเป็นโค้ดที่จะถูกใช้งานจริง ดังนั้นเราต้องคิดและเขียนออกมาให้สวยงาม ง่ายต่อการใช้งาน หลังจากระลึกเรื่อง “Programming by Intention” แล้วเราก็มาลงมือเขียนกัน
Listing 2.3 Writing the actual test
import org.junit.Test;
import static org.junit.Assert.*;
public class TestTemplate {
@Test
public void oneVariable() throws Exception {
Template template = new Template("Hello, ${name}");
template.set("name", "Reader");
assertEquals("Hello, Reader", template.evaluate());
}
}
เรา สร้าง Template คลาสก่อนเลยและใส่พารามิเตอร์เข้าไปเป็น String ที่ระบุรูปแบบของ Template จากนั้นเราเลือกที่จะใช้ method ชื่อ set เพื่อกำหนดค่า “Reader” ให้กับ “name” หลังจากนั้นเราก็ assert ค่าที่ได้จาก Template คลาสว่าตรงกับสิ่งที่เราต้องการหรือไม่
อย่า เพิ่งไปไกลกว่านี้นะครับ หยุดก่อนแล้วดูว่าไอ้ที่เราเพิ่งเขียนไปนั้นตรงกับที่เราคิดไว้ไหม การใช้งานคลาส Template เป็นไปตามที่เราคาดหวังไหมนี่คือ “Programming by Intention” อยากให้สิ่งที่เราต้องการสร้างถูกใช้งาน อย่างไรจงเขียนไว้ใน test
มา ถึงตรงนี้เราได้ test มาแล้วแต่เราจะเห็นว่า compiler จะบ่นเราว่าไม่มีคลาสชื่อ Template นะ ดังนั้นสิ่งแรกที่เราต้อง ทำคือเพิงมันเข้าไป จากนั้น error ยังไม่หายไปเพราะยังเหลืออีกสองสิ่งที่เรายังไม่ได้ทำเพิ่มนั่นก็คือ set(String, String ) และ evaluate() ดังนั้นเพื่อไม่ให้เป็นการเสียเวลาเราก็เพิ่มเข้าไปใน code ของเราเลย
Listing 2.4 Satisfying the compiler by adding empty methods and constructors
public class Template {
public Template(String templateText) {
}
public void set(String variable, String value) {
}
public String evaluate() {
return null;
}
}
สุดท้ายเมื่อทุกอย่างเข้าที่หน้าที่ของเราคือการ run test 
Running the test
run test ครั้งแรกแน่นอนว่า fail แน่นอนไม่ต้องเดาเพราะเรายังไม่ได้ใส่กลไกอะไรลงไป method เลยแม้แต่นิดเดียว เพราะเราจะค่อยๆทำไป หัวข้อก่อนหน้านี้เราทำแค่ให้มันไม่ แดง บน IDE ไปก่อน นี่เป็นขั้นตอนแรกที่มักทำกันเสมอเพื่อการก้าวเดินทีละขั้น ขั้นเล็กๆ ดังนั้นเมือเรา rub test ครั้งแรกมันต้อง fail ดังนั้นต่อไปเราจะเริ่มใส่กลไกให้มันทีละนิด
A failing test is progress
มา ถึงตรงนี้บางคนยังสงสัยว่านี่เราอำอะไรลงไป เรายังไม่ได้เขียนโค้ดส่วนที่เป็นกลไกการทำงานจริงๆที่เราต้องการจะ test เลย อย่าเพิ่งสงสัยอ่านตรงนี้ก่อนสิ่ง ที่เรามีในมือตอนนี้คือ test ที่สามารถบอกเราได้อย่างชัดเจนว่าเมื่อไหร่สิ่งที่เราทำจะสามารถเรียกได้ว่า เสร็จโดยหลัง จากนี้ไม่ช้าก็เร็ว แต่ไอ้ test ที่ว่ามันไม่ได้บอกเราเป็น % ว่า “ตอนนี้คุณสำเร็จไป 90%” หรือ “อีก 5 นาทีจะเสร็จ” อะไรเทือกๆนี้ไม่มีนะใน test แต่สิ่งที่เราจะรู้ได้คือเมื่อ test ทั้งหมด run แล้ว “ผ่าน” ดังนั้นเรารู้อยู่แล้วว่าเรายังไม่ได้สร้าง ใส่รายละเอียดลงไปใน Template คลาสอีกสองอย่างดังนั้นเราจะค่อยๆปรับ Template คลาสไปเรื่อยๆจนเข้าที่การ run test ในตัวอย่างที่ 2.2 ทำให้เรารู้ว่าเราได้ว่า null มาซึ่งมันไม่ตรงกับสิ่งที่เราคาดหวังไว้เำพราะมันควรจะได้ “Hello, Reader” มาแทนดังนั้นตอนนี้เราอยู่ในสถานะ fail
2.2.3 Making the first test pass
ก้าวต่อไปที่เราจะทำคือการทำให้ test ผ่านให้เร็วที่สุดเท่าที่จะทำได้ก่อน ดังนั้นตอนนี้เรามีโครงของ Template คลาสที่มีลักษณะแบบนี้
public class Template {
public Template(String templateText) {
}
public void set(String variable, String value) {
}
public String evaluate() {
return null;
}
}
ดัง นั้นขั้นตอนนี้เป็นขั้นตอนการตัดสินใจที่สำคัญมาก คำถามคือเราจะผ่าน test นี้ไปได้อย่างไรให้เร็วที่สุด? เพราะถ้าเราต้องมานั่งเขียนโค้ดเพื่อทำ String Replacement สำหรับ “Hello, ${name}” คงจะใช้เวลาหลายนาทีอยู่ แต่เราต้องการเร็วกว่านั้นนี่ ดังนั้นทางออกมีไม่มาก ไม่มากจริง และอย่าเพิ่งตกใจ ทางออกคือแบบนี้ครับ
Listing 2.6 Passing as quickly as possible with a hard-coded return statement
public class Template {
public Template(String templateText) {
}
public void set(String variable, String value) {
}
public String evaluate() {
return "Hello, Reader";
}
}
ทางออก มันดู เกรียน มากแต่นี่เป็นไปตาม principle ครับคือทำให้ผ่านก่อนเพราะเราเขียน test แค่นี้แสดงว่าโค้ดที่ได้มา แค่นี้ก็ถูกต้องแล้ว อย่าคิดไปไกลแต่ไอ้แค่นี้มันก็ยังไม่ถูกนะครับ ดังนั้นเราต้องเขยิบไปข้างหน้าอีกนิดและวิธีการเขยิบก็ คือการสร้าง test อีกอันนั่นเอง
2.2.4 Writing another test
ตอน นี้เราติดอยู่ตรงที่เรา hardcode ค่าที่ได้จาก evaluate method ซึ่งมันผิดชัดๆแต่ถ้าวัดด้วย unit test แล้วมันถูกดังนั้นการที่เราจะแก้ไขเรื่องนี้ได้ก็คือเราต้องสร้าง test อีกตัวขึ้นมา
public class TestTemplate {
@Test
public void oneVariable() throws Exception {
Template template = new Template("Hello, ${name}");
template.set("name", "Reader");
assertEquals("Hello, Reader", template.evaluate());
}
@Test
public void differentValue() throws Exception {
Template template = new Template("Hello, ${name}");
template.set("name", "someone else");
assertEquals("Hello, someone else", template.evaluate());
}
}
test ที่สองที่เราใส่เข้าไปเป็น test ปราบมาร เพราะมันจะตรวจว่า put(String, String) และ evaluate() ทำงานได้ถูกต้อง หรือไม่เพราะเราใส่ตัวแปรอื่นลงไปค่าที่ได้ต้องแปรเปลี่ยนตามไปด้วย ดังนั้น hardcode ที่เราเพิ่งใส่ลงไปไม่รอดแล้วต้อง โดนระเบิดไปเพราะมันจะไม่มีทางผ่าน test ที่สองได้
การ เขียน test ในลักษณะนี้มีชื่อเรียกนะครับ เท่ด้วย เราเรียกมันว่า Triangulation (Triangulation เป็นศัพท์ที่มาจากวิชาการสำรวจทางภูมิศาสตร์ (Physical Survey) และการนำร่องในการเดินเรือหรือขับเครื่องบิน (Navigation) โดยการหาพิกัดตำแหน่งที่ต้องการโดยการวัดมุมระหว่างตำแหน่งที่อยู่กับจุด อ้างอิงที่ทราบพิกัดอย่างน้อย 2 จุด จุดตัดของมุมทั้ง 2 จะเป็นจุดพิกัดที่ทำให้เราทราบตำแหน่งที่อยู่ หากจุดอ้างอิงทั้ง 2 จุดไม่มีพิกัด ตำแหน่งที่จุดที่ต้องการจะไม่สามารถบอกจุดที่แน่นอนได้บอกได้เพียงความ สัมพันธ์ของตำแหน่งที่ต้องการกับจุดอ้างอิงทั้ง 2 เท่านั้น) เรามี test ที่สองขึ้นมาเพื่อช่วยกำหนดทิศทางที่ถูกต้องของ Tempate คลาสเรื่องการแทนที่ตัวแปร และการทำงานแบบนี้เราจะค่อยๆเดินไปข้างหน้าเราจะแก้ปัญหาเรื่อง over engineering ได้ไปแบอ้อมๆด้วยเช่นกัน เพราะเราไม่ได้เขียนโค้ดเผื่อ เราเขียนแค่พอดี แต่อย่างไรก็ตามการผ่าน test ที่สองนั้นเราก็ทำแค่พอดีเช่นกันดังนั้นเราจะได้ Template คลาสหน้าตาใหม่ออกมาแบบนี้
Listing 2.8 Making the second test pass by storing and returning the set value
public class Template {
private String variableValue;
public Template(String templateText) {
}
public void set(String variable, String value) {
this.variableValue = value;
}
public String evaluate() {
return "Hello, " + variableValue;
}
}
เรา ทำให้ test ผ่านไปได้อีกครั้งและการผ่านครั้งนี้เราก็ลงแรงน้อยๆเท่าที่จำเป็นอีกเมือ นเดิม แต่เราก็เห็นกับตาอยู่ว่ามัน ไม่ดีเพราะยังมี hardcode อยู่ดังนั้นเราต้องทำ triangulation ต่อไปเพื่อผลักให้เราเดินไปข้างหน้าอีกนิดโดยเราจะ ถอด test ที่ชื่อ differentValue ออกแล้วใส่ test ตัวใหม่ลงไปโดยมันจะต้องรับมือกับ template แบบต่างๆได้มากกว่าคำว่า Hello เราต้องส่งอะไรเข้ามาก็ได้ตามที่เราต้องการ
Listing 2.9 Applying triangulation for the static template text
public class TestTemplate {
@Test
public void oneVariable() throws Exception {
Template template = new Template("Hello, ${name}");
template.set("name", "Reader");
assertEquals("Hello, Reader", template.evaluate());
}
@Test
public void differentTemplate() throws Exception {
Template template = new Template("Hi, ${name}");
template.set("name", "someone else");
assertEquals("Hi, someone else", template.evaluate());
}
}
เมื่อ เรา run test อ่ะแน่นอนว่ามันต้อง red เพราะเรายังมี hardcode อยู่ในโค้ดดังนั้นตอนนี้เราต้องเขียน parsing text แล้ว ดังนั้นเราอาจต้องกระโดดไปคุยเรื่อง parsing logic ก่อนแล้วเราค่อยวนกลับมาเรื่อง test ต่อ ดังนั้นถึงตรงนี้เราคง ต้องคุยกันเรื่อง breadth และ depth กันแล้ว