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

ดูว่าการครอบคลุมของโค้ดคืออะไรและดูวิธีทั่วไป 4 วิธีในการวัดความครอบคลุมของโค้ด

คุณเคยได้ยินวลี "code configuration" ไหม ในโพสต์นี้ เราจะมาดูกันว่าการครอบคลุมของโค้ดในการทดสอบคืออะไร และวิธีการวัดผลที่ใช้ทั่วๆ ไป 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 (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 สาขา ได้แก่

  1. กำลังโทรหา calcCoffeeIngredient โดยใช้เพียง coffeeName เครื่องหมายตรวจสอบ
  2. กำลังโทรหา calcCoffeeIngredient ด้วย coffeeName และ cup เครื่องหมายตรวจสอบ
  3. กาแฟคือเอสเปรสโซ เครื่องหมายตรวจสอบ
  4. กาแฟคือ Americano เครื่องหมาย 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%

หากคุณเขียนหนึ่งข้อความต่อบรรทัดเสมอ ความครอบคลุมบรรทัดของคุณจะคล้ายกับความครอบคลุมของใบแจ้งยอด

คุณควรเลือกการครอบคลุมของโค้ดประเภทใด

เครื่องมือความครอบคลุมของโค้ดส่วนใหญ่ประกอบด้วยความครอบคลุมของโค้ดทั่วไป 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 ทำได้ยากและท้าทายเนื่องจากลักษณะของการทดสอบเหล่านี้มีความซับซ้อน แทนที่จะใช้ความครอบคลุมของโค้ด การครอบคลุมของข้อกำหนดอาจเป็นวิธีที่ดีกว่า เนื่องจากจุดมุ่งเน้นของการทดสอบ 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({});
  });
});