[unit-testing] 개인용 메소드를 테스트해야합니까? [닫은]

개인 메소드를 테스트하는 방법에 대한 이 게시물 을 읽었습니다 . 나는 항상 객체 외부에서 호출되는 공용 메소드 만 테스트하는 것이 더 빠르다고 생각하기 때문에 테스트하지 않습니다. 개인 메소드를 테스트합니까? 항상 테스트해야합니까?



답변

개인 메소드를 단위 테스트하지 않습니다. 개인용 메소드는 클래스 사용자에게 숨겨져 야하는 구현 세부 사항입니다. 전용 메서드를 테스트하면 캡슐화가 중단됩니다.

개인 메소드가 거대하거나 복잡하거나 자체 테스트가 필요할 정도로 중요하다는 것을 알게되면 다른 클래스에 넣고 공개합니다 ( Method Object ). 그런 다음 이제는 자체 클래스에 존재하는 이전의 개인용이지만 현재 공용 인 방법을 쉽게 테스트 할 수 있습니다.


답변

테스트의 목적은 무엇입니까?

지금까지 답변의 대부분은 개인 메소드가 퍼블릭 인터페이스가 잘 테스트되고 작동하는 한 중요하지 않은 구현 세부 사항이라고 말합니다. 테스트의 유일한 목적이 공용 인터페이스가 작동하도록 보장하는 것이라면 이것이 맞습니다 .

개인적으로, 코드 테스트의 주된 용도는 향후 코드 변경으로 인해 문제가 발생하지 않도록하고 문제가있는 경우 디버깅 작업을 지원하는 것입니다. 공개 인터페이스 (더 이상 그렇지 않다면!)가 철저히 목적을 달성하는 것처럼 개인 메소드를 테스트하는 것이 그 목적을 달성한다는 것을 알았습니다.

다음을 고려하십시오. 개인 메소드 B를 호출하는 공용 메소드 A가 있습니다. A와 B는 모두 메소드 C를 사용합니다. C는 변경 될 수 있으며 (아마도 공급 업체에 의해) 벤더가 테스트 실패를 시작합니다. 문제가 A의 C 사용, B의 C 사용 또는 두 가지 모두에 있는지 알 수 있도록 비공개 테스트를 수행해도 B에 대한 테스트를하는 것이 유용하지 않습니까?

공용 메서드의 테스트 범위가 불완전한 경우 개인 메서드를 테스트해도 가치가 높아집니다. 이것이 일반적으로 피하고자하는 상황이지만 효율성 단위 테스트는 버그를 찾는 테스트와 해당 테스트의 개발 및 유지 관리 비용에 따라 달라집니다. 경우에 따라 100 % 테스트 범위의 이점이 해당 테스트 비용을 보증하기에 충분하지 않은 것으로 판단되어 공용 인터페이스의 테스트 범위에 차이가 생길 수 있습니다. 그러한 경우, 개인 메소드에 대한 목표가 명확한 테스트는 코드 기반에 매우 효과적 일 수 있습니다.


답변

나는 그들의 책 Pragmatic Unit Testing 에서 Dave Thomas와 Andy Hunt의 조언을 따르는 경향이 있습니다 .

일반적으로 테스트 목적으로 캡슐화를 중단하고 싶지 않습니다 (또는 엄마가 “개인을 노출시키지 마십시오!”라고 말한 것처럼). 대부분의 경우 공용 메소드를 사용하여 클래스를 테스트 할 수 있어야합니다. 개인 또는 보호 된 액세스 뒤에 숨겨져있는 중요한 기능이있는 경우, 다른 클래스가 나 가려고 애 쓰고 있다는 경고 신호일 수 있습니다.

그러나 때로는 개인적인 방법을 테스트하는 것을 막을 수 없습니다 . 완전히 강력한 프로그램을 작성 하고 있다는 확신을 줄 수 있기 때문 입니다.


답변

프로젝트에서 최신 QA 권장 사항을 점점 더 많이 따르면서 개인 기능을 테스트해야한다고 생각합니다.

함수 당 순환 복잡성 이 10 개 이하 입니다.

이제이 정책을 시행 할 때의 부작용은 매우 큰 공공 기능 중 다수가 더 집중되고 이름이 잘 지정된 개인 기능 으로 나뉘어져 있다는 것입니다 .
공공 기능은 여전히 ​​(물론) 있지만 본질적으로 모든 개인 ‘하위 기능’을 호출하도록 축소되었습니다.

콜 스택을 읽기가 훨씬 쉽기 때문에 실제로 멋지다. ‘어떻게 도착했는지’)

그러나 이제는 이러한 개인 기능을 직접 단위 테스트하는 것이 더 쉬워 보이고 대규모 공용 기능의 테스트는 시나리오를 해결해야하는 일종의 ‘통합’테스트로 남겨 둡니다.

그냥 내 2 센트.


답변

예, 개인 함수는 공개 메소드로 테스트되었지만 TDD (Test Driven Design)에서는 응용 프로그램의 가장 작은 부분을 테스트하는 것이 좋기 때문에 개인 함수를 테스트합니다. 그러나 테스트 단위 클래스에있을 때는 개인 기능에 액세스 할 수 없습니다. 다음은 개인 메소드를 테스트하기 위해 수행하는 작업입니다.

왜 우리는 개인적인 방법을 가지고 있습니까?

개인 함수는 주로 공개 메소드에서 읽을 수있는 코드를 작성하려고하기 때문에 클래스에 존재합니다. 우리는이 클래스의 사용자가 이러한 메소드를 직접 호출하는 것이 아니라 공용 메소드를 통해 호출하기를 원하지 않습니다. 또한 클래스를 확장 할 때 (보호 된 경우) 행동을 변경하지 않기를 원하므로 비공개입니다.

코딩 할 때는 TDD (Test-driven-design)를 사용합니다. 이것은 때때로 우리가 개인적인 기능을 테스트하고 싶어한다는 것을 의미합니다. 비공개 함수는 phpUnit에서 테스트 할 수 없습니다. Test 클래스에서는 비공개 함수이기 때문에 액세스 할 수 없기 때문입니다.

우리는 여기에 3 가지 해결책이 있다고 생각합니다.

1. 공개 방법을 통해 개인을 테스트 할 수 있습니다

장점

  • 간단한 단위 테스트 ( ‘해킹’필요 없음)

단점

  • 프로그래머는 공개 메소드를 이해해야하지만 개인 메소드 만 테스트하려고합니다.
  • 응용 프로그램의 테스트 가능한 가장 작은 부분을 테스트하지 않습니다

2. 개인이 매우 중요한 경우, 별도의 새 클래스를 만드는 것이 코드 스멜 일 수 있습니다.

장점

  • 이 클래스를 새로운 클래스로 리팩토링 할 수 있습니다. 중요한 경우 다른 클래스도이를 필요로하기 때문에
  • 테스트 가능한 단위는 이제 공개 방법이므로 테스트 가능

단점

  • 클래스가 필요하지 않은 경우 클래스를 작성하고 싶지 않으며 메소드가 시작된 클래스에서만 사용됩니다.
  • 추가 된 오버 헤드로 인한 잠재적 성능 손실

3. 액세스 수정자를 (최종) 보호로 변경하십시오.

장점

  • 응용 프로그램에서 테스트 가능한 가장 작은 부분을 테스트하고 있습니다. 최종 보호 기능을 사용하는 경우이 기능은 무시할 수 없습니다 (개인과 동일).
  • 성능 손실 없음
  • 추가 오버 헤드 없음

단점

  • 보호 된 개인 액세스 권한을 변경하고 있습니다. 이는 자녀가 액세스 할 수 있음을 의미합니다.
  • 테스트 클래스에는 여전히 Mock 클래스가 필요합니다.

class Detective {
  public function investigate() {}
  private function sleepWithSuspect($suspect) {}
}
Altered version:
class Detective {
  public function investigate() {}
  final protected function sleepWithSuspect($suspect) {}
}
In Test class:
class Mock_Detective extends Detective {

  public test_sleepWithSuspect($suspect)
  {
    //this is now accessible, but still not overridable!
    $this->sleepWithSuspect($suspect);
  }
}

따라서 테스트 단위는 이제 test_sleepWithSuspect를 호출하여 이전 개인 함수를 테스트 할 수 있습니다.


답변

몇 가지 이유로 개인 기능 테스트를 좋아하지 않습니다. 다음과 같습니다 (TLDR 사람들의 주요 요점).

  1. 일반적으로 클래스의 개인 메서드를 테스트하려는 경우 디자인 냄새입니다.
  2. 공용 인터페이스를 통해 테스트 할 수 있습니다 (클라이언트가 호출 / 사용하는 방식이므로 테스트하려는 방법). 개인 메소드에 대한 모든 통과 테스트에서 녹색 표시등을 보면서 잘못된 보안 감각을 얻을 수 있습니다. 공용 인터페이스를 통해 개인 기능에 대한 최첨단 사례를 테스트하는 것이 훨씬 좋습니다.
  3. 개인 방법을 테스트하면 심각한 테스트 복제 (유사하게 보이는 느낌)가 발생할 수 있습니다. 필요한 것보다 많은 테스트가 중단 될 수 있기 때문에 요구 사항이 변경 될 때 중요한 결과를 초래합니다. 또한 테스트 스위트로 인해 리팩토링하기 어려운 위치에 배치 할 수 있습니다. 테스트 스위트는 안전하게 재 설계하고 리팩토링 할 수 있도록 도와주기 때문에 가장 아이러니합니다.

구체적인 예를 통해 이들 각각을 설명하겠습니다. 2)와 3)은 다소 복잡하게 연결되어 있으므로 개인 예제를 테스트해서는 안되는 별도의 이유를 고려하지만 예제는 비슷합니다.

개인 메소드를 테스트하는 것이 적절한 경우가 있습니다. 위에 나열된 단점을 인식하는 것이 중요합니다. 나중에 자세히 설명하겠습니다.

또한 TDD가 결국 개인 메소드를 테스트하는 데 유효한 변명이 아닌 이유에 대해서도 설명합니다.

나쁜 디자인에서 길을 리팩토링

내가 본 가장 일반적인 (반) 패터 중 하나는 Michael Feathers“Iceberg”클래스라고 부르는 것입니다 (Michael Feathers가 누구인지 모르는 경우 “레거시 코드로 효과적으로 작업하기”). 전문 소프트웨어 엔지니어 / 개발자인지 아는 사람). 이 문제를 일으키는 다른 (반) 패턴이 있지만, 이것이 내가 우연히 발견 한 가장 일반적인 패턴입니다. “Iceberg”클래스에는 하나의 공용 메소드가 있고 나머지는 개인 메소드입니다 (따라서 개인 메소드를 테스트하려는 이유가 있습니다). 일반적으로 고독한 공용 메소드가 있기 때문에 “Iceberg”클래스라고하지만 나머지 기능은 전용 메소드의 형태로 수중에 숨겨져 있습니다.

규칙 평가자

예를 들어, GetNextToken()문자열을 연속적으로 호출하여 예상 결과를 반환하는지 확인 하여 테스트 할 수 있습니다. 이와 같은 함수는 테스트를 보증합니다. 특히 토큰 화 규칙이 복잡한 경우에는 그 동작이 쉽지 않습니다. 그것이 그렇게 복잡한 것은 아니라고 가정하고 공간으로 구분 된 토큰을 묶고 싶습니다. 따라서 테스트를 작성하면 다음과 같이 보일 수 있습니다 (일부 언어에 관계없는 의사 코드, 아이디어는 분명합니다).

TEST_THAT(RuleEvaluator, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    re = RuleEvaluator(input_string);

    ASSERT re.GetNextToken() IS "1";
    ASSERT re.GetNextToken() IS "2";
    ASSERT re.GetNextToken() IS "test";
    ASSERT re.GetNextToken() IS "bar";
    ASSERT re.HasMoreTokens() IS FALSE;
}

글쎄, 그것은 실제로 꽤 좋아 보인다. 변경시이 동작을 유지하고 싶습니다. 그러나 GetNextToken()A는 개인 기능! 그래서 우리는 컴파일조차 할 수 없기 때문에 이것을 테스트 할 수 없습니다 (파이썬과 같은 스크립팅 언어와는 달리 실제로 공개 / 개인을 강제하는 언어를 사용한다고 가정). 그러나 RuleEvaluator단일 책임 원칙 (Single Responsibility Principle)을 따르도록 수업을 바꾸는 것은 어떻습니까? 예를 들어, 파서, 토크 나이저 및 평가자가 하나의 클래스에 걸린 것 같습니다. 그러한 책임을 분리하는 것이 낫지 않습니까? 게다가 Tokenizer클래스 를 만들면 공개 메소드는 HasMoreTokens()and GetNextTokens()입니다. RuleEvaluator클래스는있을 수 있습니다Tokenizer멤버로서의 객체. 이제 Tokenizer클래스 대신 클래스를 테스트하는 것을 제외하고는 위와 동일한 테스트를 유지할 수 있습니다 RuleEvaluator.

UML에서 다음과 같이 보일 수 있습니다.

규칙 평가자가 리팩토링

이 새로운 디자인은 모듈성을 향상 시키므로 잠재적으로 시스템의 다른 부분에서 이러한 클래스를 재사용 할 수 있습니다 (할 수 없었기 전에 개인 메소드는 정의에 의해 재사용 할 수 없음). 이는 이해도 / 지역성을 높이고 RuleEvaluator를 분류 할 때의 주요 이점입니다.

GetNextToken()메소드는 Tokenizer클래스에서 공개 되기 때문에 이번에는 실제로 컴파일한다는 점을 제외하면 테스트는 매우 유사하게 보입니다 .

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS FALSE;
}

공용 인터페이스를 통해 개인 구성 요소 테스트 및 테스트 복제 방지

당신이 문제를 더 적은 수의 모듈 식 구성 요소로 나눌 수 있다고 생각하지 않더라도 ( 공식적으로 시도 하면 95 % 할 수 있음) 공용 인터페이스를 통해 개인 기능을 테스트 할 수 있습니다. 많은 경우 개인 구성원은 공용 인터페이스를 통해 테스트되므로 테스트 할 가치가 없습니다. 내가 본 것은 매우 유사한 테스트이지만 두 가지 다른 기능 / 방법을 테스트하는 것입니다. 결국 요구 사항이 변경 될 때 (그리고 항상 수행되는 경우) 이제는 1 대신에 2 번의 테스트가 끊어졌습니다. 실제로 모든 개인 방법을 테스트 한 경우 1 아닌 10 번의 테스트가 더 많이 진행될 수 있습니다. , 개인 기능 테스트 (FRIEND_TEST공개 인터페이스를 통해 테스트 할 수있는 공개 또는 리플렉션 사용) 테스트 복제가 발생할 수 있습니다 . 테스트 스위트가 속도를 늦추는 것보다 아무것도 아프지 않기 때문에 당신은 정말로 이것을 원하지 않습니다. 개발 시간을 줄이고 유지 보수 비용을 줄여야합니다! 공용 인터페이스를 통해 테스트 된 개인 메소드를 테스트하는 경우 테스트 스위트는 그 반대의 경우를 잘 수행 할 수 있으며 유지 보수 비용을 적극적으로 증가시키고 개발 시간을 늘릴 수 있습니다. 개인 기능을 공개하거나 유사 FRIEND_TEST및 / 또는 리플렉션 을 사용하는 경우 일반적으로 장기적으로 후회하게됩니다.

Tokenizer클래스 의 다음 가능한 구현을 고려하십시오 .

여기에 이미지 설명을 입력하십시오

하자 말 SplitUpByDelimiter()배열의 각 요소는 토큰이되도록 배열을 반환 할 책임이있다. 또한 GetNextToken()이 벡터에 대한 반복 자라고 가정 해 봅시다 . 따라서 공개 테스트는 다음과 같습니다.

TEST_THAT(Tokenizer, canParseSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);

    ASSERT tokenizer.GetNextToken() IS "1";
    ASSERT tokenizer.GetNextToken() IS "2";
    ASSERT tokenizer.GetNextToken() IS "test";
    ASSERT tokenizer.GetNextToken() IS "bar";
    ASSERT tokenizer.HasMoreTokens() IS false;
}

Michael Feather가 모색 도구 라고 부르는 것처럼 가정 해 봅시다 . 다른 사람의 개인 부품을 만질 수있는 도구입니다. 예를 들어 FRIEND_TESTgoogletest 또는 언어가 지원하는 경우 반영입니다.

TEST_THAT(TokenizerTest, canGenerateSpaceDelimtedTokens)
{
    input_string = "1 2 test bar"
    tokenizer = Tokenizer(input_string);
    result_array = tokenizer.SplitUpByDelimiter(" ");

    ASSERT result.size() IS 4;
    ASSERT result[0] IS "1";
    ASSERT result[1] IS "2";
    ASSERT result[2] IS "test";
    ASSERT result[3] IS "bar";
}

이제 요구 사항이 바뀌고 토큰 화가 훨씬 더 복잡해 진다고 가정 해 봅시다. 간단한 문자열 구분 기호로 충분하지 않다고 결정 Delimiter하고 작업을 처리 하는 클래스가 필요합니다 . 당연히 하나의 테스트가 중단 될 것으로 예상하지만 개인 기능을 테스트하면 통증이 커집니다.

개인 테스트 방법은 언제 적절한가요?

소프트웨어에는 “하나의 크기가 모두 맞는”것은 없습니다. 때때로 “규칙을 어기는 것”(괜찮은)이 좋습니다. 가능한 경우 개인 기능을 테스트하지 않는 것이 좋습니다. 괜찮다고 생각되는 두 가지 주요 상황이 있습니다.

  1. 나는 레거시 시스템 (광활한 Michael Feathers 팬이기 때문에)과 광범위하게 일해 왔으며 때로는 개인 기능을 테스트하는 것이 가장 안전하다고 말할 수 있습니다. “특성 테스트”를 기준으로 가져 오는 데 특히 도움이 될 수 있습니다.

  2. 당신은 서두르고 있으며 지금 여기에서 가능한 가장 빠른 일을해야합니다. 장기적으로는 개인 메소드를 테스트하고 싶지 않습니다. 그러나 설계 문제를 해결하기 위해 리팩토링하는 데 보통 시간이 걸린다고 말하겠습니다. 때로는 일주일 안에 배송해야합니다. 괜찮습니다. 작업을 완료하는 가장 빠르고 안정적인 방법이라고 생각하는 경우 모색 도구를 사용하여 빠르고 더러운 작업을 수행하고 개인 방법을 테스트하십시오. 그러나 당신이 한 일이 장기적으로 차선책이라는 것을 이해하고 다시 돌아 오는 것을 고려하십시오 (또는 잊어 버렸지 만 나중에 볼 경우 수정하십시오).

다른 상황에서는 괜찮을 것입니다. 괜찮다고 생각하고 정당성이 충분하다면 그렇게하십시오. 아무도 당신을 막고 있지 않습니다. 잠재적 인 비용에주의하십시오.

TDD 실례

제쳐두고, 나는 개인 메소드 테스트에 대한 변명으로 TDD를 사용하는 사람들을 정말로 좋아하지 않습니다. 나는 TDD를 연습하지만 TDD가 당신에게 이것을 강요한다고 생각하지 않습니다. 먼저 공용 인터페이스 용 테스트를 작성한 다음 해당 인터페이스를 만족시키는 코드를 작성할 수 있습니다. 때로는 공용 인터페이스에 대한 테스트를 작성하고 하나 또는 두 개의 더 작은 개인용 메소드를 작성하여이를 만족시킬 것입니다 (그러나 개인용 메소드를 직접 테스트하지는 않지만 작동하거나 공개 테스트가 실패한다는 것을 알고 있습니다 ). 해당 개인 메소드의 엣지 케이스를 테스트 해야하는 경우 공용 인터페이스를 통해 테스트를 수행하는 전체 테스트를 작성합니다.엣지 케이스를 치는 방법을 알 수 없다면 이는 각각 고유 한 공개 방법으로 작은 구성 요소를 리팩토링해야한다는 강력한 신호입니다. 개인 함수가 너무 많은 일을하고 클래스의 범위를 벗어났다는 신호 입니다.

또한 때때로 나는 씹기에는 너무 물린 테스트를 작성한다는 것을 알게 되었기 때문에 “나는 API를 더 사용할 때 나중에 다시 테스트로 돌아갈 것”이라고 생각합니다. 의견을 말하고 내 마음 뒤에 보관하십시오.) 이것은 내가 만난 많은 개발자들이 TDD를 희생양으로 사용하여 개인 기능에 대한 테스트를 작성하는 곳입니다. “아, 다른 테스트가 필요하지만이 테스트를 작성하려면 이러한 개인 메소드가 필요합니다. 따라서 테스트를 작성하지 않고는 프로덕션 코드를 작성할 수 없으므로 테스트를 작성해야합니다. 개인적인 방법으로 그러나 그들이 실제로해야 할 일은 현재 클래스에 많은 개인 메소드를 추가 / 테스트하는 대신 더 작고 재사용 가능한 구성 요소로 리팩토링하는 것입니다.

노트 :

얼마 전 GoogleTest를 사용하여 개인 메소드를 테스트 하는 것과 비슷한 질문에 대답했습니다 . 나는 대부분 더 많은 언어에 대해 그 대답을 수정했습니다.

추신 : Michael Feathers의 빙산 강의 및 모색 도구에 대한 관련 강의는 다음과 같습니다. https://www.youtube.com/watch?v=4cVZvoFGJTU


답변

객체의 공개 인터페이스를 테스트하는 것이 가장 좋습니다. 외부 세계의 관점에서 볼 때 공용 인터페이스의 동작 만 중요하며 이는 유닛 테스트를 향한 것입니다.

객체에 대해 작성된 견고한 단위 테스트가 있으면 인터페이스 뒤의 구현이 변경되어 다시 돌아가서 테스트를 변경하지 않아도됩니다. 이 상황에서는 단위 테스트의 일관성을 망쳤습니다.