all passing

เขียน unit Test ทำไง คิดยังไง

กลับมาพบกับบทความเกี่ยวกับ Test โปรแกรมอีกแล้วนะครับ ถ้าใครยังไม่เคยอ่านตอนแรกแนะนำให้ไปอ่านก่อนครับที่ TDD ไม่รู้จักหรอกเอาแค่เริ่มรู้จัก Test ก่อน บทความนี้เราจะมาทำความรู้จัก Unit test แบบโง่ๆก่อนว่าทไมต้องทำ แล้วทำยังไง ดียังไง ช่วยเหลืออะไรเราได้บ้าง ไม่ทำได้ไหม

Unit test คือไร

เขียน unit test ยังไง
all passing

เอาง่ายๆภาษาบ้านๆคือ การเขียนทดสอบ หน่วยที่เล็กที่สุดของโปรแกรม สมมติว่าเรามี function บวกค่าสองค่า ยังไม่มี function อื่นทำงานด้วย คือเรียกแล้วจบในตัวเองเลย อย่างนี้แหละคือ unit ส่วนเล็กสุดแล้วเราค่อยไป test พวก function ที่มันเรียกตัวนี้ต่อ ซึ่งเราค่อยไปเจอกันบทความหน้าสำหรับเรื่อง test double พวกที่เราต้อง mock, stub บ้าบอพวกนี้ ช่างแม่งก่อน เอาง่ายๆกันก่อน ถ้าง่ายไม่ได้ยากไม่ต้องพูดถึง

Unit test ทำไง

เกือบทุกภาษาน่าจะมีตัวทดสอบโปรแกรมให้เขียน Test อยู่แล้วเอาที่ผมรู้จักที่ผมเขียนก็มี PHPunit test https://phpunit.de/ ไม่สอนนะว่าติดตั้งยังไง เพราะแม่งเรื่องเยอะ ถ้าอยากให้สอนเขียน comment ไว้แล้วกัน ผู้เขียนต้องการกำลังใจ ( ฮา ) สำหรับตัวอย่างในบทความนี้จะเขียนด้วย Node.js เพราะอะไร ? แม่งง่ายดี !! และตัว Node.js มันเขียนเป็น function โง่ๆง่ายกว่าพวก PHP framework อ่ะ บทความนี้สอนไอเดียในการคิดและให้เห็นภาพคงไม่สอนระดับเข้าไปทำยังไง เรียกยังไง แต่ถ้าเขียน Node.js มาก็สบายหน่อย

โดยส่วนใหญ่ตัว Unit Test มันจะมีทุกภาษาอย่างที่บอกก็ลองหาดู และเมื่อติดตั้งแล้วส่วนใหญ่มันจะให้รันด้วย command line เพราะฉะนั้นก็ทำใจ ใครไม่ชอบสาย window cmd ก็ต้องเรื่องเยอะหน่อย สมมติจากโจทย์เดิมขอบทความแรกเรามี 4 function ใช่ไหมครับ ตอนนี้เราต้องการทดสอบ แค่ function เดียว คือ sanitizeInputReturnArray() จากตัวอย่างมันรับค่า 2 ค่าคือ username , password เพื่อ remove พวก tag หรือของบางอย่างที่เราไม่ต้องการออก และสุดท้ายมันจะ return Array ออกมา เอาแค่นี้ก่อน เราไป set up ตัวทดสอบก่อนมันชื่อ mocha

Set up mocha

command line สร้างมา ซัก Folder หนึ่ง ในตัวอย่างผมสร้าง folder ชื่อว่า ‘unittest’ เสร็จแล้วสั่ง ‘npm init’ แล้วก็ enter รัวๆ ไป

mkdir unittest 
cd unittest
npm init 
------ enter รัว --------
เสร็จลง mocha 
npm i mocha --save
npm i chai --save

npm i mocha –save คือการลงตัว lib สำหรับเขียน test โดยจริงๆเราจะไม่ได้ใช้ตัวเดียว เนี้ยแหละข้อเสีย open source ให้มานิดๆหน่อยๆ ส่วน –save คือให้มันเขียนลงในไฟล์ package.json ไปเลยเวลาเอาไปที่อื่นเราจะได้ไม่ต้องมานั่งเขียน อีกรอบเอาไปที่อื่นสั่ง npm i จบ

ส่วน chai คือ lib เป็นเหมือนส่วนเสริมทำให้ mocha เรามันเทพขึ้น เพื่อใช้เช็คค่าต่างๆแล้วอ่านง่ายขึ้นเฉยๆ ผมชอบใช้

เมื่อเรา install mocha เสร็จแล้วให้เราสร้าง ไฟล์ index.js เพื่อจะเขียน function sanitizeInputReturnArray ขึ้นมาเพื่อให้เราทดสอบ ซึ่งความเป็นจริงมันจะเป็นซักไฟล์หนึ่งใน project เรามันอาจจะอยู่ใน controller , model , what ever แต่เราต้องเรียกไฟล์นั้นมาเพื่อจะได้รันทดสอบไอ้ function ที่เราต้องการในตัวอย่างเราคือไฟล์ index.js หน้าตามันเป็นอย่างนี้

exports.sanitizeInputReturnArray = (username, password) => {
  $result = [];
  return $result;
};

คือเข้าใจว่าโลกความจริงพอเปิดไฟล์มาอาจจะต้องตกใจว่า เชี้ยไรวะเนี้ย แต่เอาเถอะอันนี้คือตัวอย่างว่ามันคงมีเขียนอะไรเยอะเยะในนั้น สิ่งที่เราจะทำคือค่อยๆใส่ค่าเพื่อให้มันออกมาก่อน เพราะตอนนี้เราตัดแค่เอา function มาทดสอบ คือในความจริงในไฟล์นี้มันอาจจะมีอีกหลายฟังก์ชั่น แต่เราต้องการ focus ที่อันนี้ก่อน ว่าค่าที่ส่งไปถูกต้อง และออกมาตรงกับที่เราคิดหรือเปล่า คราวนี้เราไปทำด้าน Test กันส่วนใน function นี้ถ้าสมมติคุณรู้ว่า business logic คืออะไรก็เขียนใหม่ก็ได้ เช่น รู้อยู่แล้วว่า function นี้มันทำ Data ที่โยนเข้ามาสะอาด ( หมายถึง remove tag หรือพวก sql injection หรือพวก javascript เกรียนๆ ) ก็เขียนใหม่เลยฝั่ง test ให้เราสร้าง folder ‘test’ และสร้าง ไฟล์ indexTest.js เพื่อให้เรารู้ว่าไฟล์นี้เอาไว้ทดสอบเกี่ยวกับ ไฟล์ต้นแบบอะไร ในไฟล์ indexTest.js มีหน้าตาประมาณนี้

const assert = require('assert');
const chai = require('chai');
let expect = chai.expect;
const login = require('../index');

describe('login', function() {
  describe('sanitizeInputReturnArray', function() {
    it('[SUCCESS] should return array 2 lenght', function() {
      result = login.sanitizeInputReturnArray('test', 'password');
      expect(result).to.have.lengthOf(2);
    });
  });
});

ตรงตัวแปร login ผมไม่ทราบว่าแต่ละคนจะเรียกอะไรมานะครับ แต่อันนี้ผมตั้งชื่อว่า login จะได้เรียกใช้งาน function เกี่ยวกับเรื่องนั้นๆได้ง่าย ให้สังเกตุตรง describe มันจะซ้อนกันเพื่อเวลาทำ test หลายๆตัวเราจะได้ตามถูกว่า test case ที่มัน fail มัน fail ที่ตรงไหนอย่างไร โอเคเรามาดู test case แรกเรากันคือตอนนี้ผมบอกว่า ผมจะทดสอบว่า function sanitizeInputReturnArray เราจะโยนสองค่าคือ ‘test’, ‘password’ และสุดท้ายของ โปรแกรมเราจะคืออะไรไม่รู้ออกมาแต่ต้องเป็น array 2 ตำแหน่ง โดยตัวแปร expect มันมีวิธีใช้เยอะมากลองไปดูได้ที่นี่ครับ http://chaijs.com/api/bdd/#method_lengthof ซึ่งภาษาอื่นก็มีทดสอบไม่ค่อยต่างกันเท่าไร

อันนี้คือ Test case แรกของเราไปดูกันว่าตอนเรา run test มันจะเป็นยังไงนะครับ

test mocha
test case

วิธีรัน test คือให้เราไปที่ root project ก่อนในตัวอย่างคือ unittest พิมพ์ว่า

./node_module/mocha/bin/mocha test/indexTest.js

ตัวหน้าคือการที่เราสั่งให้ mocha ทำงาน และค่าด้านหลังคือ ไฟล์ที่เราจะทำการทดสอบ จะเห็นว่าตอนนี้ function เรา Fail และมันจะบอกตามลำดับที่เรา describe ไว้ซึ่งใน login เราอาจจะมีหลาย function ทดสอบอีกจะได้อ่านได้ง่าย ที่มันเป็นสีแดงเพราะว่ามัน fail ผลของการทดสอบไม่ตรงกับสิ่งที่เราคาดหวังให้มันเป็น เวลาผมจะเขียนก็จะเขียนไว้เลยว่า Test case นี้ผมต้องการทดอบให้มันสำเร็จหรือให้มันล้มเหลว และถ้ามันสำเร็จมันต้องเป็นอะไร ซึ่งเราจะได้ทราบทันที่ว่าอ๋อ Test case นี้มันต้องคืน Array ออกมาสองตัว แต่อันนี้มันคืนมาเป็น array ว่าง สังเกตุด้านล่างต่อมาระบบมันจะบอกเราเลยครับว่า มี pass กี่อันและ fail กี่อัน และสาเหตุของการสิ่งที่มันทำไม่สำเร็จเพราะอะไร

วิธีการดูคือดูที่สาเหตุที่ระบบแจ้ง อย่างกรณีในตัวอย่างคือ

AssertionError: expected [] to have a length of 2 but got 0

คือในตัวการทดสอบนี้เราหวังว่ามันจะคือเป็น array ที่มีค่าอะไรก็ได้ 2 ตำแหน่งแต่มันคือมาเป็น array ว่าง ก็ใช่น่ะสิเพราะเราสร้าง function ให้ return array ว่างตั้งแต่แรก ส่วน

+expect -actual คือสิ่งที่มันคือกลับมาเป็นอะไร กับสิ่งที่เราอยากให้มันเป็นคืออะไร ในที่นี้คือ function เราคืน 0 มาและระบบต้องการ 2 ส่วน ‘-‘ หรือ ‘+’ ไม่มีอะไรแค่มันแยกให้ดูชัดๆเฉยๆว่าส่วนไหนคืออะไร โอเคคราวนี้เรากลับไปแก้ไขก่อนว่าให้มันคือ อะไรก็ได้มาสองอันหรือเราจะแก้ไขโดยให้มันคือ user,password มาเลยก็ได้แต่ผมจะใส่อะไรก็ได้ตามใจก่อนตามนี้

exports.sanitizeInputReturnArray = (username, password) => {
  $result = ['123', '456'];
  return $result;
};

คราวนี้เรากลับไปเทสโดยสลับไปหน้า command line แล้วสั่งเหมือนเดิมกดปุ่ม ลูกศรขึ้นแล้ว enter ก็ได้ครับ

test passing
test passing

ผลลัพธ์คือ เราทำ Test case แรกสำเสร็จแล้ว แต่ว่ามันยังไม่ครอบคลุมเพราะเรายังไม่รู้เลยว่าสิ่งที่คืนกลับมามันถูกต้องไหม เราแค่รู้ว่ามันคือมา array 2 ตำแหน่งเท่านั้นเอง เรามาดู test case ต่อไป เราจะ test ว่าถ้าเรา โยนค่า username กับ password ไป มันต้องกลับมาเป็นเหมือนตอนที่เราโดยนเข้าไปแต่เป็น array โดยสองตำแหน่งเราจะให้มันคือกลับมามีค่าเหมือนตอนแรกที่เราโยนเข้าไปตัวอย่างคือเราจะโยน username ว่า ‘oxygenyoyo’ และ password มีค่าเป็น ‘thisisabook’ Test case ที่เราจะเขียนจะเป็นดังนี้ครับ

const assert = require('assert');
const chai = require('chai');
let expect = chai.expect;
const login = require('../index');

describe('login', function() {
  describe('sanitizeInputReturnArray', function() {
    it('[SUCCESS] should return array 2 lenght', function() {
      result = login.sanitizeInputReturnArray('test', 'password');
      expect(result).to.have.lengthOf(2);
    });

    it('[SUCCESS] should return ["oxygenyoyo", "thisisabook"]', function() {
      result = login.sanitizeInputReturnArray('oxygenyoyo', 'thisisabook');
      expect(result[0]).to.equal('oxygenyoyo');
      expect(result[1]).to.equal('thisisabook');
    });
  });
});

และเมื่อเรารัน mocha test มันจะแสดงออกมาเป็นแบบนี้ครับ

2 test case mocha
2 test case mocha

เห็นไหมครับว่า test อันแรกเราผ่านแต่อันที่สองมันล้มเหลว ซึ่งผมให้มันคืนมาเป็น 123 แต่เราเช็คมันต้องเป็นแบบที่เราคาดหวังคือ ‘oxygenyoyo’ ซึ่งมันไม่ถูก เราก็กลับไปแก้ไขตัว code ให้มันส่งค่าที่ถูกต้องประมาณนี้

exports.sanitizeInputReturnArray = (username, password) => {
  $result = [username, password];
  return $result;
};

เมื่อเรารันอีกที มันก็จะผ่าน

second test passing
second test passing

สุดท้ายแหละที่เราทดสอบมามันแค่ความถูกต้องแบบพื้นฐานแต่สมมติเราบอกว่า ถ้ามีคนพิมพ์มาว่า

" or ""="

ไอ้ข้างบนคือมีคนพยายามจะลองของกับเรา ซึ่งถ้าเราไม่กรองสิ่งเหล่านี้ออก บรรลัยแน่ๆเวลาที่เราเอา username ไปหาใน db มันผ่านทันทีเลย ไม่อธิบายเรื่องนี้นะหาอ่านเอา keyword sql injection example

เราก็จะทดสอบว่าถ้ามีคนกรอกแบบนี้เราจะทำยังไง ในตัวอย่าง test นี้เราจะทำการแจ้งเตือนกลับออกมาเป็น error ก็ได้หรือจะเปลี่ยนพวก specialchar พวกนี้ ให้เป็นค่าอื่นก็ได้ แต่ผมจะแจ้งเตือนกลับไปแล้วกันโดยเราอาจจะคืนค่าว่ามันไม่ถูกต้องก็เพียงพอแล้วครับแต่ผมจะแสดงตัวอย่างอีกอันว่าถ้าหากเราคาดหวังให้มัน fail จะต้องเขียนยังไงไปเริ่มที่ Test case เราก่อน

const assert = require('assert');
const chai = require('chai');
let expect = chai.expect;
const login = require('../index');

describe('login', function() {
  describe('sanitizeInputReturnArray', function() {
    it('[SUCCESS] should return array 2 lenght', function() {
      result = login.sanitizeInputReturnArray('test', 'password');
      expect(result).to.have.lengthOf(2);
    });

    it('[SUCCESS] should return ["oxygenyoyo", "thisisabook"]', function() {
      result = login.sanitizeInputReturnArray('oxygenyoyo', 'thisisabook');
      expect(result[0]).to.equal('oxygenyoyo');
      expect(result[1]).to.equal('thisisabook');
    });

    it('[ERROR] should throw error when user try to input sql injection', function() {
      result = login.sanitizeInputReturnArray('" or ""="', 'thisisabook');
      expect(result).to.equal('do not insert single qoute or double qoute');
    });
  });
});

คราวนี้เรารันเทสดู

fail case
fail case

สิ่งที่เราคืนมันคือค่า array แต่ใน Test case นี้เราคาดหวังให้คืนเป็น do not insert single qoute or double qoute  เรากลับไปดูที่ function เรากันครับ

exports.sanitizeInputReturnArray = (username, password) => {
  const isSqlInjectionInputThenThrowError = /[\'\"\=]/gmi.test(username);
  if ( isSqlInjectionInputThenThrowError ) {
    return 'do not insert single qoute or double qoute';
  } 
  result = [username, password];
  return result;

  
};

เราทำการเพิ่มการ check พวกตัวอักษรแปลกๆ จริงๆอาจจะใช้ \w ก็ได้นะครับพวก regex ลองไปศึกษาอันนี้ผมทำเป็นตัวอย่างเฉยๆครับ เมื่อเรารันเทส จะได้ผลลัพธ์ออกมาแบบนี้

all passing
all passing

คราวนี้เราก็จะผ่านแล้วก็ไปคิด Test case ต่อไปคราวนี้หลายๆคนจะถามว่าแล้วเราต้องคิดเยอะแค่ไหน ขนาดไหน กี่ Test case ดีคำตอบคือ ก็ดูว่ามันสำคัญขนาดไหน ถ้าสำคัญมากก็ควรคิดให้ครบไม่ก็เรียก business มาคุยกว่าต้องการความครอบคลุมแบบไหน หมายถึงคุณต้องคิดว่ากรณีไหนเกิดขึ้นแล้ว business เขาคิดว่ามันเป็นเไปได้ไหม business เขาไม่รู้หรอก แต่อย่างน้อยๆคุณต้องถามว่าผลลัพธ์ของ flow การกระทำหนึ่งถ้าไปกระทบอีกอันจะเกิดแบบไหน คือ ตอนนี้เราทดสอบแค่ function เดียวอาจจะไม่เห็นภาพ แต่สมมติว่าอนาคตคุณทำ 10 function แล้วถ้ามีอันไหนเปลี่ยน มีผลกระทบต่ออีก function คุณก็ควรคิด case ที่เกิดขึ้นด้วย เมื่อคุณทดสอบหลายๆเคสจนกระทั่ง มันเชื่อถือได้ว่าโยนค่าอะไรมามันจะไม่รับ ถ้าโยนเข้ามาถูกต้องมันจะส่งออกถูกต้อง ก็ถือว่า function นี้ test ผ่านแล้วแหละครับ

มันช่วยอะไรเรา ?

จากที่เราทำมาทั้งหมดจะเห็นว่าเมื่อไหร่ที่เราแก้ไข​ Code เราสามารถรัน Test เพื่อทดสอบสิ่งที่เรา ไม่รู้ นึกไม่ถึง ได้หลายกรณีเช่น ถ้าเราไปเพิ่มเงื่อนไขอะไรแล้วเขียน Test ทดสอบเพิ่มเติมเราจะรู้ว่าถ้ามันมี Test ที่เราเคยทำไปแล้วมัน fail เราจะทราบว่า อ๋อถ้าเราแก้ไขตรงนี้ ตรงนั้นจะพัง แล้วถ้าเราทำผ่านทุกกรณีมันจะทำให้เรามั่นใจที่จะบอกคนอื่นได้ว่า มันจะไม่พัง ถ้ามีกรณีใหม่เข้ามาก็เขียน Test ทดสอบได้เลยไม่ต้องไปรันทั้งระบบใหม่ เช่น ถ้าสมมติว่า function มันต้องไปคลิกหน้าจออีก 4-5 ทีเพื่อให้มันทำงานเราสามารถสั่งให้มันทำงานได้เลย ทันทีแบบนี้ไม่ดีกว่าหรอ ถูกต้องไหมครับ ? หรือการ Login บางทีมันทำงานครั้งเดียว เราจะมากรอก form ทุกครั้งไหมที่เราแก้ไขโค้ด เราคงไม่ทำตรงนี้มันเหมือน save point ว่าเออถ้าแก้ไขอะไรไปอย่างน้อยๆที่ผ่านมาทั้งหมดมันจะไม่พังนะ

สรุป

เราได้เรียนรู้แล้วว่า Unittest มันทำยังไง และต้องคิด test case อะไรบ้าง แต่โลกความเป็นจริงแม่งไม่มีเวลาให้เราคิดหรอกเชื่อเถอะ ( ฮา ) ถ้าไปอยู่สถานที่ทำงานแบบเร่งๆจะเอางานอย่างเดียวต้องระวังมากๆ เพราะอะไรรู้ไหม สุดท้ายคุณจะต้องใช้หนี้ทางเทคนิคชนิดที่ว่าต้องมา support 7 วัน 24 ชั่วโมงเลยแหละ ตอนหน้าจะสอนเขียนเกี่ยวกับเมื่อเราต้อง test function ที่มันดันไปพึ่งพาคำตอบจาก function อื่นอีกที เราเรียกว่า dependency คือตัว function นี้ไม่สามารถกำหนดค่าทางเข้าออกได้เลย ถ้าอีก function หนึ่งไม่ทำงานก่อนแบบนี้ ติดตามและแชร์บทความนี้ด้วย สวัสดี

Loading

เป็นโปรแกรมเมอร์ที่ตามหาคุณค่าของชีวิตและความฝันในวัยเด็ก ชอบเล่นเกม เรียนรู้ทุกอย่าง ชอบเจอคนใหม่ๆ งานสังคมทุกชนิด ออกกำลังกายในวันว่าง อ่านหนังสือ มีเว็บรีวิวหนังสือด้วย www.readraide.in.th