Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 

(시리즈 글이 8개 있습니다.)
VS.NET IDE: 105. Visual Studio의 단위 테스트 작성 시 Fakes를 이용한 메서드 재정의 방법
; https://www.sysnet.pe.kr/2/0/10858

VS.NET IDE: 169. 비주얼 스튜디오 - 단위 테스트 선택 시 MSTestv2 외의 xUnit, NUnit 사용법
; https://www.sysnet.pe.kr/2/0/12726

.NET Framework: 1078. C# 단위 테스트 - MSTestv2/NUnit의 Assert.Inconclusive 사용법(?)
; https://www.sysnet.pe.kr/2/0/12727

.NET Framework: 1079. MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공
; https://www.sysnet.pe.kr/2/0/12728

.NET Framework: 1080. xUnit 단위 테스트에 메서드/클래스 수준의 문맥 제공 - Fixture
; https://www.sysnet.pe.kr/2/0/12729

개발 환경 구성: 590. Visual Studio 2017부터 단위 테스트에 DataRow 특성 지원
; https://www.sysnet.pe.kr/2/0/12749

개발 환경 구성: 593. MSTest - 단위 테스트에 static/instance 유형의 private 멤버 접근 방법
; https://www.sysnet.pe.kr/2/0/12755

.NET Framework: 1084. C# - .NET Core Web API 단위 테스트 방법
; https://www.sysnet.pe.kr/2/0/12756




MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공

그냥 바로 코드를 보는 것이 빠르겠죠? ^^

예를 들기 위해, DLL 프로젝트를 만들어 다음의 코드를 추가한 다음,

using System;

namespace ClassLibrary1
{
    public class Class1
    {
        public int Add(int x, int y)
        {
            return x + y;
        }
    }
}

Add 메서드에 마우스 우클릭을 해 "Create Unit Tests..." 메뉴를 선택, 이어지는 "Create Unit Tests" 창에서는 그냥 기본값으로 두고 "OK" 버튼을 눌러 단위 테스트 프로젝트와 Add 메서드에서 대한 단위 테스트 코드의 뼈대를 생성합니다.

그럼 대충 AddTest 메서드 내의 코드를 다음과 같은 식으로 바꿔 단위 테스트 코드를 완성하게 될 것입니다.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ClassLibrary1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1.Tests
{
    [TestClass()]
    public class Class1Tests
    {
        [TestMethod()]
        public void AddTest()
        {
            Class1 cl = new Class1();
            Assert.AreEqual(4, cl.Add(1, 3));
        }
    }
}

이 상태에서 Add 메서드뿐만 아니라 Subtract를 추가하면,

public class Class1
{
    // ...[생략]...

    public int Subtract(int x, int y)
    {
        return x - y;
    }
}

단위 테스트도 이렇게 만들어 줄 것입니다.

[TestClass()]
public class Class1Tests
{
    // ...[생략]...

    [TestMethod()]
    public void SubtractTest()
    {
        Class1 cl = new Class1();
        Assert.AreEqual(-2, cl.Subtract(1, 3));
    }
}

슬슬 중복 코드가 나오는데요, Class1 cl 인스턴스를 매번 생성해야 하는 것입니다. 혹시 이럴 때 "단위 테스트 메서드" 레벨로 매번 수행하는 코드를 작성하면 되지 않을까요?

바로 그런 목적으로 나온 것이 "[TestInitialize]" 특성입니다. 그래서, 단위 테스트 코드를 다음과 같은 식으로 바꿀 수 있습니다.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using ClassLibrary1;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ClassLibrary1.Tests
{
    [TestClass()]
    public class Class1Tests
    {
        Class1 _cl;

        [TestInitialize]
        public void Init_Per_Method() // 반드시 public 접근자에 반환 및 인자는 없음
        {
            _cl = new Class1();
        }

        [TestMethod()]
        public void AddTest()
        {
            Assert.AreEqual(4, _cl.Add(1, 3));
        }

        [TestMethod()]
        public void SubtractTest()
        {
            Assert.AreEqual(-2, _cl.Subtract(1, 3));
        }
    }
}

즉, 위와 같은 환경에서 단위 테스트의 실행 순서는 이렇게 됩니다.

Init_Per_Method
    AddTest

Init_Per_Method
    SubtractTest

(우리가 만든 클래스에서는 필요 없지만) 만약 Class1 타입이 IDisposable도 구현했다면,

public class Class1 : IDisposable
{
    // ...[생략]...

    public void Dispose() { /* ... */ }
}

이제 단위 테스트의 메서드마다 수행되는 Cleanup 호출도 있어야 할 것입니다. 그리고 당연히 이것도 TestCleanup 특성을 이용해 제공할 수 있습니다.

[TestClass()]
public class Class1Tests
{
    // ...[생략]...

    [TestCleanup]
    public void Clean_Per_Method() // 반드시 public 접근자에 반환 및 인자는 없음
    {
        _cl.Dispose();
    }
}

예상할 수 있듯이, 이제 실행 순서는 다음과 같게 됩니다.

Init_Per_Method
    AddTest
Clean_Per_Method

Init_Per_Method
    SubtractTest
Clean_Per_Method




그런데, 사실 TestInitialize/TestCleanup이 일반적으로는 크게 필요하지 않습니다. 왜냐하면 이미 단위 테스트를 위한 클래스(위의 경우 Class1Tests)는 메서드 호출마다 새로운 인스턴스가 생성되어 호출이 되기 때문입니다. 실제로 위의 코드는 다음과 같이 변경해도 동일하게 동작합니다.

[TestClass()]
public class Class1Tests : IDisposable
{
    Class1 _cl = new Class1();

    /* 또는,
    public Class1Tests()
    {
        _cl = new Class1();
    }
    */

    public void Dispose()
    {
        _cl.Dispose();
    }

    [TestMethod()]
    public void AddTest()
    {
        Assert.AreEqual(4, _cl.Add(1, 3));
    }

    [TestMethod()]
    public void SubtractTest()
    {
        Assert.AreEqual(-2, _cl.Subtract(1, 3));
    }
}

단지, 몇몇 특수한 사례들이 있는데요, 이에 대해서는 아래의 글에 답변이 나옵니다.

Do you use TestInitialize or the test class constructor to prepare each test? and why?
; https://stackoverflow.com/questions/334515/do-you-use-testinitialize-or-the-test-class-constructor-to-prepare-each-test-an

가령, (아래에서 설명하게 될) TestContext 개체가 Class1Tests 생성자 호출 시점에는 아직 초기화가 안 됐다거나, 생성자는 async 메서드로 만들 수 없지만 TestInitialize 특성이 부여된 메서드는 만들 수 있다는 식의 (거의 사용할 것 같지 않은) 차이만 있습니다.




"단위 테스트 메서드"마다 수행되는 TestInitialize/TestCleanup가 있다면, 당연히 "단위 테스트 클래스"마다 수행되는 특성도 있을 것입니다. 즉, ClassInitialize, ClassCleanup 특성이 바로 그것입니다.

예를 들어, SqlConnection을 클래스 수준에서 공통으로 제공하고 싶다면 다음과 같이 문맥을 제공할 수 있습니다.

[TestClass()]
public class Class1Tests
{
    Class1 _cl;
    static SqlConnection _sqlCon;

    [ClassInitialize]
    public static void Init_Per_Class(TestContext context) // 반드시 public static 접근에, 인자로 TestContext
    {
        _sqlCon = new SqlConnection();
    }

    [ClassCleanup]
    public static void Clean_Per_Class() // 반드시 public static 접근에, 인자 및 반환값 없음
    {
        _sqlCon.Dispose();
    }        

    // ...[생략]...
}

그럼 이제 실행 순서는 다음과 같이 조정이 됩니다.

Init_Per_Class
    Init_Per_Method
        AddTest
    Clean_Per_Method

    Init_Per_Method
        SubtractTest
    Clean_Per_Method
Clean_Per_Class




ClassInitialize 특성이 부여된 메서드의 경우 TestContext 인자를 하나 받는데, 이것은 나중에 보관해 테스트 환경에 대한 정보를 얻고 싶을 때 사용할 수 있습니다.

static TestContext _classContext;

[ClassInitialize]
public static void Init_Per_Class(TestContext context)
{
    _classContext = context;
}

[TestMethod]
public void ExampleTest()
{
    _classContext.WriteLine(_classContext.TestRunDirectory);
}

그런데, ClassInitialize를 사용하지 않는다면 TestContext도 사용할 수 없는 걸까요? 이게 좀 재미있습니다. TestContext도 static과 instance 레벨로 각각 제공되는데요, static 범위는 저렇게 ClassInitialize의 인자로 받지만, instance 레벨의 경우에는 일종의 Property Injection과 같은 방식으로 받게 됩니다.

TestContext _methodContext;

public TestContext TestContext
{
    get { return _testContext; }
    set { _methodContext = value; }
}

여기에는 약간 미묘한 정도의 차이가 있는데요, 가령 다음과 같이 테스트 메서드마다 _classContext를 사용한 경우,

[TestMethod()]
public void AddTest()
{
    Assert.AreEqual(4, _cl.Add(1, 3));
    _classContext.WriteLine($"{_classContext.TestName}, {_classContext.FullyQualifiedTestClassName}");
}

[TestMethod()]
public void SubtractTest()
{
    Assert.AreEqual(-2, _cl.Subtract(1, 3));
    _classContext.WriteLine($"{_classContext.TestName}, {_classContext.FullyQualifiedTestClassName}");
}

개별 메서드 단위로 단위 테스트를 수행했을 때는 다음과 같이 TestContext.WriteLine의 메시지가 잘 나오지만,

vs_unittest_context_1.png

"Run All Tests"로 전체 테스트를 했을 때는 해당 메시지가 나오지 않습니다.

vs_unittest_context_2.png

대신, 위의 경우 첫 번째 테스트에 위치한 "AddTest"의 경우에는 _classContext.WriteLine의 결과가 나옵니다. 이런 이상한 출력 규칙을 해결하려면 (_classContext가 아닌) _methodContext를 사용해야 합니다.

[TestMethod()]
public void AddTest()
{
    Assert.AreEqual(4, _cl.Add(1, 3));
    _methodContext.WriteLine($"{_methodContext.TestName}, {_methodContext.FullyQualifiedTestClassName}");
}

[TestMethod()]
public void SubtractTest()
{
    Assert.AreEqual(-2, _cl.Subtract(1, 3));
    _methodContext.WriteLine($"{_methodContext.TestName}, {_methodContext.FullyQualifiedTestClassName}");
}

엄밀히, "Run All Tests" 시의 Context Message 출력은 _methodContext.WriteLine만 사용하면 됩니다. 하지만, 다른 출력값을 _classContext로 사용하면,

[TestMethod()]
public void AddTest()
{
    Assert.AreEqual(4, _cl.Add(1, 3));
    _methodContext.WriteLine($"{_classContext.TestName}, {_classContext.FullyQualifiedTestClassName}");
}

[TestMethod()]
public void SubtractTest()
{
    Assert.AreEqual(-2, _cl.Subtract(1, 3));
    _methodContext.WriteLine($"{_classContext.TestName}, {_classContext.FullyQualifiedTestClassName}");
}

위와 같은 경우 _methodContext.WriteLine으로 인해 "Run All Tests" 시 모두 결과가 나오지만, _classContext 수준에서는 TestName이 정상적으로 설정이 되지 않아 다음과 같이,

vs_unittest_context_3.png

엉뚱한 결과가 나옵니다. (당연히 AddTest의 MessageContxt만 올바른 결과를 냅니다.) 위와 같은 것을 이해하지 못해서 나오는 질문이 바로 아래와 같은 유형입니다. ^^

Understanding the MSTest TestContext
; https://stackoverflow.com/questions/24249133/understanding-the-mstest-testcontext

현실적으로 봤을 때, 저런 이상한 동작을 고려해 보면 _classContext를 사용할 일은 거의 없을 것 같고, _methodContext로 단일화하는 것이 좋을 것입니다. 게다가 사실 위에서 길게 설명하긴 했지만 TestContext 자체마저도 그다지 쓸 일이 거의 없습니다.

참고로, TestContext의 속성 및 메서드는 다음과 같은 것들이 있습니다.

  • DeploymentDirectory
  • ResultsDirectory
  • TestRunResultsDirectory
  • TestResultsDirectory
  • TestDir
  • TestDeploymentDir
  • TestLogsDir
  • FullyQualifiedTestClassName
  • TestName
  • CurrentTestOutcome
  • AddResultFile(string fileName);
  • WriteLine(string format, params object[] args);




기타, 마지막으로 역시 잘 사용하지는 않지만 어셈블리 수준의 문맥을 제공하는 Init/Clean도 있습니다.

AssemblyInitialize, AssemblyCleanup

이것 역시 ClassInitialize/ClassCleanup과 메서드 signature에 유사한 제약을 갖는데,

[AssemblyInitialize]
public static void Init_Per_Assembly(TestContext context)
{
}

[AssemblyCleanup]
public static void Clean_Per_Assembly()
{
}

거의 쓸 일이 희박할 것 같습니다. ^^

그 외, 관련해서 다음의 도움말을 보면 보다 더 자세한 정보를 얻을 수 있습니다.

Use the MSTest framework in unit tests
; https://learn.microsoft.com/en-us/visualstudio/test/using-microsoft-visualstudio-testtools-unittesting-members-in-unit-tests




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]







[최초 등록일: ]
[최종 수정일: 7/28/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13229정성태1/25/20235093.NET Framework: 2090. C# - UDP Datagram의 최대 크기
13228정성태1/24/20235218.NET Framework: 2089. C# - WMI 논리 디스크가 속한 물리 디스크의 정보를 얻는 방법 [2]파일 다운로드1
13227정성태1/23/20234920개발 환경 구성: 659. Windows - IP MTU 값을 바꿀 수 있을까요? [1]
13226정성태1/23/20234587.NET Framework: 2088. .NET 5부터 지원하는 GetRawSocketOption 사용 시 주의할 점
13225정성태1/21/20233841개발 환경 구성: 658. Windows에서 실행 중인 소켓 서버를 다른 PC 또는 WSL에서 접속할 수 없는 경우
13224정성태1/21/20234196Windows: 221. Windows - Private/Public/Domain이 아닌 네트워크 어댑터 단위로 방화벽을 on/off하는 방법
13223정성태1/20/20234393오류 유형: 838. RDP 연결 오류 - The two computers couldn't connect in the amount of time allotted
13222정성태1/20/20234069개발 환경 구성: 657. WSL - DockerDesktop.vhdx 파일 위치를 옮기는 방법
13221정성태1/19/20234302Linux: 57. C# - 리눅스 프로세스 메모리 정보파일 다운로드1
13220정성태1/19/20234419오류 유형: 837. NETSDK1045 The current .NET SDK does not support targeting .NET ...
13219정성태1/18/20233990Windows: 220. 네트워크의 인터넷 접속 가능 여부에 대한 판단 기준
13218정성태1/17/20233927VS.NET IDE: 178. Visual Studio 17.5 (Preview 2) - 포트 터널링을 이용한 웹 응용 프로그램의 외부 접근 허용
13217정성태1/13/20234504디버깅 기술: 185. windbg - 64비트 운영체제에서 작업 관리자로 뜬 32비트 프로세스의 덤프를 sos로 디버깅하는 방법
13216정성태1/12/20234765디버깅 기술: 184. windbg - 32비트 프로세스의 메모리 덤프인 경우 !peb 명령어로 나타나지 않는 환경 변수
13215정성태1/11/20236322Linux: 56. 리눅스 - /proc/pid/stat 정보를 이용해 프로세스의 CPU 사용량 구하는 방법 [1]
13214정성태1/10/20235869.NET Framework: 2087. .NET 6부터 SourceGenerator와 통합된 System.Text.Json [1]파일 다운로드1
13213정성태1/9/20235410오류 유형: 836. docker 이미지 빌드 시 "RUN apt install ..." 명령어가 실패하는 이유
13212정성태1/8/20235159기타: 85. 단정도/배정도 부동 소수점의 정밀도(Precision)에 따른 형변환 손실
13211정성태1/6/20235188웹: 42. (https가 아닌) http 다운로드를 막는 웹 브라우저
13210정성태1/5/20234279Windows: 219. 윈도우 x64의 경우 0x00000000`7ffe0000 아래의 주소는 왜 사용하지 않을까요?
13209정성태1/4/20234178Windows: 218. 왜 윈도우에서 가상 메모리 공간은 64KB 정렬이 된 걸까요?
13208정성태1/3/20234137.NET Framework: 2086. C# - Windows 운영체제의 2MB Large 페이지 크기 할당 방법파일 다운로드1
13207정성태12/26/20224434.NET Framework: 2085. C# - gpedit.msc의 "User Rights Assignment" 특권을 코드로 설정/해제하는 방법파일 다운로드1
13206정성태12/24/20224665.NET Framework: 2084. C# - GetTokenInformation으로 사용자 SID(Security identifiers) 구하는 방법 [3]파일 다운로드1
13205정성태12/24/20224981.NET Framework: 2083. C# - C++과의 연동을 위한 구조체의 fixed 배열 필드 사용 (2)파일 다운로드1
13204정성태12/22/20224276.NET Framework: 2082. C# - (LSA_UNICODE_STRING 예제로) CustomMarshaler 사용법파일 다운로드1
... [16]  17  18  19  20  21  22  23  24  25  26  27  28  29  30  ...