Skip to content

Node.js | Test doubles ด้วย Sinon.js

การทำ 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 ได้เลยครับ เขียนไว้ละเอียดและมีตัวอย่างโค้ดให้ดูด้วย

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.