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

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

ความครอบคลุมข้อความ 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. กาแฟเป็นอเมริกาโน เครื่องหมายกากบาท
  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);
}

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