ศึกษาเกี่ยวกับความครอบคลุมของโค้ด และค้นพบ 4 วิธีทั่วไปในการวัดผล
คุณเคยได้ยินวลี "Code Coverage" ไหม ในโพสต์นี้ เราจะมาดูกันว่า การครอบคลุมของโค้ดในการทดสอบคืออะไร และวิธีทั่วไป 4 วิธีในการวัดค่าดังกล่าว
ความครอบคลุมของรหัสคืออะไร
ความครอบคลุมของโค้ดเป็นเมตริกที่วัดเปอร์เซ็นต์ของซอร์สโค้ดที่การทดสอบของคุณใช้ โดยช่วยคุณระบุส่วนที่อาจขาดการทดสอบที่เหมาะสม
บ่อยครั้ง การบันทึกเมตริกเหล่านี้มีลักษณะดังนี้
ไฟล์ | % ข้อความ | % สาขา | ฟังก์ชัน% | % เส้น | เส้นที่ไม่ได้ครอบคลุม |
---|---|---|---|---|---|
file.js | 90% | 100% | 90% | 80% | 89,256 คน |
coffee.js | 55.55% | 80% | 50% | 62.5% | 10-11, 18 ปี |
ในขณะที่คุณเพิ่มฟีเจอร์และการทดสอบใหม่ๆ การเพิ่มเปอร์เซ็นต์ความครอบคลุมของโค้ดจะทำให้คุณมั่นใจว่าแอปพลิเคชันได้รับการทดสอบมาอย่างถี่ถ้วนแล้ว อย่างไรก็ตาม ยังมีอะไรให้ค้นพบอีกมากมาย
การครอบคลุมของโค้ดที่พบบ่อย 4 ประเภท
วิธีทั่วไปในการรวบรวมและคำนวณการครอบคลุมของโค้ดมีอยู่ 4 วิธี ได้แก่ ฟังก์ชัน บรรทัด สาขา และความครอบคลุมของใบแจ้งยอด
หากต้องการดูวิธีที่ความครอบคลุมของรหัสแต่ละประเภทคำนวณเปอร์เซ็นต์รหัสดังกล่าว ลองดูตัวอย่างโค้ดต่อไปนี้ในการคำนวณส่วนผสมของกาแฟ
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
การทดสอบที่ยืนยันฟังก์ชัน calcCoffeeIngredient
มีดังนี้
/* coffee.test.js */
import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown');
expect(result).to.deep.equal({});
});
});
คุณสามารถรันโค้ดและทดสอบในการสาธิตแบบสดนี้ หรือจะไปที่ที่เก็บก็ได้
การครอบคลุมของฟังก์ชัน
การครอบคลุมของโค้ด: 50%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
}
function isValidCoffee(name) {
// ...
}
การครอบคลุมของฟังก์ชันเป็นเมตริกที่ตรงไปตรงมา โดยจะบันทึกเปอร์เซ็นต์ของฟังก์ชันในโค้ดที่การทดสอบเรียกใช้
ในตัวอย่างโค้ด มี 2 ฟังก์ชัน ได้แก่ calcCoffeeIngredient
และ isValidCoffee
การทดสอบจะเรียกใช้ฟังก์ชัน calcCoffeeIngredient
เท่านั้น ดังนั้นความครอบคลุมของฟังก์ชันจึงเท่ากับ 50%
ความครอบคลุมของเส้น
การครอบคลุมของโค้ด: 62.5%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
ความครอบคลุมของบรรทัดจะวัดเปอร์เซ็นต์ของบรรทัดโค้ดที่เรียกใช้ได้ซึ่งชุดทดสอบของคุณดำเนินการ หากบรรทัดโค้ดยังคงไม่มีการดำเนินการ แสดงว่าโค้ดบางส่วนยังไม่ได้รับการทดสอบ
ตัวอย่างโค้ดมีโค้ดสั่งการ 8 บรรทัด (ไฮไลต์เป็นสีแดงและสีเขียว) แต่การทดสอบไม่ได้เรียกใช้เงื่อนไข americano
(2 บรรทัด) และฟังก์ชัน isValidCoffee
(บรรทัดเดียว) ซึ่งส่งผลให้มีความครอบคลุมบรรทัด 62.5%
โปรดทราบว่าความครอบคลุมของบรรทัดไม่คำนึงถึงคำแถลงประกาศ เช่น function isValidCoffee(name)
และ let espresso, water;
เนื่องจากไม่สามารถดำเนินการได้
ความครอบคลุมของสาขา
การครอบคลุมของโค้ด: 80%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
if (coffeeName === 'espresso') {
// ...
return { espresso };
}
if (coffeeName === 'americano') {
// ...
return { espresso, water };
}
return {};
}
…
การครอบคลุมของสาขาจะวัดเปอร์เซ็นต์ของสาขาที่ดำเนินการหรือจุดตัดสินใจในโค้ด เช่น คำสั่งหรือการวนซ้ำ ค่านี้กำหนดว่าการทดสอบจะตรวจสอบทั้งส่วนจริงและเท็จของคำสั่งแบบมีเงื่อนไขหรือไม่
ตัวอย่างโค้ดมี 5 Branch คือ
- การโทรไปยัง
calcCoffeeIngredient
โดยใช้เวลาเพียงcoffeeName
- กำลังโทรหา
calcCoffeeIngredient
กับcoffeeName
และcup
- กาแฟคือเอสเปรสโซ
- กาแฟคืออเมริกาโน
- กาแฟอื่นๆ
การทดสอบครอบคลุมทุกสาขายกเว้นเงื่อนไข Coffee is Americano
ดังนั้นความครอบคลุมของสาขาคือ 80%
ความครอบคลุมของใบแจ้งยอด
ความครอบคลุมของโค้ด: 55.55%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
return {};
}
export function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
การครอบคลุมของใบแจ้งยอดจะวัดเปอร์เซ็นต์ของคำสั่งในโค้ดที่การทดสอบของคุณดำเนินการ คุณอาจสงสัยว่า "อย่างนี้เหมือนกับความครอบคลุมของสายไหม" เมื่อมองเผินๆ อันที่จริง ความครอบคลุมของคำสั่งนั้นคล้ายกับความครอบคลุมของบรรทัด แต่จะพิจารณาโค้ดบรรทัดเดียวที่มีหลายคำสั่ง
ในตัวอย่างโค้ดจะมีโค้ดสั่งการ 8 บรรทัด แต่มี 9 คำสั่ง คุณเห็นบรรทัดที่มี 2 คำสั่งไหม
espresso = 30 * cup; water = 70 * cup;
การทดสอบดังกล่าวครอบคลุมเพียง 5 ข้อความจาก 9 ข้อความ ดังนั้นการครอบคลุมของใบแจ้งยอดคือ 55.55%
หากคุณเขียนใบแจ้งยอด 1 รายการต่อบรรทัดเสมอ ความครอบคลุมของบรรทัดรายการจะคล้ายกับความครอบคลุมของใบแจ้งยอด
คุณควรเลือกการครอบคลุมของโค้ดประเภทใด
เครื่องมือการครอบคลุมของโค้ดส่วนใหญ่จะมีการครอบคลุมโค้ด 4 ประเภททั่วไปดังนี้ การเลือกเมตริกการครอบคลุมของโค้ดที่จะให้ความสำคัญนั้นขึ้นอยู่กับข้อกำหนดของโปรเจ็กต์ แนวทางการพัฒนา และเป้าหมายการทดสอบที่เฉพาะเจาะจง
โดยทั่วไปแล้ว ความครอบคลุมของข้อความเป็นจุดเริ่มต้นที่ดี เนื่องจากเป็นเมตริกที่เข้าใจง่าย ซึ่งแตกต่างจากการครอบคลุมของใบแจ้งยอดตรงที่การครอบคลุมสาขาและการครอบคลุมของฟังก์ชันจะวัดว่าการทดสอบเรียกใช้เงื่อนไข (Branch) หรือฟังก์ชัน ดังนั้นข้อมูลดังกล่าวจึงเป็นความคืบหน้าตามธรรมชาติหลังการรายงานข่าว
เมื่อคุณได้รับใบแจ้งยอดในระดับสูงแล้ว คุณสามารถย้ายไปยังการครอบคลุมของสาขาและความครอบคลุมของฟังก์ชัน
การครอบคลุมของการทดสอบเหมือนกับการครอบคลุมของโค้ดไหม
ไม่ การครอบคลุมของการทดสอบและความครอบคลุมของโค้ดมักมีความสับสน แต่มีความแตกต่างกันดังนี้
- การครอบคลุมของการทดสอบ: เมตริกเชิงคุณภาพที่วัดว่าชุดทดสอบครอบคลุมฟีเจอร์ของซอฟต์แวร์ได้ดีเพียงใด ช่วยในการกำหนดระดับความเสี่ยงที่เกี่ยวข้อง
- การครอบคลุมของโค้ด: เมตริกเชิงปริมาณที่วัดสัดส่วนของโค้ดที่เรียกใช้ระหว่างการทดสอบ ซึ่งขึ้นอยู่กับว่าการทดสอบครอบคลุมโค้ดมากเพียงใด
ต่อไปนี้เป็นอุปมาอุปไมยง่ายๆ: ลองจินตนาการว่าเว็บแอปพลิเคชันเป็นเหมือนบ้าน
- ความครอบคลุมของการทดสอบจะวัดว่าการทดสอบครอบคลุมห้องต่างๆ ในบ้านได้ดีเพียงใด
- การครอบคลุมของโค้ดจะวัดจำนวนบ้านที่การทดสอบเดินผ่าน
ความครอบคลุมของโค้ด 100% ไม่ได้หมายความว่าไม่มีข้อบกพร่อง
แม้ว่าโค้ดที่ครอบคลุมสำหรับการทดสอบเป็นสิ่งที่ควรทำได้อยู่แล้ว แต่การครอบคลุมของโค้ด 100% ก็ไม่ได้เป็นการรับประกันว่าโค้ดจะไม่มีข้อบกพร่องหรือข้อบกพร่องใดๆ
วิธีที่ไม่มีความหมายเพื่อให้มีการครอบคลุมโค้ด 100%
ลองพิจารณาการทดสอบต่อไปนี้
/* coffee.test.js */
// ...
describe('Warning: Do not do this', () => {
it('is meaningless', () => {
calcCoffeeIngredient('espresso', 2);
calcCoffeeIngredient('americano');
calcCoffeeIngredient('unknown');
isValidCoffee('mocha');
expect(true).toBe(true); // not meaningful assertion
});
});
การทดสอบนี้ได้รับฟังก์ชัน บรรทัด สาขา และความครอบคลุมของคำสั่ง 100% แต่ไม่สมเหตุสมผลเพราะไม่ได้ทดสอบโค้ดจริง การยืนยัน expect(true).toBe(true)
จะถูกส่งไปเสมอไม่ว่าโค้ดจะทำงานอย่างถูกต้องหรือไม่
เมตริกที่ไม่เหมาะสมแย่กว่าไม่มีเมตริกเลย
เมตริกที่ไม่ดีอาจทำให้คุณไม่ได้รับความปลอดภัยเลย ซึ่งแย่กว่าการไม่มีเมตริกเลย ตัวอย่างเช่น หากคุณมีชุดทดสอบที่ครอบคลุมโค้ด 100% แต่การทดสอบทั้งหมดนั้นไม่มีความหมาย คุณอาจได้รับความรู้สึกด้านความปลอดภัยที่ผิดว่าโค้ดของคุณได้รับการทดสอบมาอย่างดี หากคุณลบโค้ดแอปพลิเคชันบางส่วนหรือทำให้เสียหายโดยไม่ได้ตั้งใจ การทดสอบจะยังคงผ่านแม้ว่าแอปพลิเคชันจะทำงานไม่ถูกต้องอีกต่อไป
วิธีหลีกเลี่ยงสถานการณ์นี้
- ตรวจสอบการทดสอบ เขียนและตรวจสอบการทดสอบเพื่อให้แน่ใจว่ามีความหมาย และทดสอบโค้ดในสถานการณ์ต่างๆ ที่แตกต่างกัน
- ใช้ความครอบคลุมของโค้ดเป็นแนวทาง ไม่ใช่การวัดประสิทธิภาพการทดสอบหรือคุณภาพของโค้ดเพียงอย่างเดียว
การใช้การครอบคลุมของโค้ดในการทดสอบประเภทต่างๆ
มาดูรายละเอียดเพิ่มเติมเกี่ยวกับวิธีใช้การครอบคลุมของโค้ดกับการทดสอบทั่วไป 3 ประเภทกัน ดังนี้
- แบบทดสอบหน่วย การทดสอบประเภทนี้เป็นประเภทการทดสอบที่ดีที่สุดสำหรับการรวบรวมการครอบคลุมของโค้ด เนื่องจากออกแบบมาเพื่อให้ครอบคลุมสถานการณ์เล็กๆ มากมายและเส้นทางการทดสอบ
- การทดสอบการผสานรวม ซึ่งจะช่วยรวบรวมการครอบคลุมของโค้ดสำหรับการทดสอบการผสานรวมได้ แต่ควรใช้ด้วยความระมัดระวัง ในกรณีนี้ คุณจะคำนวณความครอบคลุมของซอร์สโค้ดขนาดใหญ่ และอาจทำให้ยากที่จะพิจารณาว่าการทดสอบใดครอบคลุมส่วนใดของโค้ดบ้าง อย่างไรก็ตาม การคำนวณความครอบคลุมของโค้ดของการทดสอบการผสานรวมอาจมีประโยชน์สำหรับระบบเดิมที่ไม่มีหน่วยแยกต่างหาก
- การทดสอบแบบเอนด์ทูเอนด์ (E2E) การวัดความครอบคลุมของโค้ดสำหรับการทดสอบ E2E นั้นทำได้ยากและยากเนื่องจากการทดสอบเหล่านี้มีความซับซ้อน ความครอบคลุมของข้อกำหนดอาจเป็นวิธีที่ดีกว่า แทนที่จะใช้การครอบคลุมของโค้ด เนื่องจากจุดมุ่งเน้นของการทดสอบ E2E คือให้ครอบคลุมข้อกําหนดของการทดสอบ ไม่ใช่เน้นที่ซอร์สโค้ด
บทสรุป
ความครอบคลุมของโค้ดเป็นเมตริกที่มีประโยชน์ในการวัดประสิทธิภาพของการทดสอบ โซลูชันนี้จะช่วยให้คุณปรับปรุงคุณภาพของแอปพลิเคชันได้ โดยตรวจสอบว่าตรรกะสำคัญในโค้ดของคุณได้รับการทดสอบมาอย่างดี
อย่างไรก็ตาม โปรดทราบว่าความครอบคลุมของโค้ดเป็นเพียงเมตริกเดียว นอกจากนี้ คุณยังต้องพิจารณาปัจจัยอื่นๆ ด้วย เช่น คุณภาพการทดสอบและข้อกำหนดการสมัคร
การมุ่งเป้าให้การครอบคลุมโค้ด 100% ไม่ใช่เป้าหมาย แต่คุณควรใช้ความครอบคลุมของโค้ด ร่วมกับแผนการทดสอบที่รอบด้าน ซึ่งใช้วิธีการทดสอบที่หลากหลาย เช่น การทดสอบ 1 หน่วย การทดสอบการผสานรวม การทดสอบจากต้นทางถึงปลายทาง และการทดสอบด้วยตนเอง
ดูตัวอย่างโค้ดแบบเต็มและการทดสอบที่มีการครอบคลุมโค้ดที่ดี นอกจากนี้ คุณยังสามารถเรียกใช้โค้ดและทดสอบด้วยการสาธิตแบบสดนี้
/* coffee.js - a complete example */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
if (!isValidCoffee(coffeeName)) return {};
let espresso, water;
if (coffeeName === 'espresso') {
espresso = 30 * cup;
return { espresso };
}
if (coffeeName === 'americano') {
espresso = 30 * cup; water = 70 * cup;
return { espresso, water };
}
throw new Error (`${coffeeName} not found`);
}
function isValidCoffee(name) {
return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */
import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';
describe('Coffee', () => {
it('should have espresso', () => {
const result = calcCoffeeIngredient('espresso', 2);
expect(result).to.deep.equal({ espresso: 60 });
});
it('should have americano', () => {
const result = calcCoffeeIngredient('americano');
expect(result.espresso).to.equal(30);
expect(result.water).to.equal(70);
});
it('should throw error', () => {
const func = () => calcCoffeeIngredient('mocha');
expect(func).toThrowError(new Error('mocha not found'));
});
it('should have nothing', () => {
const result = calcCoffeeIngredient('unknown')
expect(result).to.deep.equal({});
});
});