다음을 통해 공유


shim을 사용하여 앱을 단위 테스트를 위해 격리할 수 있습니다.

Microsoft Fakes Framework에서 활용하는 두 가지 주요 기술 중 하나인 Shim 형식은 테스트 중에 앱의 구성 요소를 격리하는 데 중요한 역할을 합니다. 호출을 가로채고 특정 메서드로 전환하여 작동합니다. 그러면 테스트 내에서 사용자 지정 코드로 직접 연결할 수 있습니다. 이 기능을 사용하면 이러한 메서드의 결과를 관리하여 외부 조건에 관계없이 각 호출 중에 결과가 일관되고 예측 가능하도록 할 수 있습니다. 이 수준의 제어는 테스트 프로세스를 간소화하고 보다 안정적이고 정확한 결과를 달성하는 데 도움이 됩니다.

솔루션의 일부를 구성하지 않는 코드와 어셈블리 간에 경계를 만들어야 하는 경우 shim 을 사용합니다. 솔루션의 구성 요소를 서로 격리하는 것이 목표인 경우 스텁 을 사용하는 것이 좋습니다.

스텁에 대한 자세한 설명은 스텁을 사용하여 단위 테스트를 위해 애플리케이션의 일부를 서로 격리하는 방법을 참조하세요.

Shim 제한 사항

shim의 제한 사항에 유의하는 것이 중요합니다.

Shim은 .NET 기본 클래스의 특정 라이브러리, 특히 .NET Framework의 mscorlibSystem 및 .NET Core 또는 .NET 5+의 System.Runtime 에서 모든 형식에서 사용할 수 없습니다. 이 제약 조건은 성공적이고 효과적인 테스트 전략을 보장하기 위해 테스트 계획 및 디자인 단계 중에 고려해야 합니다.

Shim 만들기: 단계별 가이드

구성 요소에 다음 호출이 포함되어 있다고 가정합니다 System.IO.File.ReadAllLines.

// Code under test:
this.Records = System.IO.File.ReadAllLines(path);

클래스 라이브러리 만들기

  1. Visual Studio를 Class Library 열고 프로젝트 만들기

    Visual Studio의 NetFramework 클래스 라이브러리 프로젝트 스크린샷

  2. 프로젝트 이름 설정 HexFileReader

  3. 솔루션 이름을 ShimsTutorial설정합니다.

  4. 프로젝트의 대상 프레임워크를 .NET Framework 4.8로 설정

  5. 기본 파일 삭제 Class1.cs

  6. 새 파일을 HexFile.cs 추가하고 다음 클래스 정의를 추가합니다.

    // HexFile.cs
    public class HexFile
    {
        public string[] Records { get; private set; }
    
        public HexFile(string path)
        {
            this.Records = System.IO.File.ReadAllLines(path);
        }
    }
    

테스트 프로젝트 만들기

  1. 솔루션을 마우스 오른쪽 단추로 클릭하고 새 프로젝트 추가 MSTest Test Project

  2. 프로젝트 이름 설정 TestProject

  3. 프로젝트의 대상 프레임워크를 .NET Framework 4.8로 설정

    Visual Studio의 NetFramework 테스트 프로젝트 스크린샷

Fakes 어셈블리 추가

  1. 에 프로젝트 참조 추가 HexFileReader

    프로젝트 참조 추가 명령의 스크린샷.

  2. Fakes 어셈블리 추가

    • 솔루션 탐색기에서

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

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

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

    • System.IO.File.ReadAllLines의 정의가 포함된 어셈블리 System를 선택하십시오.

    • 바로 가기 메뉴에서 Fakes Assembly 추가를 선택합니다.

빌드하면 일부 경고 및 오류가 발생하므로 shim과 함께 모든 형식을 사용할 수 없기 때문에 해당 형식을 제외하기 위해 콘텐츠를 Fakes\mscorlib.fakes 수정해야 합니다.

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
  <Assembly Name="mscorlib" Version="4.0.0.0"/>
  <StubGeneration>
    <Clear/>
  </StubGeneration>
  <ShimGeneration>
    <Clear/>
    <Add FullName="System.IO.File"/>
    <Remove FullName="System.IO.FileStreamAsyncResult"/>
    <Remove FullName="System.IO.FileSystemEnumerableFactory"/>
    <Remove FullName="System.IO.FileInfoResultHandler"/>
    <Remove FullName="System.IO.FileSystemInfoResultHandler"/>
    <Remove FullName="System.IO.FileStream+FileStreamReadWriteTask"/>
    <Remove FullName="System.IO.FileSystemEnumerableIterator"/>
  </ShimGeneration>
</Fakes>

단위 테스트 만들기

  1. 다음을 추가하도록 기본 파일을 UnitTest1.cs 수정합니다. TestMethod

    [TestMethod]
    public void TestFileReadAllLine()
    {
        using (ShimsContext.Create())
        {
            // Arrange
            System.IO.Fakes.ShimFile.ReadAllLinesString = (s) => new string[] { "Hello", "World", "Shims" };
    
            // Act
            var target = new HexFile("this_file_doesnt_exist.txt");
    
            Assert.AreEqual(3, target.Records.Length);
        }
    }
    

    다음은 모든 파일을 보여 주는 솔루션 탐색기입니다.

    모든 파일을 보여 주는 솔루션 탐색기의 스크린샷.

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

각 shim 컨텍스트를 올바르게 삭제하는 것이 중요합니다. 일반적인 원칙으로, 문장 내에서 using 문에 대한 ShimsContext.Create 호출을 하여 등록된 쉼을 적절히 지우도록 보장합니다. 예를 들어, DateTime.Now 메서드를 항상 2000년 1월 1일을 반환하는 델리게이트로 대체하는 테스트 메서드에 대한 shim을 등록할 수 있습니다. 테스트 메서드에서 등록된 shim을 지우는 것을 잊어버린 경우 테스트 실행의 나머지 부분도 항상 2000년 1월의 첫 번째 shim을 DateTime.Now 값으로 반환합니다. 이것은 놀랍고 혼란스러울 수 있습니다.


Shim 클래스에 대한 명명 규칙

Shim 클래스 이름은 원래 형식 이름 앞에 접두사를 지정하여 Fakes.Shim 구성됩니다. 매개 변수 이름은 메서드 이름에 추가됩니다. (System.Fakes에 어셈블리 참조를 추가할 필요가 없습니다.)

    System.IO.File.ReadAllLines(path);
    System.IO.Fakes.ShimFile.ReadAllLinesString = (path) => new string[] { "Hello", "World", "Shims" };

Shim의 작동 방식 이해

Shim은 테스트 중인 애플리케이션의 코드베이스에 우회 를 도입하여 작동합니다. 원래 메서드에 대한 호출이 있을 때마다 Fakes 시스템은 해당 호출을 리디렉션하기 위해 개입하여 원래 메서드 대신 사용자 지정 shim 코드를 실행합니다.

이러한 우회는 런타임에 동적으로 만들어지고 제거됩니다. 우회는 항상 ShimsContext가 존재하는 동안 만들어야 합니다. ShimsContext가 삭제되면 그 안에 생성된 모든 활성 shim도 제거됩니다. 이를 효율적으로 관리하려면 문 내에서 using 우회 생성을 캡슐화하는 것이 좋습니다.


다양한 종류의 메서드에 사용하는 Shim

Shim은 다양한 형식의 메서드를 지원합니다.

정적 메서드

정적 메서드를 시밍할 때 shim을 보유하는 속성은 shim 형식 내에 저장됩니다. 이러한 속성에는 대상 메서드에 대리자를 연결하는 데 사용되는 setter만 있습니다. 예를 들어 정적 메서드MyMethod를 사용하여 호출 MyClass 된 클래스가 있는 경우:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

우리는 shim을 MyMethod에 연결하여 지속적으로 5를 반환할 수 있습니다.

// unit test code
ShimMyClass.MyMethod = () => 5;

인스턴스 메서드(모든 인스턴스의 경우)

정적 메서드와 마찬가지로 모든 인스턴스에 대해 인스턴스 메서드도 변형할 수 있습니다. 이러한 쉬미를 보유하는 속성들은 혼동을 방지하기 위해 AllInstances라는 중첩 형식에 배치됩니다. 인스턴스 메서드MyMethod를 사용하는 클래스 MyClass 가 있는 경우:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

인스턴스에 관계없이 지속적으로 5를 반환하도록 MyMethod shim을 연결할 수 있습니다.

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

생성된 형식 구조 ShimMyClass 는 다음과 같이 표시됩니다.

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

이 시나리오에서 Fakes는 런타임 인스턴스를 대리자의 첫 번째 인수로 전달합니다.

인스턴스 메서드(단일 런타임 인스턴스)

호출의 수신기에 따라 다른 대리자를 사용하여 인스턴스 메서드를 수정할 수도 있습니다. 이렇게 하면 동일한 인스턴스 메서드가 형식의 인스턴스당 다른 동작을 나타낼 수 있습니다. 이러한 shim을 보유하는 속성은 shim 형식 자체의 인스턴스 메서드입니다. 인스턴스화된 각 shim 형식은 shimmed 형식의 원시 인스턴스에 연결됩니다.

예를 들어 인스턴스 메서드MyMethod를 사용하는 클래스 MyClass 가 지정됩니다.

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

우리는 MyMethod에 대해 두 가지 shim 형식을 만들 수 있으며, 첫 번째는 일관되게 5를 반환하고 두 번째는 일관되게 10을 반환합니다.

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

생성된 형식 구조 ShimMyClass 는 다음과 같이 표시됩니다.

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

실제 shimmed 형식 인스턴스는 Instance 속성을 통해 액세스할 수 있습니다.

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

또한 shim 형식에는 shim 형식을 직접 사용할 수 있는 shimmed 형식으로의 암시적 변환이 포함됩니다.

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime instance

생성자

생성자도 쉬밍의 예외가 아닙니다. 미래에 생성될 객체에 쉬밍 형식을 부착하기 위해 생성자 역시 쉬밍될 수 있습니다. 예를 들어 모든 생성자는 shim 형식 내에서 명명 Constructor된 정적 메서드로 표시됩니다. 정수에 허용하는 생성자가 있는 클래스 MyClass 를 살펴보겠습니다.

public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

생성자에 전달된 값에 관계없이 Value getter가 호출될 때 모든 이후 인스턴스가 -5 반환되도록 생성자에 대한 shim 형식을 설정할 수 있습니다.

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

각 shim 형식은 두 가지 유형의 생성자를 노출합니다. 새 인스턴스가 필요한 경우 기본 생성자를 사용해야 하는 반면, shimmed 인스턴스를 인수로 사용하는 생성자는 생성자 shim에서만 사용해야 합니다.

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

생성된 형식 ShimMyClass 의 구조는 다음과 같이 설명될 수 있습니다.

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

기본 멤버 접근하기

기본 멤버의 Shim 속성은 기본 형식에 대한 shim을 만들고 자식 인스턴스를 기본 shim 클래스의 생성자에 제공하여 접근할 수 있습니다.

예를 들어 인스턴스 메서드 MyMethod 와 하위 형식MyChild이 있는 클래스 MyBase 를 고려합니다.

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

MyBase에 대한 쉼은 새로운 ShimMyBase 쉼을 시작하여 설정할 수 있습니다.

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

기본 shim 생성자에 매개 변수로 전달될 때 자식 shim 형식은 암시적으로 자식 인스턴스로 변환됩니다.

생성된 형식 ShimMyChild 의 구조이며 ShimMyBase 다음 코드에 비유할 수 있습니다.

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

정적 생성자

Shim 형식은 정적 메서드 StaticConstructor 를 노출하여 형식의 정적 생성자를 shim합니다. 정적 생성자는 한 번만 실행되므로 형식의 멤버에 액세스하기 전에 shim이 구성되었는지 확인해야 합니다.

파이널라이저

종료자는 Fakes에서 지원되지 않습니다.

프라이빗 메서드

Fakes 코드 생성기는 서명에 표시되는 형식, 즉 매개 변수 형식 및 반환 형식만 표시되는 프라이빗 메서드에 대한 shim 속성을 만듭니다.

바인딩 인터페이스

shimmed 형식이 인터페이스를 구현하는 경우 코드 생성기는 해당 인터페이스의 모든 멤버를 한 번에 바인딩할 수 있는 메서드를 내보낸다.

예를 들어 다음을 구현하는 클래스 MyClass 가 제공됩니다.IEnumerable<int>

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

Bind 메서드를 호출하여 MyClass의 IEnumerable<int> 구현을 shim할 수 있습니다.

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

생성된 형식 구조 ShimMyClass 는 다음 코드와 유사합니다.

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

기본 동작 변경

생성된 각 shim 형식에는 IShimBehavior 인터페이스의 인스턴스를 포함하며, 이 인스턴스는 ShimBase<T>.InstanceBehavior 속성을 통해 액세스할 수 있습니다. 클라이언트가 명시적으로 쉼이 적용되지 않은 인스턴스 멤버를 호출할 때마다 이 동작이 발생합니다.

기본적으로 특정 동작이 설정되지 않은 경우 정적 ShimBehaviors.Current 속성에서 반환된 인스턴스를 사용하며, 이는 일반적으로 NotImplementedException 예외를 throw합니다.

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

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimBehaviors.DefaultValue;

또한 정적 ShimBehaviors.Current 속성을 설정하여 속성이 명시적으로 정의되지 않은 모든 shimmed 인스턴스 InstanceBehavior 의 동작을 전역적으로 변경할 수도 있습니다.

// unit test code
// change default shim for all shim instances where the behavior has not been set
ShimBehaviors.Current = ShimBehaviors.DefaultValue;

외부 종속성과의 상호 작용 식별

코드가 외부 시스템 또는 종속성(이라고도 함 environment)과 상호 작용하는 시기를 식별하기 위해 shim을 활용하여 형식의 모든 멤버에 특정 동작을 할당할 수 있습니다. 여기에는 정적 메서드가 포함됩니다. shim 형식의 정적 Behavior 속성에 ShimBehaviors.NotImplemented 동작을 설정하면, 명시적으로 셈 처리가 되지 않은 해당 형식의 멤버에 대한 액세스는 NotImplementedException를 발생시킵니다. 이는 테스트 중에 코드가 외부 시스템 또는 종속성에 액세스하려고 함을 나타내는 유용한 신호로 사용될 수 있습니다.

단위 테스트 코드에서 이를 설정하는 방법의 예는 다음과 같습니다.

// unit test code
// Assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.Behavior = ShimBehaviors.NotImplemented;

편의를 위해 동일한 효과를 얻기 위해 약식 메서드도 제공됩니다.

// Shorthand to assign the NotImplementedException behavior to ShimMyClass
ShimMyClass.BehaveAsNotImplemented();

Shim 메서드 내에서 원래 메서드를 호출하기

shim 메서드를 실행하는 동안 원래 메서드를 실행해야 하는 시나리오가 있을 수 있습니다. 예를 들어 메서드에 전달된 파일 이름의 유효성을 검사한 후 파일 시스템에 텍스트를 쓸 수 있습니다.

이 상황을 처리하는 한 가지 방법은 다음 코드에 설명된 대로 대리 ShimsContext.ExecuteWithoutShims()자를 사용하여 원래 메서드에 대한 호출을 캡슐화하는 것입니다.

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

또는 shim을 비활성화하고 원래 메서드를 호출한 다음 shim을 원래 상태로 복원할 수 있습니다.

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter");
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

Shim 형식을 사용하여 동시성 처리

Shim 형식은 AppDomain 내의 모든 스레드에서 작동하며 스레드 선호도를 가지고 있지 않습니다. 이 속성은 동시성을 지원하는 테스트 실행기를 활용하려는 경우에 유의해야 합니다. 이 제한은 Fakes 런타임에 의해 적용되지 않지만 shim 형식과 관련된 테스트는 동시에 실행할 수 없습니다.

Shimming System.Environment

클래스를 System.Environment 변경하려면 mscorlib.fakes 파일을 수정해야 합니다. Assembly 요소 다음에 다음 콘텐츠를 추가합니다.

<ShimGeneration>
    <Add FullName="System.Environment"/>
</ShimGeneration>

이러한 변경 내용을 적용하고 솔루션을 다시 빌드한 후에는 클래스의 System.Environment 메서드와 속성을 시밍할 수 있습니다. 다음은 메서드에 동작을 할당하는 방법의 예입니다 GetCommandLineArgsGet .

System.Fakes.ShimEnvironment.GetCommandLineArgsGet = ...

이러한 수정을 통해 포괄적인 단위 테스트를 위한 필수 도구인 시스템 환경 변수와 코드가 상호 작용하는 방식을 제어하고 테스트할 수 있습니다.