테스트중인 수업의 일부를 가짜로해도 괜찮습니까?


22

내가 수업을 가지고 있다고 가정 해 봅시다 (생각 된 예와 나쁜 디자인을 용서하십시오).

class MyProfit
{
  public decimal GetNewYorkRevenue();
  public decimal GetNewYorkExpenses();
  public decimal GetNewYorkProfit();

  public decimal GetMiamiRevenue();
  public decimal GetMiamiExpenses();
  public decimal GetMiamiProfit();

  public bool BothCitiesProfitable();

}

(GetxxxRevenue () 및 GetxxxExpenses () 메소드에는 종속 된 종속성이 있음을 참고하십시오)

이제 GetNewYorkProfit () 및 GetMiamiProfit ()에 따라 BothCitiesProfitable ()을 단위 테스트하고 있습니다. GetNewYorkProfit () 및 GetMiamiProfit ()을 스텁해도 괜찮습니까?

그렇지 않으면 BothCitiesProfitable ()과 함께 GetNewYorkProfit () 및 GetMiamiProfit ()을 동시에 테스트하는 것 같습니다. GetxxxProfit () 메서드가 올바른 값을 반환하도록 GetxxxRevenue () 및 GetxxxExpenses ()에 대한 스터 빙을 설정해야합니다.

지금까지는 내부 메소드가 아닌 외부 클래스에 대한 의존성을 스터 빙하는 예제 만 보았습니다.

그리고 그것이 괜찮다면, 이것을하기 위해 사용해야하는 특정 패턴이 있습니까?

최신 정보

나는 우리가 핵심 문제를 잃어 버렸을지도 모른다고 우려하고 있으며 이는 아마도 저의 나쁜 예의 잘못 일 것입니다. 근본적인 질문은 클래스의 메소드가 동일한 클래스에서 공개적으로 노출 된 다른 메소드에 종속되어 있다면 다른 메소드를 스텁 아웃하는 것이 괜찮거나 권장 되는가?입니다.

어쩌면 나는 뭔가를 놓치고 있지만 클래스를 나누는 것이 항상 의미가 있는지 확신 할 수 없습니다. 아마도 가장 좋은 또 다른 예는 다음과 같습니다.

class Person
{
 public string FirstName()
 public string LastName()
 public string FullName()
}

여기서 전체 이름은 다음과 같이 정의됩니다.

public string FullName()
{
  return FirstName() + " " + LastName();
}

FullName ()을 테스트 할 때 FirstName ()과 LastName ()을 스텁해도 괜찮습니까?


일부 코드를 동시에 테스트하면 어떻게 나쁜가요? 뭐가 문제 야? 일반적으로 코드를 더 자주 그리고 더 집중적으로 테스트하는 것이 좋습니다.
사용자가 알 수 없음

그냥 업데이트에 대한 발견, 나는 내 대답 업데이트 한
윈스턴 Ewert

답변:


27

문제의 수업을 해체해야합니다.

각 수업은 간단한 작업을 수행해야합니다. 작업이 테스트하기에 너무 복잡하면 클래스가 수행하는 작업이 너무 큽니다.

이 디자인의 구피를 무시합니다.

class NewYork
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}


class Miami
{
    decimal GetRevenue();
    decimal GetExpenses();
    decimal GetProfit();
}

class MyProfit
{
     MyProfit(NewYork new_york, Miami miami);
     boolean bothProfitable();
}

최신 정보

클래스에서 스텁 메소드의 문제점은 캡슐화를 위반한다는 것입니다. 테스트는 객체의 외부 동작이 사양과 일치하는지 확인해야합니다. 개체 내부에서 일어나는 일은 비즈니스의 대상이 아닙니다.

FullName이 FirstName과 LastName을 사용한다는 사실은 구현 세부 사항입니다. 수업 이외의 어떤 것도 그것이 사실임을 신경 쓰지 않아야합니다. 객체를 테스트하기 위해 퍼블릭 메소드를 조롱함으로써 해당 객체에 대한 가정을하고 있습니다.

미래의 어느 시점에서 그 가정은 정확하지 않을 수 있습니다. 아마도 모든 이름 논리는 Person이 단순히 호출하는 Name 객체로 재배치 될 것입니다. FullName은 FirstName 및 LastName을 호출하는 대신 멤버 변수 first_name 및 last_name에 직접 액세스합니다.

두 번째 질문은 왜 그렇게해야하는지 느끼는 것입니다. 모든 개인 수업은 다음과 같이 테스트 될 수 있습니다.

Person person = new Person("John", "Doe");
Test.AssertEquals(person.FullName(), "John Doe");

이 예제에 대한 스텁이 필요하다고 생각해서는 안됩니다. 당신이 그렇게하면 당신은 행복하고 잘 ... 중지하세요! 어쨌든 메소드를 제어 할 수 있기 때문에 메소드를 조롱하면 이점이 없습니다.

FullName이 조롱하기 위해 사용하는 방법에 적합한 것으로 보이는 유일한 경우는 FirstName ()과 LastName ()이 사소한 작업이 아닌 경우입니다. 어쩌면 당신은 임의의 이름 생성기 중 하나를 작성하거나 FirstName 및 LastName이 데이터베이스에 답을 쿼리합니다. 그러나 그것이 일어나고 있다면 객체가 Person 클래스에 속하지 않는 것을하고 있다고 제안합니다.

다른 방법으로, 방법을 조롱하면 객체를 가져 와서 두 조각으로 나눕니다. 한 조각은 조롱되고 다른 조각은 테스트되고 있습니다. 당신이하고있는 일은 본질적으로 객체를 임시로 분해하는 것입니다. 이 경우 이미 개체를 분리하십시오.

수업이 단순하다면, 시험하는 동안 수업을 조롱 할 필요가 없다고 생각해야합니다. 수업이 복잡해서 조롱 할 필요가 있다고 생각되면 수업을 더 간단한 조각으로 나누어야합니다.

다시 업데이트

내가 보는 방식으로, 개체에는 외부 및 내부 동작이 있습니다. 외부 행동에는 다른 객체 등에 대한 값 호출이 포함됩니다. 분명히 해당 범주의 모든 항목을 테스트해야합니다. (그렇지 않으면 무엇을 테스트 하시겠습니까?) 그러나 내부 동작을 실제로 테스트해서는 안됩니다.

이제 내부 동작이 외부 동작의 결과이기 때문에 내부 동작이 테스트됩니다. 그러나 나는 내부 행동에 대한 테스트를 직접 작성하지 않고 외부 행동을 통해 간접적으로 만 작성합니다.

무언가를 테스트하고 싶다면 외부 행동이되도록 움직여야한다고 생각합니다. 그래서 당신이 무언가를 조롱하고 싶다면, 당신이 조롱하려는 것이 이제 문제의 객체의 외부 행동에 있도록 객체를 분리해야한다고 생각합니다.

그러나 어떤 차이가 있습니까? FirstName ()과 LastName ()이 다른 개체의 구성원이면 FullName ()의 문제가 실제로 변경됩니까? FirstName을 조롱하는 것이 필요하다고 결정하고 LastName은 실제로 다른 객체에있는 데 도움이됩니까?

당신이 당신의 조롱 접근법을 사용한다면, 당신은 객체에 이음새를 만듭니다. 외부 데이터 소스와 직접 통신하는 FirstName () 및 LastName ()과 같은 함수가 있습니다. 또한 FullName ()이 없습니다. 그러나 그들은 모두 같은 클래스에 있기 때문에 분명하지 않습니다. 일부 조각은 데이터 소스에 직접 액세스하지 않아야하고 다른 조각은 직접 액세스하지 않아야합니다. 두 그룹을 분리하면 코드가 명확 해집니다.

편집하다

한 걸음 물러서서 물어 보자. 왜 테스트 할 때 객체를 조롱 하는가?

  1. 테스트를 일관되게 실행합니다 (실행마다 변경되는 항목에 액세스하지 마십시오)
  2. 고가의 자원에 접근하지 마십시오
  3. 테스트중인 시스템 단순화
  4. 가능한 모든 시나리오 (예 : 실패 시뮬레이션 등)를 쉽게 테스트 할 수 있습니다.
  5. 다른 코드 조각의 세부 사항에 따라 다른 코드 조각을 변경해도이 테스트가 중단되지 않도록하십시오.

이제 1-4 이유가이 시나리오에 적용되지 않는다고 생각합니다. 전체 이름을 테스트 할 때 외부 소스를 조롱하면 조롱에 대한 모든 이유가 해결됩니다. 처리되지 않은 유일한 부분은 단순하지만 객체가 충분히 단순 해 걱정할 필요가없는 것 같습니다.

귀하의 우려는 사유 번호 5라고 생각합니다. 향후 어느 시점에서 FirstName 및 LastName의 구현을 변경하면 테스트가 중단 될 것입니다. 나중에 FirstName과 LastName은 다른 위치 나 소스에서 이름을 가져올 수 있습니다. 그러나 FullName은 아마도 항상입니다 FirstName() + " " + LastName(). 그렇기 때문에 FirstName과 LastName을 조롱하여 FullName을 테스트하려고합니다.

당신이 가지고있는 것은 개인 객체의 일부 하위 집합이며 다른 하위 개체보다 변경 될 가능성이 더 큽니다. 나머지 오브젝트는이 서브 세트를 사용합니다. 해당 하위 집합은 현재 하나의 소스를 사용하여 데이터를 가져 오지만 나중에 완전히 다른 방식으로 해당 데이터를 가져올 수 있습니다. 그러나 나에게 그것은 그 하위 집합이 나 가려고하는 독특한 물건처럼 들립니다.

객체의 방법을 조롱하면 객체가 분할되는 것 같습니다. 그러나 당신은 임시 방식으로 그렇게하고 있습니다. 귀하의 코드는 Person 객체 내부에 두 개의 별개의 조각이 있음을 명확하게 나타내지 않습니다. 따라서 객체를 실제 코드로 분할하면 진행중인 코드를 읽을 수 없습니다. 의미있는 대상의 실제 분할을 선택하고 각 테스트마다 대상을 다르게 분할하지 마십시오.

나는 당신이 당신의 물건을 나누는 것에 반대 할 수 있다고 생각하지만 왜 그럴까요?

편집하다

내가 틀렸어.

개별 방법을 조롱하여 임시 분할을 도입하는 대신 객체를 분할해야합니다. 그러나 객체를 분리하는 한 가지 방법에 지나치게 집중했습니다. 그러나 OO는 객체를 분할하는 여러 가지 방법을 제공합니다.

내가 제안하는 것 :

class PersonBase
{
      abstract sring FirstName();
      abstract string LastName();

      string FullName()
      {
            return FirstName() + " " + LastName();
      }
 }

 class Person extends PersonBase
 {
      string FirstName(); 
      string LastName();
 }

 class FakePerson extends PersonBase
 {
      void setFirstName(string);
      void setLastName(string);
      string getFirstName();
      string getLastName();
 }

어쩌면 그것은 당신이 함께하고있는 일입니다. 그러나 각 방법이 어느쪽에 있는지 명확하게 묘사했기 때문에이 방법에 조롱 방법으로 볼 때 문제가 있다고 생각하지 않습니다. 상속을 사용하면 추가 래퍼 객체를 사용했을 때 발생할 수있는 어색함을 피할 수 있습니다.

이것은 약간의 복잡성을 초래하며, 몇 가지 유틸리티 기능에 대해서만 기본 타사 소스를 조롱하여 테스트 할 것입니다. 물론, 그들은 깨질 위험이 높지만 재정렬 할 가치가 없습니다. 당신이 그것을 분리해야 할만 큼 복잡한 물체를 가지고 있다면, 나는 이것과 같은 것이 좋은 생각이라고 생각합니다.


8
잘했다. 테스트에서 해결 방법을 찾으려면 디자인을 다시 고려해야 할 수도 있습니다 (부수적으로 Frank Sinatra의 목소리가 내 머리에 붙어 있습니다).
문제의

2
"객체를 테스트하기 위해 공개 메소드를 조롱함으로써 그 객체가 어떻게 구현되는지에 대한 가정을하고 있습니다." 그러나 객체를 스터브 할 때가 그렇지 않습니까? 예를 들어, xyz () 메소드가 다른 객체를 호출한다는 것을 알고 있다고 가정하십시오. xyz ()를 독립적으로 테스트하려면 다른 객체를 스텁해야합니다. 따라서 테스트에서 xyz () 메소드의 구현 세부 사항에 대해 알아야합니다.
사용자

필자의 경우 "FirstName ()"및 "LastName ()"메서드는 간단하지만 결과에 대해 타사 API를 쿼리합니다.
사용자

@User, 아마도 업데이트되었으므로 타사 API를 조롱하여 FirstName과 LastName을 테스트하고 있습니다. FullName을 테스트 할 때 동일한 조롱을 수행하는 데 어떤 문제가 있습니까?
Winston Ewert

@Winston, 그것은 내 요점입니다. 현재 fullname ()을 테스트하기 위해 firstname과 lastname에 사용 된 타사 API를 조롱하지만 전체 이름을 테스트 할 때 firstname과 lastname이 구현 되는 방식에 신경 쓰지 않고 싶습니다 (물론 이름과 성을 테스트 할 때 괜찮습니다). 따라서 이름과 성을 조롱하는 것에 대한 내 질문.
사용자

10

Winston Ewert의 답변에 동의하지만 시간 제약으로 인해 API가 이미 다른 곳에서 사용되는지 또는 무엇을 가지고 있는지에 따라 클래스를 분리하는 것이 불가능할 수도 있습니다.

그런 경우에는 모의 메소드 대신 클래스를 점증 적으로 테스트하기 위해 테스트를 작성했습니다. 테스트 getExpenses()getRevenue()다음 테스트 방법을 getProfit(), 방법을 다음 공유 방법을 테스트합니다. 특정 방법을 다루는 테스트가 두 개 이상인 것은 사실이지만, 방법을 개별적으로 다루기 위해 통과 테스트를 작성 했으므로 테스트 된 입력을 통해 출력이 신뢰할 수 있는지 확인할 수 있습니다.


1
동의했다. 그러나이 글을 읽는 사람의 요점을 강조하기 위해 더 나은 솔루션을 수행 할 수없는 경우 사용하는 솔루션입니다.
Winston Ewert

5
@Winston, 이것은 더 나은 솔루션을 얻기 전에 사용 하는 솔루션입니다. 즉, 레거시 코드의 큰 공을 가지고 있다면 리팩토링 하기 전에 단위 테스트로 코드를 다루어야 합니다.
Péter Török

@Peter Török, 좋은 지적입니다. 나는 그것이 더 나은 해결책을 할 수는 없다고 생각하지만. :)
Winston Ewert

@all getProfit () 메소드 테스트는 getExpenses () 및 getRevenues ()에 대한 다른 시나리오가 실제로 getProfit ()에 필요한 시나리오를 곱하기 때문에 매우 어려울 것입니다. getExpenses ()를 테스트하고 Revenues ()를 독립적으로 얻는 경우 getProfit () 테스트를 위해 이러한 메소드를 조롱하는 것이 좋지 않습니까?
Mohd Farid

7

더 귀하의 예를 단순화하기 위해, 당신에게있는 거 테스트 말 C()에 따라, A()그리고 B()자신의 종속성이 각각을,. IMO는 당신의 시험이 성취하려고하는 것에 달려 있습니다.

당신의 행동을 테스트하는 경우 C()주어진 알려진 행동을 A()하고 B()그것은 아마도 가장 쉽고 가장 밖으로 스텁하는 것입니다 A()B(). 이것은 아마도 순수 주의자들에 의해 단위 테스트라고 불릴 것입니다.

당신이 (에서 전체 시스템의 동작을 테스트하는 경우 C()의 관점), 나는 떠날 것이다 A()B()같이 구현 및 종속성 밖으로 중 하나 스텁 또는 테스트로, 샌드 박스 환경을 설정 (그것이 시험을 가능하게하는 경우) 데이터 베이스. 이것을 통합 테스트라고 부릅니다.

두 가지 방법 모두 유효 할 수 있으므로 사전에 테스트 할 수 있도록 설계된 경우 장기적으로 더 나을 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.