추상 클래스를 단위 테스트하는 방법 : 스텁으로 확장?


446

추상 클래스와 추상 클래스를 확장하는 클래스를 단위 테스트하는 방법이 궁금합니다.

추상 클래스를 확장하여 추상 메소드를 스텁 아웃 한 다음 모든 구체적인 메소드를 테스트해야합니까? 그런 다음 재정의 된 메소드 만 테스트하고 추상 클래스를 확장하는 객체에 대한 단위 테스트의 추상 메소드를 테스트합니까?

추상 클래스의 메소드를 테스트하는 데 사용할 수있는 추상 테스트 케이스가 있어야하고 테스트 클래스에서 추상 클래스를 확장하는 오브젝트에 대해이 클래스를 확장해야합니까?

내 추상 클래스에는 구체적인 메소드가 있습니다.

답변:


268

Mock 객체를 작성하고 테스트 용도로만 사용하십시오. 그것들은 일반적으로 매우 매우 적으며 (추상 클래스에서 상 속됨) 더 이상은 아닙니다. 그러면 단위 테스트에서 테스트하려는 추상 메소드를 호출 할 수 있습니다.

가지고있는 다른 모든 클래스와 같이 일부 논리가 포함 된 추상 클래스를 테스트해야합니다.


9
젠장, 내가 모의를 사용한다는 생각에 동의 한 것은 이번이 처음입니다.
Jonathan Allen

5
모의와 테스트의 두 클래스가 필요합니다. mock 클래스는 테스트중인 Abstract 클래스의 추상 메소드 만 확장합니다. 이러한 메소드는 테스트되지 않으므로 no-op, null 반환 등이 될 수 있습니다. 테스트 클래스는 추상이 아닌 공개 API (즉, 추상 클래스에 의해 구현 된 인터페이스) 만 테스트합니다. Abstract 클래스를 확장하는 클래스에는 abstract 메소드가 포함되지 않았으므로 추가 테스트 클래스가 필요합니다.
사이버 스님

10
분명히이 작업을 수행하는 것이 가능합니다. 그러나 파생 클래스를 실제로 테스트하려면이 기본 기능을 반복해서 테스트 할 것입니다. 그러면 테스트에서 중복을 고려할 수있는 추상 테스트 픽스처가 생성됩니다. 이 모든 냄새가 난다! 먼저 추상 클래스를 사용하는 이유를 살펴보고 다른 것이 더 잘 작동하는지 확인하는 것이 좋습니다.
Nigel Thorne

5
과대 평가 된 다음 답변이 훨씬 좋습니다.
Martin Spamer

22
@MartiSpamer :이 답변이 아래에서 더 나은 것으로 생각되는 답변보다 훨씬 더 일찍 (2 년) 작성 되었기 때문에이 답변이 과대 평가되었다고 말할 수는 없습니다. Patrick이이 답변을 게시했을 때의 상황에서 훌륭했기 때문에 격려해 드리겠습니다. 서로 격려합시다. 건배
마빈 Thobejane

449

추상 기본 클래스가 사용되는 두 가지 방법이 있습니다.

  1. 추상 객체를 전문화하고 있지만 모든 클라이언트는 기본 인터페이스를 통해 파생 클래스를 사용합니다.

  2. 디자인의 객체 내에서 중복을 제거하기 위해 추상 기본 클래스를 사용하고 있으며 클라이언트는 자체 인터페이스를 통해 구체적인 구현을 사용합니다.!


1 전략 패턴에 대한 솔루션

옵션 1

첫 번째 상황 인 경우 실제로 파생 클래스가 구현하는 추상 클래스의 가상 메소드로 정의 된 인터페이스가 있습니다.

이것을 실제 인터페이스로 만들고 추상 클래스를 구체적으로 변경하고 생성자에서이 인터페이스의 인스턴스를 가져와야합니다. 파생 클래스는이 새로운 인터페이스의 구현이됩니다.

아이 모터

즉, 이제 새로운 인터페이스의 모의 인스턴스를 사용하여 이전 추상 클래스를 테스트 할 수 있으며 이제는 공용 인터페이스를 통해 각각의 새로운 구현을 테스트 할 수 있습니다. 모든 것이 간단하고 테스트 가능합니다.


2를위한 해결책

두 번째 상황이있는 경우 추상 클래스는 도우미 클래스로 작동합니다.

AbstractHelper

포함 된 기능을 살펴보십시오. 이 복제를 최소화하기 위해 조작중인 물체에 밀어 넣을 수 있는지 확인하십시오. 여전히 남아있는 것이 있다면 구체적인 구현이 생성자를 가져와 기본 클래스를 제거하는 도우미 클래스로 만드는 것을보십시오.

모터 도우미

이것은 간단하고 쉽게 테스트 할 수있는 구체적인 수업으로 이어집니다.


원칙적으로

복잡한 복잡한 네트워크를 통해 단순한 단순한 네트워크를 선호합니다.

확장 가능한 테스트 가능 코드의 핵심은 작은 빌딩 블록과 독립적 인 배선입니다.


업데이트 : 두 가지 혼합물을 처리하는 방법?

이 두 역할을 모두 수행하는 기본 클래스를 가질 수 있습니다. 즉, 공용 인터페이스가 있으며 도우미 메소드를 보호합니다. 이 경우 도우미 메소드를 하나의 클래스 (시나리오 2)로 인수 화하고 상속 트리를 전략 패턴으로 변환 할 수 있습니다.

기본 클래스가 직접 구현하고 다른 메소드가 가상 인 일부 메소드가있는 경우에도 상속 트리를 전략 패턴으로 변환 할 수 있지만 책임이 올바르게 정렬되지 않았 음을 나타내는 좋은 지표로 사용할 수도 있습니다. 리팩토링이 필요합니다.


업데이트 2 : 디딤돌로서의 추상 클래스 (2014/06/12)

요 전에는 초록을 사용한 상황이 있었으므로 그 이유를 살펴보고 싶습니다.

구성 파일에 대한 표준 형식이 있습니다. 이 특정 도구에는 해당 형식으로 된 3 개의 구성 파일이 있습니다. 종속성 설정을 통해 클래스가 관심있는 설정을 요청할 수 있도록 각 설정 파일에 대해 강력한 유형의 클래스를 원했습니다.

설정 파일 형식을 구문 분석하는 방법을 알고있는 추상 기본 클래스와 동일한 방법을 노출하지만 설정 파일의 위치를 ​​캡슐화 한 파생 클래스를 사용하여이를 구현했습니다.

3 개의 클래스가 래핑 된 "SettingsFileParser"를 작성한 다음 기본 클래스에 위임하여 데이터 액세스 방법을 노출 할 수있었습니다. 나는 다른 어떤 것보다 더 많은 위임 코드를 가진 3 개의 파생 클래스로 이어질 수 있기 때문에 아직 이 작업을 수행하지 않기로 선택했습니다 .

그러나 ...이 코드가 발전하고 이러한 각 설정 클래스의 소비자가 더 명확 해집니다. 각 설정 사용자는 몇 가지 설정을 요구하고 어떤 방식 으로든 설정을 변경합니다 (설정은 텍스트이므로 설정을 숫자 등으로 변환하는 개체로 래핑 할 수 있습니다). 이런 일이 발생하면이 논리를 데이터 조작 방법으로 추출하고 강력한 형식의 설정 클래스로 다시 밀어 넣습니다. 이는 각 설정 세트에 대해 더 높은 수준의 인터페이스로 연결되어 결국 '설정'을 다루는 것을 더 이상 인식하지 못합니다.

이 시점에서 강력한 형식의 설정 클래스는 더 이상 기본 '설정'구현을 노출하는 "게터"메소드가 필요하지 않습니다.

그 시점에서 나는 더 이상 공개 인터페이스에 설정 접근 자 메소드를 포함시키지 않기를 원합니다. 따라서이 클래스를 변경하여 설정 파서 클래스를 캡슐화하지 않고 변경합니다.

따라서 Abstract 클래스는 현재 위임 코드를 피할 수있는 방법이며 코드의 마커는 나중에 디자인을 변경하도록 상기시킵니다. 나는 그것을 얻지 못할 수도 있으므로 좋은 시간 동안 살 수 있습니다 ... 코드 만 말할 수 있습니다.

"정적 메소드 없음"또는 "비공개 메소드 없음"과 같은 규칙에서이 사실을 발견했습니다. 그들은 코드에서 냄새를 나타냅니다 ... 그리고 좋습니다. 그것은 당신이 놓친 추상화를 계속 찾고 ... 그 동안 고객에게 가치를 계속 제공합니다.

계곡에 유지 가능한 코드가있는 풍경을 정의하는 것과 같은 규칙을 상상합니다. 새로운 행동을 추가하면 코드에 비가 오는 것과 같습니다. 처음에는 착륙 할 때마다 그것을 배치합니다. 그런 다음 리팩터링하여 좋은 디자인의 힘이 모든 동작이 계곡에서 끝날 때까지 행동을 옮길 수 있도록합니다.


18
이것은 좋은 대답입니다. 최고 등급보다 훨씬 낫습니다. 그러나 실제로 테스트 가능한 코드를 작성하려는 사람들만이 그것을 고맙게 생각합니다 .. :)
MalcomTucker

22
나는 이것이 실제로 얼마나 좋은 대답인지 극복 할 수 없다. 추상 수업에 대한 나의 생각 방식이 완전히 바뀌 었습니다. 감사합니다 나이젤.
MalcomTucker

4
아 .. 다시 생각해야 할 또 다른 신조! 고마워요 (지금은 냉소적으로 그리고 비 차별적으로 동화하고 더 나은 프로그래머처럼 느끼면)
Martin Lyne

11
좋은 대답입니다. 분명히 생각해 볼 점은 있지만 추상적 클래스를 사용하지 않기 위해 기본적으로 말하지 않습니까?
brianestey

32
규칙만으로는 +1, "복잡한 객체의 단순한 네트워크를 통해 단순한 객체의 복잡한 네트워크를 즐겨라"
David Glass

12

내가 추상 클래스와 인터페이스를 위해하는 일은 다음과 같다 : 나는 테스트를 작성하고, 객체를 구체적으로 사용한다. 그러나 X 유형의 변수 (X는 추상 클래스 임)는 테스트에서 설정되지 않습니다. 이 테스트 클래스는 테스트 스위트에 추가되지 않지만 서브 클래스에는 변수를 X의 구체적인 구현으로 설정하는 setup-method가 있습니다. 그렇게하면 테스트 코드를 복제하지 않습니다. 사용되지 않은 테스트의 서브 클래스는 필요한 경우 더 많은 테스트 방법을 추가 할 수 있습니다.


이것이 서브 클래스에서 캐스팅 문제를 일으키지 않습니까? X에 메소드 a가 있고 Y가 X를 상속하지만 메소드 b도있는 경우 테스트 클래스를 서브 클래스 화 할 때 b에서 테스트를 수행하기 위해 추상 변수를 Y로 캐스팅 할 필요가 없습니까?
Johnno Nolan

8

구체적으로 추상 클래스에서 단위 테스트를 수행하려면 테스트 목적, base.method () 결과 테스트 및 상속시 의도 된 동작을 테스트해야합니다.

메소드를 호출하여 테스트하므로 추상 클래스를 구현하여 테스트하십시오.


8

귀하의 추상 클래스에 비즈니스 가치가있는 구체적인 기능이 포함되어 있으면 일반적으로 추상 데이터를 스텁하는 테스트 더블을 작성하거나 모의 프레임 워크를 사용하여 직접 테스트하여 직접 테스트합니다. 내가 선택하는 것은 추상 메소드의 테스트 특정 구현을 작성해야하는지 여부에 달려 있습니다.

가장 일반적인 시나리오는 템플릿 메소드 패턴을 사용하는 경우 (예 : 타사에서 사용할 일종의 확장 가능한 프레임 워크를 빌드하는 경우)입니다. 이 경우 추상 클래스는 테스트하려는 알고리즘을 정의하는 것이므로 특정 구현보다 추상 기반을 테스트하는 것이 더 합리적입니다.

그러나 이러한 테스트는 실제 비즈니스 로직구체적인 구현 에만 중점을 두어야 합니다 . 취성 테스트로 끝날 것이기 때문에 추상 클래스의 구현 세부 사항 을 단위 테스트하지 않아야 합니다.


6

한 가지 방법은 추상 클래스에 해당하는 추상 테스트 사례를 작성한 다음 추상 테스트 사례를 서브 클래스하는 구체적인 테스트 사례를 작성하는 것입니다. 원래 추상 클래스의 각 구체적인 서브 클래스에 대해이를 수행하십시오 (예 : 테스트 케이스 계층 구조가 클래스 계층 구조를 반영 함). junit 레시피 서적에서 인터페이스 테스트 : http://safari.informit.com/9781932394238/ch02lev1sec6을 참조하십시오 .

xUnit 패턴의 Testcase Superclass도 참조하십시오 : http://xunitpatterns.com/Testcase%20Superclass.html


4

나는 "추상적 인"테스트에 반대 할 것이다. 테스트는 구체적인 아이디어이며 추상화가 없다고 생각합니다. 공통 요소가있는 경우 모든 사람이 사용할 수 있도록 도우미 메소드 또는 클래스에 배치하십시오.

추상 테스트 클래스를 테스트 할 때는 테스트 대상이 무엇인지 스스로에게 물어보십시오. 몇 가지 접근 방식이 있으며 시나리오에서 무엇이 작동하는지 찾아야합니다. 서브 클래스에서 새로운 메소드를 테스트하려고합니까? 그런 다음 테스트가 해당 메소드와 만 상호 작용하도록하십시오. 기본 수업에서 메소드를 테스트하고 있습니까? 그런 다음 해당 클래스에 대해서만 별도의 픽스쳐를 사용하고 필요한만큼 많은 테스트로 각 방법을 개별적으로 테스트하십시오.


나는 이미 테스트 한 코드를 다시 테스트하고 싶지 않았기 때문에 추상 테스트 사례를 진행했습니다. 추상 클래스의 모든 구체적인 메소드를 한곳에서 테스트하려고합니다.
Paul Whelan

7
적어도 일부 (많은?) 경우에 도우미 클래스에 공통 요소를 추출하는 데 동의하지 않습니다. 추상 클래스에 구체적인 기능이 포함되어 있으면 해당 기능을 직접 단위 테스트하는 것이 완벽하게 허용됩니다.
Seth Petry-Johnson

4

이것은 추상 클래스를 테스트하기 위해 하네스를 설정할 때 일반적으로 따르는 패턴입니다.

public abstract class MyBase{
  /*...*/
  public abstract void VoidMethod(object param1);
  public abstract object MethodWithReturn(object param1);
  /*,,,*/
}

그리고 테스트에서 사용하는 버전 :

public class MyBaseHarness : MyBase{
  /*...*/
  public Action<object> VoidMethodFunction;
  public override void VoidMethod(object param1){
    VoidMethodFunction(param1);
  }
  public Func<object, object> MethodWithReturnFunction;
  public override object MethodWithReturn(object param1){
    return MethodWihtReturnFunction(param1);
  }
  /*,,,*/
}

예상치 못한 추상 메서드가 호출되면 테스트가 실패합니다. 테스트를 정렬 할 때 어설 션을 수행하고 예외를 throw하며 다른 값을 반환하는 람다로 추상 메서드를 쉽게 스텁 할 수 있습니다.


3

구체적인 메서드가 전략이 작동하지 않는 추상 메서드를 호출하는 경우 각 자식 클래스 동작을 개별적으로 테스트하려고합니다. 그렇지 않으면, 추상 클래스의 구체적인 메서드가 자식 클래스와 분리되어 있다면, 확장하고 설명하는 추상 메서드를 스텁하는 것이 좋습니다.


2

나는 당신이 추상 클래스의 기본 기능을 테스트하고 싶을 것이라고 생각합니다 ... 그러나 아마도 메소드를 재정의하지 않고 클래스를 확장하고 추상적 인 메소드를 위해 최소한의 노력을 기울이는 것이 가장 좋습니다.


2

추상 클래스를 사용하는 주요 동기 중 하나는 응용 프로그램 내에서 다형성을 활성화하는 것입니다. 즉, 런타임에 다른 버전을 대체 할 수 있습니다. 실제로 이것은 추상 클래스가 종종 템플릿 패턴 이라고 불리는 일반적인 배관을 제공한다는 점을 제외하고는 인터페이스를 사용하는 것과 거의 동일합니다 .

단위 테스트 관점에서 고려해야 할 두 가지가 있습니다.

  1. 추상 클래스와 관련 클래스의 상호 작용 . 모의 테스트 프레임 워크를 사용하는 것은 추상 클래스가 다른 클래스와 잘 작동 함을 보여주기 때문에이 시나리오에 이상적입니다.

  2. 파생 클래스의 기능 . 파생 클래스에 대해 작성한 사용자 지정 논리가있는 경우 해당 클래스를 별도로 테스트해야합니다.

편집 : RhinoMocks는 클래스에서 동적으로 파생시켜 런타임에 모의 객체를 생성 할 수있는 멋진 모의 테스트 프레임 워크입니다. 이 접근 방식을 사용하면 수많은 코딩 작업 시간을 절약 할 수 있습니다.


2

먼저 추상 클래스에 구체적인 방법이 포함되어 있다면이 예제를 고려해야한다고 생각합니다.

 public abstract class A 

 {

    public boolean method 1
    {
        // concrete method which we have to test.

    }


 }


 class B extends class A

 {

      @override
      public boolean method 1
      {
        // override same method as above.

      }


 } 


  class Test_A 

  {

    private static B b;  // reference object of the class B

    @Before
    public void init()

      {

      b = new B ();    

      }

     @Test
     public void Test_method 1

       {

       b.method 1; // use some assertion statements.

       }

   }

1

추상 클래스가 구현에 적합한 경우 파생 된 콘크리트 클래스를 테스트하십시오 (위에서 제안한대로). 당신의 가정은 정확합니다.

미래의 혼란을 피하기 위해이 구체적인 테스트 클래스는 모의가 아니라 가짜 입니다.

엄밀히 말하면 모의 는 다음과 같은 특성으로 정의됩니다.

  • 테스트 대상 클래스의 모든 의존성 대신 모의가 사용됩니다.
  • 모의는 인터페이스의 의사 구현입니다 (일반적으로 종속성을 인터페이스로 선언해야한다는 것을 기억할 수 있습니다. 테스트 가능성은 이것의 주된 이유입니다)
  • 테스트 시점에 (모의 프레임 워크를 사용하여) 메소드 또는 속성과 같은 모의 인터페이스 멤버의 동작이 제공됩니다. 이런 방식으로, 테스트중인 구현과 종속 구현 (모두 별도의 테스트가 있어야 함)을 구현하지 않아도됩니다.

1

@ Patrick-desjardins 답변에 따라 abstract를 구현 @Test했으며 다음과 같이 구현 클래스 가 있습니다.

추상 클래스-ABC.java

import java.util.ArrayList;
import java.util.List;

public abstract class ABC {

    abstract String sayHello();

    public List<String> getList() {
        final List<String> defaultList = new ArrayList<>();
        defaultList.add("abstract class");
        return defaultList;
    }
}

으로 추상 클래스를 인스턴스화 할 수 있지만,이 서브 클래스화할 수있는 구체적인 클래스 DEF.java , 같은 다음입니다 :

public class DEF extends ABC {

    @Override
    public String sayHello() {
        return "Hello!";
    }
}

초록과 비 추상적 방법을 모두 테스트하는 @Test 클래스 :

import org.junit.Before;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.contains;
import java.util.Collection;
import java.util.List;
import static org.hamcrest.Matchers.equalTo;

import org.junit.Test;

public class DEFTest {

    private DEF def;

    @Before
    public void setup() {
        def = new DEF();
    }

    @Test
    public void add(){
        String result = def.sayHello();
        assertThat(result, is(equalTo("Hello!")));
    }

    @Test
    public void getList(){
        List<String> result = def.getList();
        assertThat((Collection<String>) result, is(not(empty())));
        assertThat(result, contains("abstract class"));
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.