Angular Testing part 2

Phai Panda
6 min readOct 31, 2019

--

TypeScript ดีกว่า JavaScript ยากไปเหรอ?

background photo created by freepik

จาก jasmine examples ใน part แรก โอกาสนี้ผมจะพาทำ TDD แบบง่ายๆตามตัวอย่างที่เราได้โหลดมาไว้ในโปรเจกต์ เขียนด้วย JavaScript ก่อนจะพาไป config เพื่อเขียนมันอีกครั้งด้วย TypeScript แทน สรุปเป็นเนื้อหาของ part ได้ดังนี้

  • เล่าเรื่องแบบ TDD ที่ง่ายที่สุด
  • เขียน Test ด้วย JavaScript
  • config โปรเจกต์ให้เขียน TypeScript ได้
  • เขียน Test ด้วย TypeScript

เตียมความพร้อม

Let get started

เวลาผมจะเรียนรู้อะไรไวๆ ผมมองหา 2 อย่าง 1. หัวใจหรือเป้าหมาย 2. หลักการหรือตัวอย่าง

เป้าหมายเราคือเข้าใจและเขียน Angular Testing ได้ หลักการคือ Jasmine Test Framework กับตัวอย่างที่หาได้จากมันนี่แหละ

  1. เรามี contacts folder ที่สร้างไว้แล้ว ให้ลบทุกสิ่งออกจากมัน ให้มันว่างเปล่าอีกครั้ง
  2. พิมพ์ jasmine init เพื่อเริ่มต้นโครงสร้างโปรเจกต์
  3. แก้ไขค่า config โดยกำหนด stopSpecOnExpectationFailure เป็น true และ random เป็น false
  4. ขณะนี้ root project คือ contacts ภายในมี folder ชื่อ spec อยู่แล้ว ให้สร้าง folder ชื่อ player อยู่ภายใต้ spec folder อีกที
  5. ใน player folder สร้างไฟล์ชื่อ player.spec.js

TDD บอกว่าให้เขียน Test แล้วให้ Test เขียน Code ให้ จากตรงนี้เราต้องการคลาส Player หรือว่าเครื่องเล่นเพลง เพื่อเอามาเล่นเพลง ​(Song) ที่เราต้องการ

อยากได้เครื่องเล่นเพลงใช่ไหม งั้นเราต้องมี Player ก่อน ก็บอกเข้าไปใน player.spec.js ว่า อยากได้ Player

describe('Player', function() {    var Player = require('../../data/player')})

จากนั้นรัน

jasmine player.spec.js

เท่านี้มันก็ฟ้องแล้วว่า หาไม่เจอนะ Player module น่ะ

นี่ยังไม่เรียกว่า Test ไม่ผ่านนะครับ เพราะโปรแกรมนี้พังก่อน (หา module ไม่เจอไง) เอาล่ะไม่เป็นไร เพราะเราสายเลื้อยอยู่แล้ว เรากำหนดว่าจะพบ Player ได้ที่ data folder ซึ่งอยู่ถัดจาก root project ช้าอะไร ไปสร้างมันสิ

สร้างไฟล์ player.js
player.js อยู่ใน data folder

จาก data folder ก็ไกลจาก player.spec.js เหลือเกิน งั้นเลื้อยไปหามันหน่อยแล้วกัน

jasmine ../spec/player/player.spec.js

เพื่อนๆจะเห็นว่าก็เลื้อยไปได้นะ แต่มันกลับบอกว่า jasmine — random=true ? ไหนมันบอกว่าเรา random เฉยเลย หรือว่ามันไม่ได้อ่านไฟล์ jasmine.json กันนะ อื่ม…

jasmine ../spec/player/player.spec.js --config=../spec/support/jasmine.json

เป็นอันว่าสำเร็จ มันอ่านไฟล์ config แล้ว และยังบอกอีกว่าใน player.spec.js นี้ไม่มี specs ใดๆเลย อะแน่สิ ก็เรายังไม่ได้สร้าง

คาดหวัง player เป็น null

ตอนนี้เราอ้างอิงไปยังไฟล์ player.js ได้แล้ว ต่อไปให้สร้างตัวแปร player ขึ้นมา ก็คาดหวังว่ามันจะได้ค่าเป็น null เพราะว่าเรายังไม่ได้มอบค่าให้

describe('Player', function() {    var Player = require('../../data/player')    var player    it('should be null', function() {        expect(player).toEqual(null)    })})

แล้วรัน (อย่าลืมนะว่าเราอยู่ที่ data folder)

jasmine ../spec/player/player.spec.js --config=../spec/support/jasmine.json

โดนเลยทีนี้

Started
F
Failures:
1) Player should be null
Message:
Expected undefined to equal null.
Stack:
Error: Expected undefined to equal null.
1 spec, 1 failure
Finished in 0.008 seconds

เห็นไหม spec ให้ค่าเป็น false หรือก็คือ Test พัง กล่าวคือ ไม่ผ่าน!

เหตุเพราะ JavaScript เวลาประกาศตัวแปรเฉยๆมันจะ assign ค่า undefined ให้อัตโนมัติ ดังนั้นแก้ไข

describe('Player', function() {    var Player = require('../../data/player')    var player = null    it('should be null', function() {        expect(player).toBe(null)    })})

แล้วรันอีกครั้ง

Started
.
1 spec, 0 failures
Finished in 0.007 seconds

ผ่าน!

คาดหวัง player ไม่เป็น null

ต่อไปสร้าง Player ออกมาครับ คาดหวังว่ามันจะไม่ null

describe('Player', function() {    var Player = require('../../data/player')    var player = null    it('should be null', function() {        expect(player).toBe(null)    })    it('should not be null', function() {        player = new Player()        expect(player).not.toBe(null)    })})

แล้วรันอีกครั้ง

Started
.F
Failures:
1) Player should not be null
Message:
TypeError: Player is not a constructor
2 specs, 1 failure
Finished in 0.01 seconds

ไม่ผ่าน!

เหตุเพราะ Player ไม่มี constructor ดังนั้นแก้ไข

เปิดไฟล์ player.js แล้วเพิ่มโค้ด

function Player() { }module.exports = Player

แล้วรันอีกครั้ง

Started
..
2 specs, 0 failures
Finished in 0.01 seconds

เพื่อนๆจะเห็นว่า Code ในไฟล์ player.js ค่อยๆเกิดขึ้นแล้วจาก Test ที่พัง ลุยต่ออีกนิดครับ

คาดหวัง player เรียก play(song) ได้

จิ้ม Test ไปก่อนเลยครับ

...
it('should be able to play a song', function() {
player = new Player() song = new Song() player.play(song); expect(player.currentlyPlayingSong).toEqual(song) })...

แล้วรัน

Started
..F
Failures:
1) Player should be able to play a song
Message:
ReferenceError: Song is not defined
3 specs, 1 failure
Finished in 0.012 seconds

แก้ไข เพิ่ม require Song

describe('Player', function() {    var Player = require('../../data/player')    var Song = require('../../data/song')    var player = null...

แล้วรัน

Error: Cannot find module ‘../../data/song’

แก้ไข สร้างไฟล์ song.js ไว้ใน data folder

แล้วรัน

TypeError: Song is not a constructor

แก้ไข สร้างคลาส Song ในไฟล์ song.js

function Song() { }module.exports = Song

แล้วรัน

TypeError: player.play is not a function

แก้ไข เพิ่ม play function ที่มี song เป็นพารามิเตอร์ให้กับคลาส Player

function Player() { }Player.prototype.play = function(song) { }module.exports = Player

ปกติแล้ว JavaScript ไม่มีคลาส กล่าวคือมันใช้ function นั่นแหละสร้างเป็นคลาส โดยสามารถเพิ่มสิ่งที่เรียกว่า prototype เข้าไปได้ภายหลังเพื่อให้ได้สมบัติการสืบทอด

แล้วรัน

Expected undefined to equal Song({ })

แก้ไข เหตุเพราะ currentlyPlayingSong ยังไม่ถูกสร้างขึ้น ค่านี้จึงเป็น undefined ดังนั้นสร้าง currentlyPlayingSong ให้กับคลาส Player

function Player() { }Player.prototype.currentlyPlayingSong = nullPlayer.prototype.play = function(song) { }module.exports = Player

แล้วรัน

Expected null to equal Song({ })

แก้ไข เราคาดหวังว่า expect(player.currentlyPlayingSong).toEqual(song) ค่าของ currentlyPlayingSong จะชี้ไปยังค่าของ song ฉะนั้นเพิ่มโค้ดที่ Player อีก

...Player.prototype.play = function(song) {    this.currentlyPlayingSong = song}
...

แล้วรัน

Started
...
3 specs, 0 failures
Finished in 0.011 seconds

สรุปการเล่าเรื่องแบบ TDD ที่ง่ายที่สุด

เราใช้ Test นำ Code เสมอ เราอยากได้อะไรให้ไปบอกกับ Test ในรูปแบบของ specs พอรัน Test แล้วมันจะต้องพัง (ไม่ผ่าน) เป็นอันดับแรกก่อน จากนั้นเราจึงเขียน Code เพื่อให้ spec ผ่านไปทีละอัน กระทั่งได้ 0 failures

เมื่อใดก็ตามที่ต้องการเพิ่มหรือแก้ไขโปรแกรม ก็ให้ไปเพิ่มหรือแก้ไข specs ก่อน จากนั้นรันให้พัง (ไม่ผ่าน) แล้วค่อยไปเพิ่มหรือแก้ไขโค้ดของโปรแกรมในภายหลัง วนไปอย่างนี้ Test นำ Code ตาม จบข่าว

ตัวอย่างข้างต้นก็หอมปากหอมคอพอประมาณ ความยากและความนานของ Test ในการเขียน specs สำหรับโปรแกรมเมอร์มือใหม่ก็คือ jasmine มันมีอะไรให้ใช้บ้างและใช้อย่างไร รวมไปถึง syntax ของ JavaScript อันสุดคัน

Jasmine กับ TypeScript

ผมจะพามา config โปรเจกต์ให้เข้าใจ TypeScript ตามบทความของท่านผู้นี้ครับ

ย้าย prompt ไปที่ contacts folder จากนั้นพิมพ์

npm init

จะได้ไฟล์ package.json

ต่อด้วย dev dependencies เหล่านี้

npm i -D jasmine ts-node typescript @types/jasmine

แก้ไขไฟล์ package.json ตรง scripts test กำหนดเป็น

...
"scripts": {
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json"},
...

ย้าย jasmine.json เดิมซึ่งอยู่ใน support folder ออกมาไว้ที่ root project (ไม่ใช่อะไรหรอกครับ ต้องการให้ path มันสั้นเท่านั้นเอง) แล้วลบ support folder ทิ้งไป

แก้ไขไฟล์ jasmine.json อีกเล็กน้อยให้รู้จัก .ts

...
"spec_files": [
"**/*[sS]pec.js", "**/*[sS]pec.ts"],
...

จากนั้นรัน

npm run test
สำเร็จ

มาทดลองเขียน spec เพิ่มกัน

คาดหวังสามารถเรียก constructor คลาส Song ได้

สร้าง song folder ไว้ภายใต้ spec folder จากนั้นภายใน song folder สร้างไฟล์ชื่อ song.spec.ts (สังเกตนามสกุลไฟล์เป็น .ts)

เขียน spec เข้าไปว่า

import * as Song from '../../data/song'describe('Song', () => {    it('should have a valid constructor', () => {        const song = new Song();        expect(song).not.toBeNull()    })})

แล้วรัน

Started
....
4 specs, 0 failures

คาดหวังสามารถระบุชื่อเพลงผ่าน constructor คลาส Song ได้

ไฟล์ song.spec.ts เพิ่ม spec เข้าไป

...
it('should set song name correctly through constructor', ()=> {
const song = new Song('Love yourself') expect(song.name).toEqual('Love yourself') })...

แล้วรัน

....FFailures:
1) Song should set song name correctly through constructor
Message:
Expected undefined to equal 'Love yourself'.
5 specs, 1 failure
Finished in 0.016 seconds

แก้ไข เปิดไฟล์ song.js แล้วเพิ่ม name เป็นพารามิเตอร์ของ constructor จากนั้นระบุมันให้กับ property ชื่อ name ตามที่ spec กำหนด

function Song(name) {    this.name = name}module.exports = Song

แล้วรัน

Started
.....
5 specs, 0 failures
Finished in 0.012 seconds

เห็นไหม เราทำได้

หมายเหตุ หากว่า VS Code ของเพื่อนๆแจ้ง message เกี่ยวกับ esModuleInterop เมื่อ TypeScript พยายาม import (default) JavaScript module มาใช้งาน ให้เพื่อนๆสร้างไฟล์ชื่อ tsconfig.json ไว้ ณ root project แล้วกำหนดค่านี้ลงไป

{    "compilerOptions": {        "esModuleInterop": false    }}

อ่านเพิ่มเติมเกี่ยวกับ TypeScript Compiler Options

สรุป

โปรแกรมเมอร์มือใหม่จะเขียน TypeScript แทน JavaScript นั้นไม่ยากหากว่ามีความรู้และรู้จัก TypeScript มากพอ ปกติแล้วเมื่อเราเขียน Test ด้วย Angular framework เรามักเรียนรู้ TypeScript ตั้งแต่แรกเริ่มไปพร้อมกับตัว framework โดยปริยาย ความสับสนจะเกิดขึ้นก็ต่อเมื่อพยายามแยกแยะระหว่าง ES5 หรือเก่ากว่านี้กับ ES6 หรือใหม่กว่านี้ ไม่เป็นไร เมื่อรู้ช่องโหว่ของตนเองแล้วก็หาเวลาศึกษาเพิ่มเติมครับ

รู้จักกับ ES6

เจอกัน part ถัดไปครับ (ถ้าชอบอย่าลืมปรบมือให้ด้วยล่ะ)

--

--

No responses yet