동일한 클래스 내에서 다른 메소드를 호출하는 단위 테스트 메소드를 작성하는 가장 좋은 방법


35

나는 최근에 어떤 친구들과 다음 두 가지 방법 중 어느 것이 같은 클래스 내부의 메소드에서 같은 클래스 내의 메소드에 대한 반환 결과 또는 호출을 스텁하는 것이 가장 좋은지에 대해 이야기했습니다.

이것은 매우 간단한 예입니다. 실제로 기능은 훨씬 더 복잡합니다.

예:

public class MyClass
{
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionB()
     {
         return new Random().Next();
     }
}

이를 테스트하기 위해 두 가지 방법이 있습니다.

방법 1 : 기능 및 동작을 사용하여 방법의 기능을 대체하십시오. 예:

public class MyClass
{
     public Func<int> FunctionB { get; set; }

     public MyClass()
     {
         FunctionB = FunctionBImpl;
     }

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected int FunctionBImpl()
     {
         return new Random().Next();
     }
}

[TestClass]
public class MyClassTests
{
    private MyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new MyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionB = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

방법 2 : 멤버를 가상으로, 클래스 파생 및 파생 클래스에서 함수 및 작업을 사용하여 기능 대체

public class MyClass
{     
     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }

     protected virtual int FunctionB()
     {
         return new Random().Next();
     }
}

public class TestableMyClass
{
     public Func<int> FunctionBFunc { get; set; }

     public MyClass()
     {
         FunctionBFunc = base.FunctionB;
     }

     protected override int FunctionB()
     {
         return FunctionBFunc();
     }
}

[TestClass]
public class MyClassTests
{
    private TestableMyClass _subject;

    [TestInitialize]
    public void Initialize()
    {
        _subject = new TestableMyClass();
    }

    [TestMethod]
    public void FunctionA_WhenNumberIsOdd_ReturnsTrue()
    {
        _subject.FunctionBFunc = () => 1;

        var result = _subject.FunctionA();

        Assert.IsFalse(result);
    }
}

나는 왜 더 나은지 알고 싶어?

업데이트 : 참고 : FunctionB도 공개 가능


귀하의 예는 간단하지만 정확하지는 않습니다. FunctionA부울을 반환하지만 로컬 변수 만 설정하고 x아무것도 반환하지 않습니다.
Eric P.

1
이 특정 예에서 FunctionB는 public static다른 클래스에 있을 수 있습니다 .

코드 검토의 경우 실제 코드를 단순화 된 버전으로 게시하지 않아야합니다. FAQ를 참조하십시오. 그 의미로, 당신은 코드 검토를 찾지 않는 특정 질문을 요구하고 있습니다.
Winston Ewert

1
FunctionB디자인에 의해 깨졌습니다. new Random().Next()거의 항상 잘못되었습니다. 의 인스턴스를 주입해야합니다 Random. ( Random또한 잘못 설계된 클래스이므로 몇 가지 추가 문제가 발생할 수 있습니다.)
CodesInChaos

델리게이트를 통한보다 일반적인 메모 DI는 절대적으로 훌륭한 imho
jk입니다.

답변:


32

원래 포스터 업데이트에 따라 수정되었습니다.

면책 조항 : C # 프로그래머가 아닙니다 (주로 Java 또는 Ruby). 내 대답은 : 나는 전혀 테스트하지 않을 것이며, 당신이해야한다고 생각하지 않습니다.

더 긴 버전은 : 개인 / 보호 된 메소드는 API의 일부가 아니며 기본적으로 구현 선택 사항이므로 외부에 영향을 미치지 않고 검토, 업데이트 또는 완전히 버릴 수 있습니다.

외부 세계에서 볼 수있는 클래스의 일부인 FunctionA ()에 대한 테스트가 있다고 가정합니다. 구현 계약이 있고 테스트 할 수있는 유일한 계약이어야합니다. 귀하의 개인 / 보호 방법은 이행 및 / 또는 테스트 계약이 없습니다.

관련 토론을 참조하십시오 : https : //.com/questions/105007/should-i-test-private-methods-or-only-public-ones

주석 다음에 FunctionB가 공개라면 단위 테스트를 사용하여 간단히 테스트 할 것입니다. FunctionA의 테스트가 완전히 "단위"가 아니라고 생각할 수도 있지만 (FunctionB라고 함) 너무 걱정하지는 않을 것입니다. FunctionB 테스트가 작동하지만 FunctionA 테스트가 아닌 경우 문제가 FunctionB의 하위 도메인으로, 그것은 차별 자로 충분합니다.

두 테스트를 완전히 분리하려면 FunctionA를 테스트 할 때 일종의 조롱 기술을 사용하여 FunctionB를 조롱합니다 (일반적으로 알려진 고정 값을 반환합니다). 특정 조롱 라이브러리에 조언 할 C # 생태계 지식이 없지만 이 질문을 볼 수 있습니다 .


2
@Martin 답변에 완전히 동의하십시오. 클래스에 대한 단위 테스트를 작성할 때 메소드를 테스트해서는 안됩니다 . 테스트하는 것은 클래스 동작이며 계약 (클래스가 수행 해야하는 선언)이 충족된다는 것입니다. 따라서 단위 테스트는 예외 사례를 포함하여이 클래스에 노출 된 모든 요구 사항 (공용 메서드 / 속성 사용)을 포함해야합니다.

안녕하세요, 답변 주셔서 감사하지만 내 질문에 대답하지 않았습니다. FunctionB가 개인 / 보호 된 것인지는 중요하지 않습니다. 또한 공용 일 수 있으며 여전히 FunctionA에서 호출 될 수 있습니다.

기본 클래스를 다시 디자인하지 않고이 문제를 처리하는 가장 일반적인 방법은 MyClass스텁하려는 기능으로 메소드 를 서브 클래스 화 하고 대체하는 것입니다. FunctionB공개 될 수 있도록 질문을 업데이트하는 것이 좋습니다 .
Eric P.

1
protected다른 어셈블리에 클래스를 구현할 수없는 경우가 아니라면 메서드는 클래스의 공용 영역에 속합니다.
코드 InChaos

2
FunctionA가 FunctionB를 호출한다는 사실은 단위 테스트 관점에서 관련이 없습니다. FunctionA에 대한 테스트가 올바르게 작성되면 나중에 테스트를 중단하지 않고 (FunctionA의 전체 동작이 변경되지 않는 한) 리팩토링 될 수있는 구현 세부 사항입니다. 실제 문제는 FunctionB의 임의의 숫자 검색이 주입 된 오브젝트로 수행되어야하므로 테스트 중에 모의를 사용하여 잘 알려진 숫자가 리턴되는지 확인할 수 있습니다. 이를 통해 잘 알려진 입력 / 출력을 테스트 할 수 있습니다.
Dan Lyons

11

함수가 테스트에 중요하거나 대체해야하는 경우 테스트중인 클래스의 개인 구현 세부 정보가 아니라 다른 클래스 의 공개 구현 세부 정보가되기에 충분하다는 이론에 동의합니다 .

그래서 내가있는 시나리오에 있다면

class A 
{
     public B C()
     {
         D();
     }

     private E D();
     {
         // i actually want to control what this produces when I test C()
         // or this is important enough to test on its own
         // and, typically, both of the above
     }
}

그런 다음 리팩토링합니다.

class A 
{
     ICollaborator collaborator;

     public A(ICollaborator collaborator)
     {
         this.collaborator = collaborator;
     }

     public B C()
     {
         collaborator.D();
     }
}

이제 D ()가 독립적으로 테스트 가능하고 완전히 교체 가능한 시나리오가 있습니다.

조직의 수단으로 공동 작업자 는 동일한 네임 스페이스 수준에서 살지 않을 수 있습니다 . 예를 들어 AFooCorp.BLL에있는 경우 내 공동 작업자는 FooCorp.BLL.Collaborators (또는 적절한 이름)에서와 같이 다른 계층의 깊이 일 수 있습니다. 내 공동 작업자는 internal액세스 수정자를 통해 어셈블리 내부에서만 볼 수 있으며 InternalsVisibleTo어셈블리 속성을 통해 단위 테스트 프로젝트에도 노출됩니다 . 확인 가능한 코드를 생성하는 동안 호출자에 관한 한 API를 깨끗하게 유지할 수 있습니다.


ICollaborator에 여러 가지 방법이 필요한 경우 가능합니다. 유일한 작업 인 객체가있는 경우 단일 메소드 를 래핑하는 것이 대신 델리게이트로 대체 된 것을 볼 수 있습니다.
jk.

당신은 지명 된 델리게이트가 말이되는지 또는 인터페이스가 말이되는지 결정해야 할 것이며, 나는 당신을 위해 결정하지 않을 것입니다. 개인적으로, 나는 단일 (공용) 메소드 클래스를 싫어하지 않습니다. 점점 더 이해하기 쉬워 질수록 작을수록 좋습니다.
Anthony Pegram

0

Martin이 지적한 것에 덧붙여서

방법이 개인 / 보호 된 경우 테스트하지 마십시오. 클래스 내부에 있으며 클래스 외부에서 액세스하면 안됩니다.

언급 한 두 가지 접근 방식 모두에 대해 다음과 같은 우려가 있습니다.

방법 1-실제로 테스트에서 테스트중인 클래스의 동작을 변경합니다.

방법 2-실제로 프로덕션 코드를 테스트하지 않고 다른 구현을 테스트합니다.

언급 된 문제에서 A의 유일한 논리는 FunctionB의 출력이 짝수인지 확인하는 것입니다. 설명을 위해 FunctionB는 임의의 값을 제공하므로 테스트하기가 어렵습니다.

FunctionB가 무엇을 반환하는지 알 수 있도록 MyClass를 설정할 수있는 현실적인 시나리오를 기대합니다. 그런 다음 예상 결과가 알려지면 FunctionA를 호출하고 실제 결과를 주장 할 수 있습니다.


3
protected와 거의 동일합니다 public. 만 privateinternal구현 세부 사항입니다.
코드 InChaos

@ codeinchaos-궁금합니다. 테스트의 경우 어셈블리 속성을 수정하지 않으면 보호 된 메서드는 '비공개'입니다. 파생 형식 만 보호 멤버에 액세스 할 수 있습니다. 가상을 제외하고는 왜 protected를 테스트에서 일반과 비슷하게 취급해야하는지 모르겠습니다. 좀 더 자세히 설명해 주시겠습니까?
Srikanth Venugopalan

이러한 파생 클래스는 다른 어셈블리에있을 수 있으므로 타사 코드에 노출되므로 클래스의 공용 영역에 노출됩니다. 테스트하려면 테스트 internal protected프로젝트에서 만들거나 개인 리플렉션 도우미를 사용하거나 파생 클래스를 만들 수 있습니다.
코드 InChaos

@CodesInChaos는 파생 클래스가 다른 어셈블리에있을 수 있지만 범위는 여전히 기본 및 파생 유형으로 제한된다는 데 동의했습니다. 액세스 수정자를 테스트 가능하도록 수정하는 것은 약간 긴장된 것입니다. 나는 그것을했지만, 그것은 반 패턴으로 보인다.
Srikanth Venugopalan

0

개인적으로 Method1을 사용합니다. 즉, 모든 메소드를 Actions 또는 Funcs로 만들면 코드 테스트 가능성이 크게 향상되었습니다. 모든 솔루션과 마찬가지로이 접근법에는 장단점이 있습니다.

찬성

  1. 단위 테스팅에만 코드 패턴을 사용하면 복잡성이 추가 될 수있는 간단한 코드 구조가 가능합니다.
  2. Moq와 같이 널리 사용되는 모의 프레임 워크에 필요한 가상 메소드를 제거하고 클래스를 봉인 할 수 있습니다. 클래스를 봉인하고 가상 메서드를 제거하면 인라이닝 및 기타 컴파일러 최적화가 가능합니다. ( https://msdn.microsoft.com/en-us/library/ff647802.aspx )
  3. 단위 테스트에서 Func / Action 구현을 대체하는 것은 Func / Action에 새로운 값을 할당하는 것만 큼 간단하므로 테스트 가능성을 단순화합니다
  4. 정적 메소드를 조롱 할 수 없으므로 다른 메소드에서 정적 Func가 호출되었는지 테스트 할 수도 있습니다.
  5. 호출 사이트에서 메소드를 호출하는 구문은 동일하게 유지되므로 기존 메소드를 Funcs / Actions로 쉽게 리팩토링 할 수 있습니다. (방법을 Func / Action으로 리팩토링 할 수없는 경우에는 단점을 참조하십시오)

단점

  1. Funcs / Action에는 Methods와 같은 상속 경로가 없으므로 클래스를 파생시킬 수있는 경우 Funcs / Actions를 사용할 수 없습니다.
  2. 기본 매개 변수를 사용할 수 없습니다. 기본 매개 변수를 사용하여 Func을 만들려면 사용 사례에 따라 코드가 혼동 될 수있는 새 대리자를 만들어야합니다.
  3. 무언가 (firstName : "S", lastName : "K")와 같은 메서드를 호출하기 위해 명명 된 매개 변수 구문을 사용할 수 없습니다.
  4. 가장 큰 단점은 Funcs와 Actions에서 'this'참조에 액세스 할 수 없으므로 클래스에 대한 모든 종속성을 매개 변수로 명시 적으로 전달해야한다는 것입니다. 모든 의존성을 아는 것은 좋지만 Func가 의존 할 속성이 많은 경우 나쁩니다. 마일리지는 사용 사례에 따라 다릅니다.

요약하자면 클래스가 재정의되지 않을 것임을 알고 있다면 단위 테스트에 Funcs 및 Actions를 사용하는 것이 좋습니다.

또한 일반적으로 Funcs의 속성을 만들지 않고 직접 인라인합니다.

public class MyClass
{
     public Func<int> FunctionB = () => new Random().Next();

     public bool FunctionA()
     {
         return FunctionB() % 2 == 0;
     }
}

이것이 도움이되기를 바랍니다!


-1

모의를 사용하여 가능합니다. 너겟 : https://www.nuget.org/packages/moq/

그리고 나는 그것이 매우 간단하고 의미가 있다고 믿습니다.

public class SomeClass
{
    public SomeClass(int a) { }

    public void A()
    {
        B();
    }

    public virtual void B()
    {

    }
}

[TestFixture]
public class Test
{
    [Test]
    public void Test_A_Calls_B()
    {
        var mockedObject = new Mock<SomeClass>(5); // You can also specify constructor arguments.
        //You can also setup what a function can return.
        var obj = mockedObject.Object;
        obj.A();

        Mock.Get(obj).Verify(x=>x.B(),Times.AtLeastOnce);//This test passes
    }
}

Mock은 재정의하기 위해 가상 메소드를 필요로합니다.

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