Angular Testing part 5
เริ่มที่ง่ายก่อน แม้จะดูไม่จำเป็น คือการเขียนความต้องการให้กับ model ของเรื่อง นั่นก็คือ contact model
Contact Model
เรียกอีกอย่างว่า business หรือ domain model ก็ได้ กล่าวคือเป็นหัวใจหลักของเรื่องที่ขาดไม่ได้ เพราะหากขาดเรื่องก็จะไม่เกิดขึ้น
Contact ในที่นี้ก็คือข้อมูลบุคคลติดต่อ ประกอบด้วย ชื่อ (name) เบอร์โทรศัพท์ (phone number) อีเมล (email) และเบอร์โทรศัทพ์เคลื่อนที่ (mobile number)
พื้นฐานต้องพร้อม
หากเป็นมือใหม่หลงเข้ามาและยังไม่รู้ว่าจะเริ่มต้นอย่างไร แนะนำให้อ่าน part 4 ด้านล่างนี้ก่อน แล้วจึงมาเริ่มทำด้วยกันนะครับ
จาก src สร้าง folder ชื่อ model และภายใน model folder นั้นสร้างไฟล์ชื่อ contact.ts ภายในไฟล์นี้เขียนชื่อคลาส Contact เริ่มต้นไว้ดังนี้
src/model/contact.ts
export class Contact {}
ใน model นี้เราจะสร้างไฟล์สำหรับ Test ขึ้นมา ชื่อ contact.spec.ts แล้วเขียน import เริ่มต้นไว้ดังนี้
src/model/contact.spec.ts
import { Contact } from './contact'
แล้วลองรัน (ถ้าไม่ได้รันไว้)
ng test
จะต้องไม่ error ใดๆ
และหากว่ารัน code coverage ล่ะก็
ng test — watch=false — code-coverage
ก็จะเห็นว่ายังไม่มีอะไรเลย
ขอแนะนำ 2 ฟังก์ชัน ก่อนไปต่อ คือ beforeEach กับ afterEach
beforeEach
ทำก่อนเข้าฟังก์ชัน it ทุกครั้ง
afterEach
ทำหลังออกฟังก์ชัน it ทุกครั้ง
ประโยชน์ของ 2 ฟังก์ชันนี้คือ reset ค่าของตัวแปรที่เราสนใจหรือเคลียร์ค่ามันเพื่อการจัดการหน่วยความจำ
เรื่อง Contact class tests
ประกาศตัวแปรชื่อ contact กำหนดค่าเป็น null
หลังจากนั้นใช้ beforeEach กำหนดค่าเป็น Contact object ทุกครั้งก่อนทำ it
และใช้ afterEach เคลียร์ค่าเป็น null ทุกครั้งหลังทำ it
มีผลให้ก่อนทำแต่ละ it หรือ spec จะได้ค่าของ Contact object ที่สะอาดเสมอ
ให้สังเกตผลลัพธ์ของ ng test และ code coverage นะครับ
คาดหวังให้เรียก constructor ได้
เรียกได้อยู่แล้ว เพราะคลาสมี default constructor
อย่าลืมสังเกตผลลัพธ์ของ ng test และ code coverage
คาดหวังให้สามารถกำหนดชื่อ (name) ผ่าน name property ได้
Property ‘name’ does not exist on type ‘Contact’.
อันนี้จะเห็นว่า TypeScript ฟ้อง error เพราะ name ไม่มีอยู่จริงที่ Contact ดังนั้นจะต้องกลับไปเพิ่ม name ที่ Contact เสียก่อน
export class Contact { name: string}
เพื่อนๆจะเห็นว่าจาก spec เราได้กำหนดค่า ‘John’ ให้กับตัวแปร name ของ contact หลังจากนั้นเราคาดหวังว่ามันจะยังคงมีค่าเท่ากับ ‘John’ ดั่งเดิม (ไม่ใช่ค่าหายไปหรือมีการแก้ไข) เราจึงระบุว่า contact.name ต้องมีค่าเท่ากับ ‘John’ ใน toEqual นั่นเอง
คาดหวังให้สามารถกำหนดชื่อ (name) ผ่าน constructor ได้
Expected 0 arguments, but got 1.
contact = new Contact('John') // พังเพราะไม่ได้กำหนดให้ทำได้
TypeScript บอกว่าทำไม่ได้ เนื่องจาก constructor ของ Contact เดิมไม่ได้กำหนดไว้ (กำหนดไว้ 0 ตัว) เราจำต้องไปเขียนให้มันสามารถกำหนดได้
export class Contact { name: string constructor(name: string) { }}
พอทำอย่างนี้ปุ๊บ ตรงนั้นก็พังอีก กลายเป็นว่า
contact = new Contact('John') // ทำได้แล้ว
contact = new Contact() // กลับทำไม่ได้
แบบนี้ Test ไม่ผ่านแน่ๆ เราต้องให้ constructor สามารถรับค่า name ได้ซึ่งยังคงสมบัติเดิมคือเรียก constructor เฉยๆได้ด้วย งั้นให้ name นี้เป็น option (ทางเลือกทำก็ได้หรือไม่ทำก็ได้) กลับไปแก้ไข Contact เพิ่มเครื่องหมาย ? หลังชื่อพารามิเตอร์ name
export class Contact { name: string constructor(name?: string) { }}
เย้ TypeScript ผ่านแล้ว ทว่า spec ที่คาดหวังพังอีก
spec ที่คาดหวัง should set name through constructor พังเพราะค่า name ที่ใส่ลงไป (ค่า ‘John’) ผ่าน constructor ดันไปเทียบกับ undefined แสดงว่า name property ของคลาส Contact ไม่ได้เก็บค่านี้ไว้ (ไม่มีการส่งค่า ‘John’ ไปเก็บไว้ที่ name ของคลาส) แก้ไขโดยเพิ่มบรรทัดนี้
export class Contact { name: string constructor(name?: string) { this.name = name }}
สำเร็จสิครับแบบนี้
ดู code coverage (rerun ใหม่นะ)
ยังขาด phone number, email และ mobile number
มาเขียน spec กันต่อเลย พื้นฐานคือ 1 property ต่อ 1 unit (spec) เป็นอย่างน้อย ต้องไม่ใจร้อน ค่อยๆระบุความคาดหวังใส่ไปเรื่อยๆ แล้วรันให้ Test พัง จากนั้นค่อยไปจัดการโค้ด (ในที่นี้คือ contact.ts)
มาจัดการโค้ดกัน
‘phoneNumber’ does not exist on type ‘Contact’.
export class Contact { name: string phoneNumber: string constructor(name?: string) { this.name = name }}
‘email’ does not exist on type ‘Contact’.
export class Contact { name: string phoneNumber: string email: string constructor(name?: string) { this.name = name }}
‘mobileNumber’ does not exist on type ‘Contact’.
export class Contact { name: string phoneNumber: string email: string mobileNumber: string constructor(name?: string) { this.name = name }}
เรียบร้อย
ไม่ว่า spec ไหนจะถูกทดสอบก่อน ผลลัพธ์ต้องไม่พัง เพราะ beforeEach จัดการ reset ค่าของ contact object ให้แล้วทุกครั้งไป
เพิ่มเติม
TDD บอกว่าเขียน Test แล้วให้ Test สร้าง Code เมื่อได้ Code แล้วหากต้องการ Refactor Code ก็สามารถทำได้ (มี Test คอยการันตีอยู่จึง refactor ได้อย่างสบายใจ)
ทว่าตอนนี้เราไม่มีอะไรต้องปรับปรุง (refactor)
เป็นว่า part นี้จะขอเล่าเรื่องการสร้าง contact model ไว้เพียงเท่านี้ เพื่อนๆก็คงจะพอทำกันได้ไม่ยากนัก part ต่อไปจะเล่าเรื่องการทดสอบ component ของ Angular ครับ