ศึกษาเกี่ยวกับความครอบคลุมของโค้ด และค้นพบ 4 วิธีทั่วไปในการวัดผล
คุณเคยได้ยินวลี "Code Coverage" ไหม ในโพสต์นี้ เราจะอธิบายความหมายของโค้ดโคเวอร์เฮดในการทดสอบและวิธีวัดผล 4 วิธีที่ใช้กันโดยทั่วไป
การครอบคลุมโค้ดคืออะไร
การครอบคลุมโค้ดคือเมตริกที่วัดเปอร์เซ็นต์ของซอร์สโค้ดที่การทดสอบของคุณเรียกใช้ ซึ่งจะช่วยให้คุณระบุจุดที่อาจขาดการทดสอบที่เหมาะสม
บ่อยครั้งที่การบันทึกเมตริกเหล่านี้มีลักษณะดังนี้
ไฟล์ | % ข้อความ | % Branch | % ฟังก์ชัน | % บรรทัด | เส้นที่ไม่ครอบคลุม |
---|---|---|---|---|---|
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
(1 บรรทัด) ส่งผลให้มีเส้นครอบคลุม 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 สาขา ดังนี้
- การโทรหา
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);
}
การครอบคลุมคำสั่งจะวัดเปอร์เซ็นต์ของคำสั่งในโค้ดที่การทดสอบดำเนินการ เมื่อเห็นข้อมูลนี้ครั้งแรก คุณอาจสงสัยว่า "นี่ไม่เหมือนกับการครอบคลุมบรรทัดใช่ไหม" แน่นอนว่าการครอบคลุมคำสั่งนั้นคล้ายกับการครอบคลุมบรรทัด แต่พิจารณาโค้ด 1 บรรทัดที่มีคำสั่งหลายรายการ
ในตัวอย่างโค้ด มีโค้ดที่เรียกใช้ได้ 8 บรรทัด แต่มีคำสั่ง 9 รายการ คุณเห็นบรรทัดที่มีคําสั่ง 2 รายการไหม
espresso = 30 * cup; water = 70 * cup;
การทดสอบครอบคลุมข้อความเพียง 5 รายการจาก 9 รายการ ดังนั้นความครอบคลุมของข้อความจึงเท่ากับ 55.55%
หากคุณเขียนใบแจ้งยอด 1 รายการต่อบรรทัดเสมอ ความครอบคลุมของบรรทัดรายการจะคล้ายกับความครอบคลุมของใบแจ้งยอด
คุณควรเลือกการครอบคลุมโค้ดประเภทใด
เครื่องมือการครอบคลุมโค้ดส่วนใหญ่จะรวมการครอบคลุมโค้ดทั่วไป 4 ประเภทนี้ การเลือกเมตริกการครอบคลุมโค้ดที่จะให้ความสําคัญนั้นขึ้นอยู่กับข้อกําหนดของโปรเจ็กต์ แนวทางการพัฒนา และเป้าหมายการทดสอบที่เฉพาะเจาะจง
โดยทั่วไปแล้ว ความครอบคลุมของข้อความเป็นจุดเริ่มต้นที่ดีเนื่องจากเป็นเมตริกที่เข้าใจง่าย ความครอบคลุมของสาขาและความครอบคลุมของฟังก์ชันจะวัดว่าการทดสอบเรียกเงื่อนไข (สาขา) หรือฟังก์ชัน ต่างจากความครอบคลุมของคำสั่ง ดังนั้น จึงเป็นขั้นตอนที่พัฒนาตามปกติหลังจากการครอบคลุมคำสั่ง
เมื่อคุณได้รับใบแจ้งยอดในระดับสูงแล้ว คุณสามารถย้ายไปยังการครอบคลุมของสาขาและความครอบคลุมของฟังก์ชัน
การครอบคลุมการทดสอบเหมือนกับการครอบคลุมโค้ดไหม
ไม่ได้ ความครอบคลุมของการทดสอบและความครอบคลุมของโค้ดมักสับสนกัน แต่มีความแตกต่างกันดังนี้
- ความครอบคลุมของการทดสอบ: เมตริกเชิงคุณภาพที่วัดว่าชุดทดสอบครอบคลุมฟีเจอร์ของซอฟต์แวร์ได้ดีเพียงใด ซึ่งจะช่วยระบุระดับความเสี่ยงที่เกี่ยวข้อง
- การครอบคลุมโค้ด: เมตริกเชิงปริมาณที่วัดสัดส่วนโค้ดที่ดำเนินการระหว่างการทดสอบ หมายถึงจำนวนโค้ดที่การทดสอบครอบคลุม
ลองจินตนาการว่าเว็บแอปพลิเคชันเป็นบ้าน
- ความครอบคลุมของการทดสอบจะวัดว่าการทดสอบครอบคลุมห้องต่างๆ ในบ้านได้ดีเพียงใด
- ความครอบคลุมของโค้ดจะวัดจำนวนบ้านที่การทดสอบเดินผ่าน
ความครอบคลุมของโค้ด 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 ประเภทกัน ดังนี้
- การทดสอบ 1 หน่วย ซึ่งเป็นประเภทการทดสอบที่ดีที่สุดสำหรับการรวบรวมการครอบคลุมโค้ด เนื่องจากออกแบบมาเพื่อครอบคลุมสถานการณ์และเส้นทางการทดสอบย่อยๆ หลายรายการ
- การทดสอบการผสานรวม เครื่องมือเหล่านี้ช่วยรวบรวมการครอบคลุมโค้ดสําหรับการทดสอบการผสานรวมได้ แต่โปรดใช้ด้วยความระมัดระวัง ในกรณีนี้ คุณจะคำนวณความครอบคลุมของซอร์สโค้ดที่มีขนาดใหญ่ และทำให้การพิจารณาว่าการทดสอบใดครอบคลุมส่วนใดของโค้ดได้ยาก อย่างไรก็ตาม การคํานวณการครอบคลุมโค้ดของการทดสอบการผสานรวมอาจมีประโยชน์สําหรับระบบเดิมที่ไม่มีหน่วยแยกที่ดี
- การทดสอบแบบเอนด์ทูเอนด์ (E2E) การวัดการครอบคลุมโค้ดสําหรับการทดสอบ E2E นั้นทําได้ยากและท้าทายเนื่องจากลักษณะการทดสอบเหล่านี้มีความซับซ้อน การครอบคลุมข้อกําหนดอาจดีกว่าการใช้การครอบคลุมโค้ด เนื่องจากจุดเน้นของการทดสอบจากต้นทางถึงปลายทางคือการครอบคลุมข้อกําหนดของการทดสอบ ไม่ใช่การมุ่งเน้นที่ซอร์สโค้ด
บทสรุป
การครอบคลุมโค้ดอาจเป็นเมตริกที่มีประโยชน์ในการวัดประสิทธิภาพของการทดสอบ โซลูชันนี้จะช่วยให้คุณปรับปรุงคุณภาพของแอปพลิเคชันได้ โดยตรวจสอบว่าตรรกะสำคัญในโค้ดของคุณได้รับการทดสอบมาอย่างดี
อย่างไรก็ตาม โปรดทราบว่าความครอบคลุมของโค้ดเป็นเพียงเมตริกเดียว นอกจากนี้ อย่าลืมพิจารณาปัจจัยอื่นๆ ด้วย เช่น คุณภาพของการทดสอบและข้อกำหนดของแอปพลิเคชัน
เป้าหมายไม่ใช่การครอบคลุมโค้ด 100% แต่คุณควรใช้การครอบคลุมโค้ดร่วมกับแผนการทดสอบที่ครอบคลุมซึ่งรวมวิธีการทดสอบที่หลากหลายไว้ด้วยกัน ซึ่งรวมถึงการทดสอบหน่วย การทดสอบการผสานรวม การทดสอบจากต้นทางถึงปลายทาง และการทดสอบด้วยตนเอง
ดูตัวอย่างโค้ดและทดสอบแบบเต็มที่มีการครอบคลุมโค้ดที่ดี นอกจากนี้ คุณยังเรียกใช้โค้ดและทำการทดสอบด้วยการสาธิตเวอร์ชันที่ใช้จริงนี้ได้ด้วย
/* 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({});
});
});