숫자가 아무 의미가 없다면 단위 테스트에서 마술 숫자를 사용할 수 있습니까?


59

단위 테스트에서 종종 코드에서 임의의 값을 던져 그것이 무엇을하는지 확인합니다. 예를 들어 foo(1, 2, 3)17을 반환한다고 가정하면 다음과 같이 작성할 수 있습니다.

assertEqual(foo(1, 2, 3), 17)

이 숫자는 순전히 임의적이며 더 넓은 의미는 없습니다 (예를 들어 경계 조건은 아니지만 테스트합니다). 나는이 숫자들에 대한 좋은 이름을 내기 위해 고군분투 할 것이고, 같은 것을 쓰는 const int TWO = 2;것은 분명히 도움이되지 않습니다. 이와 같은 테스트를 작성해도 괜찮 습니까? 아니면 숫자를 상수로 계산해야합니까?

에서 모든 매직 넘버가 동일하게 만들어 집니까? 문맥에서 의미가 분명하다면 마법의 숫자는 괜찮다는 것을 알았지 만,이 경우 숫자는 실제로 의미가 없습니다.


9
값을 넣고 동일한 값을 다시 읽을 수 있기를 기대한다면 마법의 숫자는 괜찮습니다. 예를 들어, 1, 2, 3이전에 값을 저장 한 3D 배열 인덱스 인 17경우이 테스트는 멋질 것이라고 생각합니다 (부정적 인 테스트가있는 한). 그러나 계산 결과 인 경우,이 테스트를 읽는 사람은 왜 foo(1, 2, 3)되어야 하는지 이해해야 17하며 마법 숫자는 아마도 그 목표를 달성하지 못할 것입니다.
Joe White

24
const int TWO = 2;를 사용하는 것보다 더 나쁩니다 2. 그것은 규칙의 정신을 어기려는 의도로 규칙의 표현에 부합합니다.
Agent_L

4
"무의미하지 않은"숫자는 무엇입니까? 아무 의미가 없다면 왜 코드에 포함됩니까?
Tim Grant

6
확실한. "수동으로 결정된 작은 예제"와 같은 일련의 테스트 전에 의견을 남겨주십시오. 이것은 경계와 예외를 명확하게 테스트하는 다른 테스트와 관련하여 명확합니다.
davidbak 2016 년

5
귀하의 예는 오도의 소지가 있습니다. 함수 이름이 실제로 foo이면 아무 의미도 없으며 매개 변수입니다. 그러나 실제로, 나는 함수가 그 이름이없는 확신하고, 매개 변수 이름이없는 bar1, bar2하고 bar3. 이름 에 의미 있는 좀 더 현실적인 예를 만들고 테스트 데이터 값에도 이름이 필요한지 논의하는 것이 훨씬 더 합리적입니다.
Doc Brown

답변:


81

실제로 의미가없는 숫자는 언제입니까?

일반적으로 숫자에 의미가있는 경우 코드를 더 읽기 쉽고 설명하기 쉽도록 테스트 방법의 로컬 변수에 할당해야합니다. 변수의 이름은 최소한 변수가 의미하는 바를 반영해야하며 반드시 그 값은 아닙니다.

예:

const int startBalance = 10000;
const float interestRate = 0.05f;
const int years = 5;

const int expectedEndBalance = 12840;

assertEqual(calculateCompoundInterest(startBalance, interestRate, years),
            expectedEndBalance);

첫 번째 변수의 이름은되지 않습니다 HUNDRED_DOLLARS_ZERO_CENT만, startBalance그 값이 특별한 어떤 식 으로든이라는 변수의 의미이다하지만 무엇을 나타내는 데.


3
@Kevin-어떤 언어로 테스트하고 있습니까? 일부 테스트 프레임 워크를 사용하면 테스트를 위해 일련의 값 배열을 반환하는 데이터 공급자를 설정할 수 있습니다.
HorusKol

10
나는이 생각에 동의하지만, 실수로 같은 값을 추출하는 경우처럼 이러한 행위 역시 새로운 오류가 발생할 수 있음을주의 0.05f에를 int. :)
Jeff Bowman 2016 년

5
+1-좋은 것들. 특정 값이 무엇인지 상관하지 않는다고해서 이것이 여전히 마법의 숫자가 아님을 의미하지는 않습니다.
Robbie Dee

2
@PieterB : AFAIK는 const변수 개념을 공식화 한 C 및 C ++의 결함입니다 .
Steve Jessop 2016 년

2
calculateCompoundInterest? 의 명명 된 매개 변수와 동일한 변수 이름을 지정 했습니까 ? 그렇다면 추가 타이핑은 테스트중인 기능에 대한 설명서를 읽었거나 IDE에서 제공 한 이름을 최소한 복사 한 작업 증명입니다. 이것이 코드의 의도에 대해 독자에게 얼마나 많은 정보를 제공하는지 잘 모르겠지만 매개 변수를 잘못된 순서로 전달하면 적어도 의도 한 내용을 알 수 있습니다.
Steve Jessop 2016 년

20

임의의 숫자를 사용하여 수행하는 작업을 확인하는 경우 무작위로 생성 된 테스트 데이터 또는 속성 기반 테스트 일 것입니다.

예를 들어, 가설 은 이러한 종류의 테스트를위한 멋진 Python 라이브러리이며 QuickCheck를 기반으로 합니다 .

일반 단위 테스트는 다음과 같은 것으로 생각하십시오.

  1. 일부 데이터를 설정하십시오.
  2. 데이터에 대한 일부 작업을 수행하십시오.
  3. 결과에 대해 무언가를 주장하십시오.

가설을 사용하면 대신 다음과 같은 테스트를 작성할 수 있습니다.

  1. 일부 사양과 일치하는 모든 데이터
  2. 데이터에 대한 일부 작업을 수행하십시오.
  3. 결과에 대해 무언가를 주장하십시오.

아이디어는 자신을 자신의 값으로 제한하는 것이 아니라 함수가 사양과 일치하는지 확인하는 데 사용할 수있는 임의의 값을 선택하는 것입니다. 중요한 점으로, 이러한 시스템은 일반적으로 실패한 입력을 기억 한 다음 해당 입력이 향후에 항상 테스트되도록합니다.

포인트 3은 일부 사람들에게 혼란을 줄 수 있으므로 명확하게 설명하겠습니다. 그것은 당신이 정확한 답을 주장한다는 것을 의미하지는 않습니다-이것은 임의의 입력에 대해 분명히 불가능합니다. 대신 결과 의 속성 에 대해 무언가를 주장 하십시오. 예를 들어, 목록에 무언가를 추가 한 후에는 비어 있지 않거나 자체 균형 이진 검색 트리가 실제로 균형을 이루도록 (특정 데이터 구조의 기준을 사용하여) 주장 할 수 있습니다.

전반적으로, 임의의 숫자를 직접 선택하는 것은 아마도 매우 나쁠 것입니다. 그것은 실제로 많은 가치를 추가하지는 않으며 그것을 읽는 다른 사람에게는 혼란 스럽습니다. 임의의 테스트 데이터를 자동으로 생성하고 효과적으로 사용하는 것이 좋습니다. 선택한 언어에 대한 가설 또는 QuickCheck와 유사한 라이브러리를 찾는 것이 다른 사람이 이해할 수있는 상태에서 목표를 달성하는 더 좋은 방법 일 것입니다.


11
무작위 테스트는 재현하기 어려운 버그를 찾을 수 있지만 무작위 테스트는 재현 가능한 버그를 거의 찾지 않습니다. 재현 가능한 특정 테스트 사례로 테스트 실패를 캡처해야합니다.
JBR 윌킨슨

5
그리고 "결과에 대해 무언가를 주장 할 때"(이 경우 foo컴퓨팅이란 무엇인가를 재 계산할 때) 단위 테스트에 버그가 없다는 것을 어떻게 알 수 있습니까? 코드가 100 % 올바른 답을 제공한다고 확신했다면 해당 코드를 프로그램에 넣고 테스트하지 마십시오. 그렇지 않다면 테스트를 테스트해야하며 모든 사람들이 이것이 어디로 가고 있는지 알 것입니다.

2
예, 임의의 입력을 함수에 전달하면 출력이 올바르게 작동한다고 주장 할 수있는 것이 무엇인지 알아야합니다. 고정 / 선택된 테스트 값을 사용하면 물론 수작업으로 해결할 수 있지만 결과가 올바른지 여부를 자동으로 결정하는 방법은 테스트하는 기능과 동일한 문제가 발생합니다. 당신이 가지고있는 구현을 사용하거나 (작동하는지 테스트하기 때문에 불가능합니다) 버그가있을 가능성이있는 새로운 구현을 작성하거나 더 정확할 가능성이 높은 새로운 구현을 작성합니다 ).
Chris

7
@NajibIdrissi-반드시 그런 것은 아닙니다. 예를 들어 테스트중인 작업의 역수를 결과에 적용하면 시작한 초기 값이 다시 제공되는지 테스트 할 수 있습니다. 또는 예상 불변량을 테스트 할 수 있습니다 (예 : d일의 모든이자 계산 , d일 + 1 개월 의 계산 은 알려진 월별 비율이 높아야 함)
Jules

12
@Chris-많은 경우에 결과를 확인하는 것이 결과를 생성하는 것보다 정확합니다. 이것이 모든 상황에 해당되는 것은 아니지만 , 많은 곳이 있습니다. 예 : 균형 이진 트리에 항목을 추가하면 균형이 잡힌 새 트리가 만들어집니다. 테스트하기 쉽고 실제로 구현하기가 까다 롭습니다.
Jules

11

단위 테스트 이름은 대부분의 컨텍스트를 제공해야합니다. 상수 값이 아닙니다. 테스트의 이름 / 문서는 테스트 내에 존재하는 매직 넘버에 대한 적절한 컨텍스트와 설명을 제공해야합니다.

그것이 충분하지 않으면, 약간의 문서가 (변수 이름 또는 문서 문자열을 통해) 그것을 제공 할 수 있어야합니다. 함수 자체에는 의미있는 이름을 가진 매개 변수가 있습니다. 인수로 이름을 지정하기 위해 테스트에 복사하는 것은 무의미합니다.

마지막으로, 단위 테스트가 복잡하고 실용적이지 않을 정도로 복잡하면 함수가 너무 복잡하고 왜 그런지 고려할 수 있습니다.

테스트를 더 많이 작성하면할수록 실제 코드는 더 나빠집니다. 테스트를 명확하게하기 위해 테스트 값의 이름을 지정할 필요가 있다고 생각되면 실제 방법에 더 나은 이름 지정 및 / 또는 문서화가 필요하다는 것이 강력히 제안됩니다. 테스트에서 상수의 이름을 지정할 필요가 있다면 왜 이것이 필요한지 살펴볼 것입니다. 문제는 테스트 자체가 아니라 구현 일 것입니다.


이 답변은 테스트의 목적을 유추하는 데 어려움이있는 것으로 보이지만 실제 질문은 메소드 매개 변수의 마법 수에 관한 것입니다.
Robbie Dee

@Robbie 디 시험의 이름 / 문서는 시험에 존재하는 매직 넘버에 대한 적절한 맥락과 설명을 제공해야합니다. 그렇지 않은 경우 문서를 추가하거나 테스트 이름을 더 명확하게 변경하십시오.
enderland

매직 넘버에 이름을 붙이는 것이 여전히 낫습니다. 매개 변수 수가 변경되면 문서화가 오래 될 위험이 있습니다.
로비 디

1
@RobbieDee는 함수 자체에 의미있는 이름을 가진 매개 변수가 있음을 명심하십시오. 인수로 이름을 지정하기 위해 테스트에 복사하는 것은 의미가 없습니다.
enderland

"희망"응? 왜 필립이 이미 설명했듯이 일을 올바르게 코딩하고 표면 상으로 마법의 숫자 인 것을 없애지 않겠습니까?
Robbie Dee

9

이것은 테스트하는 기능에 따라 크게 다릅니다. 나는 개별 숫자가 그 자체로 특별한 의미를 갖지 않는 많은 경우를 알고 있지만 테스트 케이스 전체는 신중하게 구성되어 있으므로 특별한 의미가 있습니다. 그것은 어떤 방식으로 문서화해야하는 것입니다. 예를 들어, foo실제로 testForTriangle세 숫자가 삼각형 가장자리의 유효한 길이인지를 결정 하는 방법 인 경우 테스트는 다음과 같습니다.

// standard triangle with area >0
assertEqual(testForTriangle(2, 3, 4), true);

// degenerated triangle, length of two edges match the length of the third
assertEqual(testForTriangle(1, 2, 3), true);  

// no triangle
assertEqual(testForTriangle(1, 2, 4), false); 

// two sides equal
assertEqual(testForTriangle(2, 2, 3), true);

// all three sides equal
assertEqual(testForTriangle(4, 4, 4), true);

// degenerated triangle / point
assertEqual(testForTriangle(0, 0, 0), true);  

등등. 이를 개선하고 주석을 assertEqual테스트 실패시 표시되는 메시지 매개 변수로 설정할 수 있습니다. 그런 다음이를 개선하고이를 데이터 기반 테스트로 리팩토링 할 수 있습니다 (테스트 프레임 워크에서 지원하는 경우). 그럼에도 불구하고 코드에이 숫자를 선택한 이유와 개별 사례로 테스트하는 다양한 동작 중 하나를 메모하면 코드를 작성하는 것이 좋습니다.

물론 다른 기능의 경우 매개 변수의 개별 값이 더 중요 할 수 있으므로 매개 변수의 의미 foo를 처리하는 방법을 요청할 때 와 같이 의미없는 기능 이름을 사용하는 것이 가장 좋은 아이디어는 아닙니다.


현명한 솔루션.
user1725145

6

숫자 대신 명명 된 상수를 사용하려는 이유는 무엇입니까?

  1. DRY-3 곳의 값이 필요한 경우 한 번만 정의하고 싶기 때문에 한 곳에서 변경할 수 있습니다.
  2. 숫자에 의미를 부여하십시오.

각각 3 개의 숫자 (startBalance, interest, years)로 여러 단위 테스트를 작성하면 값을 단위 테스트에 로컬 변수로 묶습니다. 그들이 속한 가장 작은 범위.

testBigInterest()
  var startBalance = 10;
  var interestInPercent = 100
  var years = 2
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 40 )

testSmallInterest()
  var startBalance = 50;
  var interestInPercent = .5
  var years = 1
  assert( calcCreditSum( startBalance, interestInPercent, years ) == 50.25 )

명명 된 매개 변수를 허용하는 언어를 사용하는 경우 이는 물론 수퍼 플로어 스입니다. 거기에서 메소드 호출에 원시 값을 압축합니다. 이 진술을 더 간결하게 만드는 리팩토링을 상상할 수 없습니다.

testBigInterest()
  assert( calcCreditSum( startBalance:       10
                        ,interestInPercent: 100
                        ,years:               2 ) = 40 )

또는 테스트 프레임 워크를 사용하면 테스트 케이스를 일부 배열 또는 맵 형식으로 정의 할 수 있습니다.

testcases = { {
                Name: "BigInterest"
               ,StartBalance:       10
               ,InterestInPercent: 100
               ,Years:               2
              }
             ,{ 
                Name: "SmallInterest"
               ,StartBalance:       50
               ,InterestInPercent:  .5
               ,Years:               1
              }
            }

3

...하지만이 경우 숫자는 실제로 의미가 없습니다.

숫자는 메소드를 호출하는 데 사용되므로 위의 전제는 정확하지 않습니다. 당신은하지 않을 수 있습니다 관심이 숫자가 무엇인지하지만이 점 옆에있다. 예, 일부 IDE 마법사가 사용하는 숫자를 유추 할 수 있지만 매개 변수와 일치하더라도 값 이름 만 지정하면 훨씬 좋습니다.


1
필자가 쓴 가장 최근의 단위 테스트의 예에서와 같이 반드시 그런 것은 아닙니다 assertEqual "Returned value" (makeKindInt 42) (runTest "lvalue_operators"). 이 예제에서는 42테스트 스크립트의 코드에서 생성 된 스크립트에 lvalue_operators의해 리턴 될 때 점검 되는 플레이스 홀더 값입니다 . 동일한 값이 두 곳에서 발생한다는 것 외에는 전혀 의미가 없습니다. 여기서 실제로 유용한 의미를 부여하는 적절한 이름은 무엇입니까?
Jules

3

경계 조건이 아닌 하나의 입력 세트 에서 순수한 함수 를 테스트하려면 경계 조건 이 아닌 (그리고있는) 입력 세트 전체 에서 함수 를 테스트하려고합니다 . 그리고 나에게 그는이 있어야 의미 테이블 과 기능, 루프를 호출하는 값 :

struct test_foo_values {
    int bar;
    int baz;
    int blurf;
    int expected;
};
const struct test_foo_values test_foo_with[] = {
   { 1, 2, 3, 17 },
   { 2, 4, 9, 34 },
   // ... many more here ...
};

for (size_t i = 0; i < ARRAY_SIZE(test_foo_with); i++) {
    const struct test_foo_values *c = test_foo_with[i];
    assertEqual(foo(c->bar, c->baz, c->blurf), c->expected);
}

Dannnno의 답변 에서 제안 된 것과 같은 도구 를 사용하면 테스트 할 값 테이블을 구성 할 수 있습니다. bar, baz,과 blurf에서 논의 된 바와 같이 의미있는 이름으로 대체해야 필립의 대답 .

(여기서 논증 할 수있는 일반적인 원칙 : 숫자는 항상 이름을 필요로하는 "마법의 숫자"가 아니라 숫자가 data 일 수 있습니다 . 숫자를 배열, 아마도 레코드 배열에 넣는 것이 합리적이라면 아마도 데이터 일 것입니다 반대로 손에 데이터가 있다고 생각되면 배열에 넣고 더 많은 데이터를 수집하는 것이 좋습니다.)


1

테스트는 프로덕션 코드와 다르며 적어도 Spock으로 작성된 단위 테스트에서는 짧고 요점은 마술 상수를 사용하는 데 아무런 문제가 없습니다.

테스트 길이가 5 줄이고 기본 주어진 / 언제 / 그런 방식을 따르는 경우, 이러한 값을 상수로 추출하면 코드를 더 길고 읽기 어려울 수 있습니다. 논리가 "Smith라는 사용자를 추가 할 때 사용자 Smith가 사용자 목록에 반환 된 것을 볼 수 있습니다"이면 상수에 "Smith"를 추출 할 필요가 없습니다.

이것은 "given"(설정) 블록에 사용 된 값을 "when"및 "then"블록에있는 값과 쉽게 일치시킬 수있는 경우에 적용됩니다. 테스트 설정이 데이터가 사용되는 장소와 (코드로) 분리 된 경우 상수를 사용하는 것이 더 좋을 수 있습니다. 그러나 테스트는 가장 독립적으로 이루어지기 때문에 설정은 일반적으로 사용 장소에 가깝고 첫 번째 경우가 적용됩니다.이 경우 마술 상수가 상당히 수용 가능합니다.


1

먼저,“단위 테스트”는 프로그래머가 작성하는 모든 자동화 된 테스트를 다루는 데 종종 사용되며 각 테스트가 무엇을 호출해야하는지에 대해 토론하는 것은 무의미하다는 점에 동의합시다.

나는 소프트웨어가 많은 입력을받는 시스템에서 일했고 다른 수치를 최적화하면서 제약을 충족시켜야하는 "솔루션"을 만들었습니다. 정답은 없었으므로 소프트웨어는 합리적인 대답을해야만했습니다.

많은 난수를 사용하여 시작점을 얻은 다음 "힐 클라이머"를 사용하여 결과를 개선했습니다. 이것은 여러 번 실행되어 최상의 결과를 얻었습니다. 난수 생성기는 시드 할 수 있으므로 항상 동일한 순서로 동일한 번호를 제공하므로 테스트에서 시드를 설정하면 각 실행에서 결과가 동일하다는 것을 알고 있습니다.

우리는 위의 테스트를 많이 수행했으며 결과가 동일한 지 확인했습니다 . 이는 리팩토링 등으로 시스템의 일부가 실수로 수행 한 작업을 변경 하지 않았다고 알려줍니다. 시스템의 해당 부분이 수행 한 작업

최적화 코드를 변경하면 테스트가 중단 될 수 있기 때문에 이러한 테스트는 유지 관리 비용이 많이 들지만 데이터를 사전 처리하고 결과를 후 처리하는 훨씬 더 큰 코드에서 버그가 발견되었습니다.

데이터베이스를 "모의"했으므로 이러한 테스트를 "단위 테스트"라고 할 수 있지만 "단위"는 다소 컸습니다.

테스트가없는 시스템에서 작업 할 때 리팩토링이 출력을 변경하지 않는지 확인할 수 있도록 위와 같은 작업을 수행하는 경우가 종종 있습니다. 새 코드에 대해 더 나은 테스트가 작성되기를 바랍니다.


1

이 경우 숫자는 매직 넘버가 아닌 임의의 숫자로 불리고 라인을 "임의 테스트 사례"라고합니다.

물론, 일부 매직 넘버는 고유 한 "핸들"값 (물론 명명 된 상수로 대체되어야 함)과 마찬가지로 임의적 일 수 있지만 "2 주당 펄롱 단위의 유럽식 참새의 대기 속도"와 같이 미리 계산 된 상수 일 수도 있습니다. 여기서 주석이나 유용한 컨텍스트없이 숫자 값이 연결됩니다.


0

나는 명확한 예 / 아니오라고 말하지는 않겠지 만, 다음은 그것이 좋은지 아닌지를 결정할 때 스스로에게 물어봐야 할 몇 가지 질문입니다.

  1. 숫자가 아무 의미가 없다면, 왜 처음에 있습니까? 그것들을 다른 것으로 대체 할 수 있습니까? 값 어설 션 대신 메서드 호출 및 흐름을 기반으로 확인을 수행 할 수 있습니까? verify()실제로 값을 주장하는 대신 객체를 조롱하기 위해 특정 메소드 호출이 수행되었는지 여부를 확인하는 Mockito의 메소드 와 같은 것을 고려하십시오 .

  2. 숫자 의미가있는 경우 적절하게 명명 된 변수에 지정해야합니다.

  3. 특정 상황에서는 도움이 될 수 있고 다른 상황에서는 그다지 도움이되지 않는 숫자 2를 쓰십시오 TWO.

    • 예를 들어 : assertEquals(TWO, half_of(FOUR))코드를 읽는 사람에게 의미가 있습니다. 무엇 을 테스트 하고 있는지 즉시 알 있습니다.
    • 테스트는하지만 경우 assertEquals(numCustomersInBank(BANK_1), TWO), 다음이하지 않는 것을 많은 의미. 않는 BANK_1두 고객을 포함? 무엇 을 테스트하고 있습니까?
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.