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

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

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

การครอบคลุมบรรทัดจะวัดเปอร์เซ็นต์ของบรรทัดโค้ดที่เรียกใช้งานได้ซึ่งชุดทดสอบของคุณเรียกใช้ หากโค้ด 1 บรรทัดยังคงไม่ทำงาน แสดงว่าโค้ดบางส่วนยังไม่ได้รับการทดสอบ

ตัวอย่างโค้ดมีโค้ดที่เรียกใช้งานได้ 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 {};
}

การครอบคลุมสาขาจะวัดเปอร์เซ็นต์ของสาขาหรือจุดตัดสินใจที่ดำเนินการในโค้ด เช่น คำสั่ง if หรือลูป ตัวเลือกนี้จะกำหนดว่าทดสอบทั้งสาขาที่เป็นจริงและเท็จของคำสั่งเงื่อนไขหรือไม่

ตัวอย่างโค้ดนี้มี 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({});
  });
});