일반적인 코드 적용 범위의 네 가지 유형

코드 적용 범위가 무엇인지 알아보고 이를 측정하는 4가지 일반적인 방법을 살펴보세요.

'코드 적용 범위'라는 문구를 들어 보셨나요? 이 게시물에서는 테스트의 코드 적용 범위와 이를 측정하는 4가지 일반적인 방법을 살펴봅니다.

코드 적용 범위란 무엇인가요?

코드 적용 범위는 테스트가 실행하는 소스 코드의 비율을 측정하는 측정항목입니다. 적절한 테스트가 부족한 영역을 파악하는 데 도움이 됩니다.

보통 이러한 측정항목은 다음과 같이 기록합니다.

파일 % 설명 % 브랜치 % 함수 행 비율(%) 확인되지 않은 노선
file.js 90% 100% 90% 80% 89,256명
coffee.js 55.55% 80% 50% 62.5% 만 10~11세, 만 18세

새로운 기능과 테스트를 추가할 때 코드 적용 범위 비율을 높이면 애플리케이션이 철저하게 테스트되었다는 확신을 가질 수 있습니다. 하지만 아직 발견해야 할 것이 더 많이 있습니다.

코드 적용 범위의 일반적인 네 가지 유형

코드 적용 범위를 수집하고 계산하는 일반적인 방법은 함수, 라인, 브랜치, 문 적용 범위라는 네 가지입니다.

네 가지 유형의 텍스트 적용 범위

각 코드 적용 범위 유형이 백분율을 계산하는 방법을 알아보려면 커피 재료를 계산하는 다음 코드 예를 참고하세요.

/* 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) {
  // ...
}

함수 적용 범위는 간단한 측정항목입니다. 테스트에서 호출하는 코드 내 함수의 비율을 캡처합니다.

코드 예시에는 calcCoffeeIngredientisValidCoffee라는 두 가지 함수가 있습니다. 테스트는 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 조건 (두 줄)과 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 {};
}
…

브랜치 적용 범위는 if 문 또는 루프와 같이 코드에서 실행된 브랜치 또는 결정 지점의 비율을 측정합니다. 테스트가 조건문의 참 분기와 거짓 분기를 모두 검사할지 결정합니다.

코드 예시에는 5개의 브랜치가 있습니다.

  1. coffeeName 체크 표시.만으로 calcCoffeeIngredient님에게 전화 거는 중
  2. coffeeName님, cup체크 표시.님과 calcCoffeeIngredient님에게 전화 거는 중
  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개의 문이 있습니다. 두 개의 진술이 포함된 줄을 찾을 수 있나요?

정답 확인하기

코드는 다음과 같습니다. espresso = 30 * cup; water = 70 * cup;

이 테스트는 9개 문구 중 5개만 다루므로 구문 적용 범위는 55.55%입니다.

항상 한 줄당 하나의 구문을 작성하면 행 범위가 명세서 적용 범위와 비슷해집니다.

어떤 유형의 코드 적용 범위를 선택해야 할까요?

대부분의 코드 적용 범위 도구에는 다음 네 가지 유형의 공통 코드 적용 범위가 포함되어 있습니다. 우선순위를 지정할 코드 적용 범위 측정항목은 프로젝트 요구사항, 개발 관행, 테스트 목표에 따라 달라집니다.

일반적으로 명세서 노출 범위는 간단하고 이해하기 쉬운 측정항목이므로 좋은 출발점이 됩니다. 문 적용 범위와 달리 브랜치 범위 및 함수 적용 범위는 테스트가 조건 (브랜치)을 호출하는지 또는 함수를 호출하는지 측정합니다. 따라서 명세서 적용 이후에 자연스럽게 진행됩니다.

높은 명세서 적용 범위를 달성하면 브랜치 커버리지와 함수 커버리지로 이동할 수 있습니다.

테스트 적용 범위는 코드 적용 범위와 동일한가요?

아니요. 테스트 적용 범위와 코드 적용 범위는 혼동되는 경우가 많지만 서로 다릅니다.

  • 테스트 범위: 테스트 모음이 소프트웨어의 기능을 얼마나 잘 다루는지 측정하는 정성적 측정항목입니다. 이는 관련된 위험 수준을 판단하는 데 도움이 됩니다.
  • 코드 적용 범위: 테스트 중에 실행된 코드의 비율을 측정하는 정량적 측정항목입니다. 이는 테스트에 포함된 코드의 양에 관한 것입니다.

웹 애플리케이션을 집처럼 비유해 보겠습니다.

  • 테스트 커버리지는 테스트가 집 안의 방을 얼마나 잘 포함하는지를 측정합니다.
  • 코드 적용 범위는 테스트가 통과한 하우스의 양을 측정합니다.

코드 적용 범위가 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% 코드 적용 범위를 달성하는 테스트 모음이 있지만 테스트가 모두 무의미한 경우, 코드가 잘 테스트되었다는 잘못된 보안 인식을 얻을 수 있습니다. 실수로 애플리케이션 코드의 일부를 삭제하거나 중단하면 애플리케이션이 더 이상 제대로 작동하지 않더라도 테스트는 계속 통과됩니다.

이 시나리오를 피하려면 다음 단계를 따르세요.

  • 테스트 검토. 테스트가 의미가 있는지 확인하고 테스트를 작성하고 검토하여 다양한 시나리오에서 코드를 테스트합니다.
  • 테스트 효과나 코드 품질을 측정하는 유일한 기준이 아닌 코드 적용 범위를 가이드라인으로 사용하세요.

다양한 유형의 테스트에서 코드 적용 범위 사용

세 가지 일반적인 테스트 유형으로 코드 적용 범위를 사용하는 방법을 자세히 살펴보겠습니다.

  • 단위 테스트: 여러 개의 소규모 시나리오와 테스트 경로를 다루도록 설계되었기 때문에 코드 적용 범위를 수집하는 데 가장 적합한 테스트 유형입니다.
  • 통합 테스트: 통합 테스트를 위한 코드 적용 범위를 수집하는 데 도움이 될 수 있지만 주의해서 사용합니다. 이 경우 소스 코드의 더 큰 부분의 적용 범위를 계산하므로 어떤 테스트가 실제로 코드의 어떤 부분을 다루는지 확인하기 어려울 수 있습니다. 그럼에도 불구하고 유닛이 잘 격리되지 않은 기존 시스템에는 통합 테스트의 코드 적용 범위를 계산하는 것이 유용할 수 있습니다.
  • 엔드 투 엔드 (E2E) 테스트. 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({});
  });
});