การทำ unit test คือการทดสอบโค้ดโดยไม่สน dependencies ภายนอกอย่างเช่น database CRUD, Web services, APIs เพราะ unit test คือการทดสอบแค่การทำงานของฟังก์ชันนั้นๆ ว่าเป็นไปตาม flow ทำงานได้ถูกต้องตาม logic หรือไม่ เพื่อเป็นการลดความยุ่งยากเราจึงต้องจำลองการทำงานของโค้ดส่วนที่มีการติดต่อกับภายนอก
“Test-doubles” คือการสร้าง “ตัวแทน” (เพราะงี้ถึงเรียกว่า double) ของการทำงานภายนอก เสมือนว่ามีการเรียกใช้งานส่วนนั้นจริงๆ เพื่อป้องการ side-effect ที่อาจจะเกิดขึ้น ส่วนผลพลอยได้คือการรันชุดทดสอบได้เร็วขึ้นเพราะไม่ต้องติดต่อออกไปภายนอก
สำหรับ Node.js ก็มี Sinon.js ที่ช่วยทำ test-doubles ใช้ร่วมกับ test framework อย่าง Mocha, Jasmine ได้ ความจริงมีหลายตัวเลือกใช้กันได้ตามสะดวก sinon.js นั้นติดตั้งผ่าน npm ได้เลย
npm install sinon --save-dev
ยกตัวอย่างผมคลาสชื่อ DB คลาสนี้สามารถเขียนและอ่านข้อมูลจาก database ได้
class DB { constructor(connection) { this.db = connection.db('db_name'); } // save data to DB save(data) { this.db.save(data); } // get data from DB get(id) { return this.db.get(id) } }
สำหรับใครที่เขียน unit test จะได้ยินคำว่า mock ซึ่งเป็นการเรียกแบบรวมๆ ถึงการจำลองการทำงานของโค้ดภายนอก การจำลองพฤติกรรมของ function แยกย่อยได้เป็น 3 แบบคือ Spy, Stub และ Mock แล้วมันต่างกันยังไง ตอนไหนเราจะใช้อะไรล่ะ?
Spy
เป็นการติดตามดูสถิติ ติดตามการทำงานของ function ภายใน function ที่เราต้องการทดสอบว่าถูกเรียกกี่ครั้ง ด้วย parameter อะไรบ้าง ตัวอย่างการใช้ spy ก็ตามนี้
const db = new DB(connection); const spy = sinon.spy(db, 'save'); // do something about save() method db.save({text: "Hello"}) // assert/expect library what you want expect(spy.calledOnce).to.equal(true)
โค้ดข้างบนคือการทดสอบว่า save() ถูกเรียกอย่างน้อย 1 ครั้ง
Stub
ในกรณีที่เราต้องการจำลองการคืนค่าของ method หรือ function ให้ใช้ stub แทน ยกตัวอย่างผมมีคลาส Document ที่เรียกใช้ get() ของคลาส DB
class Document { constructor (db) { this.db = db; } getData(id) { return this.db.get(id); } }
ผมไม่ต้องการต่อ database จริงๆ ก็เลย stub เพื่อจำลองผลลัพธ์ที่จะคืนกลับมาตามนี้
// fake db object db = {}; // stub the method db.get = sinon.stub(); // specify argument and result to return db.get.withArgs('abc_1').returns({id: 'abc_1', text: 'some text'}); const doc = new Document(db); // expect result expect(doc.getData('abc_1')).to.deep.equal({id: 'abc_1', text: 'some text'});
Mock
การใช้งาน mock จะใช้เมื่อเราต้องการการทำงานทั้ง spy และ stub คือต้องการทั้งสถิติและจำลองผลลัพธ์จาก dependency ภายนอก เราไม่สามารถ spy และ stub method พร้อมกันได้
การใช้งาน mock เป็นการ verify ว่า method นั้นทำงานตามที่เราคาดหวังหรือเปล่าถ้าเป็นไปตามที่คาดหวัง test ก็จะผ่าน ถ้าไม่เป็นไปตามที่เราคาดหวัง test นั้นจะ fail
const db = {}; // stub the method db.get = sinon.stub(); // specify argument and result to return db.get.withArgs('abc_1').returns({id: 'abc_1', text: 'some text'}); const doc = new Document(db); sinon.mock(doc) .expects('getData') .once() .withArgs('abc_1') .returns({id: 'abc_1', text: 'some text'}); doc.verify(); doc.restore();
ส่วนการใช้งาน Sinon แบบอื่นๆ สามารถดูตาม Sinon Document ได้เลยครับ เขียนไว้ละเอียดและมีตัวอย่างโค้ดให้ดูด้วย