스텁을 사용하여 단위 테스트를 위한 애플리케이션의 여러 부분을 서로 격리

스텁 유형은 Microsoft Fakes 프레임워크에서 제공하는 중요한 기술로, 테스트 중인 구성 요소를 해당 구성 요소가 의존하는 다른 구성 요소로부터 쉽게 격리할 수 있게 해줍니다. 스텁은 테스트 중에 다른 컴포넌트를 대체하는 작은 코드 조각으로 작동합니다. 스텁 사용의 주요 이점은 일관된 결과를 얻을 수 있어 테스트 작성이 더 쉬워진다는 점입니다. 다른 컴포넌트가 아직 완전히 작동하지 않더라도 스텁을 사용하여 테스트를 실행할 수 있습니다.

스텁을 효과적으로 적용하려면 애플리케이션의 다른 부분의 구체적인 클래스보다는 주로 인터페이스에 의존하는 방식으로 컴포넌트를 설계하는 것이 좋습니다. 이러한 설계 방식은 디커플링을 촉진하고 한 부분의 변경으로 인해 다른 부분의 수정이 필요할 가능성을 줄여줍니다. 이러한 설계 방식은 디커플링을 촉진하고 한 부분의 변경으로 인해 다른 부분의 수정이 필요할 가능성을 줄여줍니다.

예를 들어 관련된 컴포넌트를 보여주는 다이어그램을 살펴봅시다:

Diagram of Real and Stub classes of StockAnalyzer.

이 다이어그램에서 테스트 중인 컴포넌트는 StockAnalyzer이며, 일반적으로 RealStockFeed라는 다른 컴포넌트에 의존합니다. 하지만 RealStockFeed은 메서드가 호출될 때마다 다른 결과를 반환하기 때문에 테스트에 어려움이 있습니다. 이러한 가변성 때문에 StockAnalyzer의 일관되고 안정적인 테스트를 보장하기 어렵습니다.

테스트 중 이러한 장애물을 극복하기 위해 의존성 주입 방식을 채택할 수 있습니다. 이 접근 방식은 애플리케이션의 다른 컴포넌트에서 클래스를 명시적으로 언급하지 않는 방식으로 코드를 작성하는 것입니다. 대신 다른 컴포넌트와 스텁이 테스트 목적으로 구현할 수 있는 인터페이스를 정의합니다.

다음은 코드에서 종속성 주입을 사용하는 방법의 예시입니다:

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

스텁 제한

스텁에 대한 다음 제한 사항을 검토하세요.

스텁 생성하기: 단계별 가이드

앞의 다이어그램에 표시된 예제로 이 연습을 시작해 보겠습니다.

클래스 라이브러리 생성

다음 단계에 따라 클래스 라이브러리를 만듭니다.

  1. Visual Studio를 열고 Class Library 프로젝트를 생성합니다.

    Screenshot of Class Library project in Visual Studio.

  2. 프로젝트 속성을 구성합니다:

    • 프로젝트 이름StockAnalysis로 설정합니다.
    • 솔루션 이름StubsTutorial로 설정합니다.
    • 프로젝트 대상 프레임워크.NET 8.0로 설정합니다.
  3. 기본 파일 Class1.cs를 삭제합니다.

  4. IStockFeed.cs라는 새 파일을 추가하고 다음 인터페이스 정의를 복사합니다.

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. StockAnalyzer.cs라는 새 파일을 하나 더 추가하고 다음 클래스 정의에 복사합니다.

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

테스트 프로젝트 생성

연습용 테스트 프로젝트를 생성합니다.

  1. 솔루션을 마우스 오른쪽 버튼으로 클릭하고 MSTest Test Project라는 새 프로젝트를 추가합니다.

  2. 프로젝트 이름을 TestProject로 설정합니다.

  3. 프로젝트의 대상 프레임워크를 .NET 8.0으로 설정합니다.

    Screenshot of Test project in Visual Studio.

가짜 어셈블리 추가

프로젝트에 Fakes 어셈블리를 추가합니다.

  1. StockAnalyzer에 프로젝트 참조를 추가합니다.

    Screenshot of the command Add Project Reference.

  2. Fakes 어셈블리를 추가합니다.

    1. 솔루션 탐색기에서 어셈블리 참조를 찾습니다:

      • 이전 .NET Framework 프로젝트(비 SDK 스타일)의 경우 단위 테스트 프로젝트의 참조 노드를 확장합니다.

      • .NET Framework, .NET Core 또는 .NET 5.0 이상을 대상으로 하는 SDK 스타일 프로젝트의 경우 종속성 노드를 확장하여 어셈블리, 프로젝트 또는 패키지에서 모조할 어셈블리를 찾습니다.

      • Visual Basic에서 작업하는 경우 솔루션 탐색기 도구 모음에서 모든 파일 표시를 선택하여 참조 노드를 봅니다.

    2. 스텁을 만들려는 클래스 정의가 포함된 어셈블리를 선택합니다.

    3. 바로 가기 메뉴에서 Fakes 어셈블리 추가를 선택합니다.

      Screenshot of the command Add Fakes Assembly.

단위 테스트 생성

이제 단위 테스트를 생성합니다.

  1. 기본 파일 UnitTest1.cs를 수정하여 다음 Test Method 정의를 추가합니다.

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = componentUnderTest.GetContosoPrice();
    
            // Assert:
            // Verify the correct result in the usual way:
            Assert.AreEqual(priceToReturn, actualResult);
    
            // Verify that the component made the correct call:
            Assert.AreEqual("COOO", companyCodeUsed);
        }
    }
    

    여기서 특별한 마법의 조각은 StubIStockFeed 클래스입니다. Microsoft Fakes는 참조된 어셈블리의 모든 인터페이스에 대해 스텁 클래스를 생성합니다. 스텁 클래스의 이름은 인터페이스의 이름에서 파생되며 “Fakes.Stub”이 접두사가 되고 매개 변수 형식 이름이 추가됩니다.

    스텁은 속성, 이벤트 및 제네릭 메서드의 getter와 setter에 대해서도 생성됩니다. 자세한 내용은 스텁을 사용하여 유닛 테스트를 위한 애플리케이션의 여러 부분을 서로 격리를 참조하세요.

    Screenshot of Solution Explorer showing all files.

  2. 테스트 탐색기를 열고 테스트를 실행합니다.

    Screenshot of Test Explorer.

다양한 형식 멤버에 대한 스텁

다양한 종류의 유형 멤버에 대한 스텁이 있습니다.

메서드

제공된 예제에서는 스텁 클래스의 인스턴스에 델리게이트를 연결하여 메서드를 스텁할 수 있습니다. 스텁 형식의 이름은 메서드 및 매개 변수 이름에서 파생됩니다. 예를 들어 다음 IStockFeed 인터페이스와 그 메서드 GetSharePrice를 생각해 봅시다:

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

GetSharePriceString를 사용하여 GetSharePrice에 스텁을 첨부합니다:

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

메서드에 대한 스텁을 제공하지 않으면 Fakes는 반환 타입의 default value을 반환하는 함수를 생성합니다. 숫자의 경우 기본값은 0입니다. 클래스 유형의 경우 기본값은 C#의 경우 null, Visual Basic의 경우 Nothing입니다.

속성

프로퍼티 게터와 세터는 별도의 델리게이트로 노출되며 개별적으로 스텁될 수 있습니다. 예를 들어, ValueIStockFeedWithProperty 속성을 고려합니다.

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

Value의 게터와 세터를 스텁하고 자동 프로퍼티를 시뮬레이션하려면 다음 코드를 사용할 수 있습니다:

// unit test code
int i = 5;
var stub = new StubIStockFeedWithProperty();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

프로퍼티의 세터 또는 게터에 대한 스텁 메서드를 제공하지 않으면 Fakes는 값을 저장하는 스텁을 생성하여 스텁 프로퍼티가 단순한 변수처럼 동작하도록 합니다.

이벤트

이벤트가 델리게이트 필드로 노출되어 이벤트 백킹 필드를 호출하기만 하면 스텁된 이벤트를 발생시킬 수 있습니다. 예를 들어, 스텁할 다음과 같은 인터페이스를 생각해 볼 수 있습니다.

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

Changed 이벤트를 발생시키려면 백킹 델리게이트를 호출합니다:

// unit test code
var withEvents = new StubIStockFeedWithEvents();
// raising Changed
withEvents.ChangedEvent(withEvents, EventArgs.Empty);

제네릭 메서드

원하는 메소드의 인스턴스화마다 델리게이트를 제공하여 일반 메소드를 스텁할 수 있습니다. 예를 들어 일반 메서드가 있는 다음 인터페이스가 있다고 가정해 보겠습니다:

interface IGenericMethod
{
    T GetValue<T>();
}

다음과 같이 GetValue<int> 인스턴스화를 스텁할 수 있습니다:

[TestMethod]
public void TestGetValue()
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

코드가 다른 인스턴스화와 함께 GetValue<T>을 호출하면 스텁이 해당 동작을 실행합니다.

가상 클래스의 스텁

위 예제에서는 인터페이스에서 스텁이 생성되었습니다. 그러나 가상 또는 추상 멤버가 있는 클래스에서 스텁을 생성할 수도 있습니다. 예시:

// Base class in application under test
public abstract class MyClass
{
    public abstract void DoAbstract(string x);
    public virtual int DoVirtual(int n)
    {
        return n + 42;
    }

    public int DoConcrete()
    {
        return 1;
    }
}

이 클래스에서 생성한 스텁에서 DoAbstract()DoVirtual()에 대한 대리자 메서드를 설정할 수 있지만 DoConcrete()에 대한 대리자 메서드는 설정할 수 없습니다.

// unit test
var stub = new Fakes.MyClass();
stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
stub.DoVirtualInt32 = (n) => 10 ;

가상 메서드에 대한 델리게이트를 제공하지 않으면 가짜는 기본 동작을 제공하거나 기본 클래스에서 메서드를 호출할 수 있습니다. 기본 메서드를 호출하려면 CallBase 속성을 설정합니다.

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set - default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
// No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

스텁의 기본 동작 변경

생성된 각 스텁 유형은 IStub.InstanceBehavior 속성을 통해 IStubBehavior 인터페이스의 인스턴스를 보유합니다. 이 동작은 클라이언트가 연결된 사용자 정의 델리게이트가 없는 멤버를 호출할 때마다 호출됩니다. 이 동작이 설정되지 않은 경우 StubsBehaviors.Current 프로퍼티에서 반환된 인스턴스를 사용합니다. 기본적으로 이 속성은 NotImplementedException 예외를 throw하는 동작을 반환합니다.

스텁 인스턴스에서 InstanceBehavior 속성을 설정하여 언제든지 동작을 변경할 수 있습니다. 예를 들어, 다음 코드조각은 스텁이 아무 작업도 수행하지 않거나 반환 유형 default(T)의 기본값을 반환하도록 동작을 변경합니다:

// unit test code
var stub = new StockAnalysis.Fakes.StubIStockFeed();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

StubsBehaviors.Current 속성으로 동작이 설정되지 않은 모든 스텁 개체에 대해 전역적으로 동작을 변경할 수도 있습니다.

// Change default behavior for all stub instances where the behavior has not been set.
StubBehaviors.Current = BehavedBehaviors.DefaultValue;