ความครอบคลุมของโค้ดที่พบบ่อย 4 ประเภท

ศึกษาเกี่ยวกับความครอบคลุมของโค้ด และค้นพบ 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 วิธี ได้แก่ ฟังก์ชัน บรรทัด สาขา และความครอบคลุมของใบแจ้งยอด

ความครอบคลุมข้อความ 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 คือ

  1. การโทรไปยัง calcCoffeeIngredient โดยใช้เวลาเพียง coffeeName เครื่องหมายถูก
  2. กำลังโทรหา calcCoffeeIngredient กับ coffeeName และ cup เครื่องหมายถูก
  3. กาแฟคือเอสเปรสโซ เครื่องหมายถูก
  4. กาแฟคืออเมริกาโน เครื่องหมาย X
  5. กาแฟอื่นๆ เครื่องหมายถูก

การทดสอบครอบคลุมทุกสาขายกเว้นเงื่อนไข 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({});
  });
});