성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 그냥 RSS Reader 기능과 약간의 UI 편의성 때문에 사용...
[이종효] 오래된 소프트웨어는 보안 위협이 되기도 합니다. 혹시 어떤 기능...
[정성태] @Keystroke IEEE의 문서를 소개해 주시다니... +_...
[손민수 (Keystroke)] 괜히 듀얼채널 구성할 때 한번에 같은 제품 사라고 하는 것이 아...
[정성태] 전각(Full-width)/반각(Half-width) 기능을 토...
[정성태] Vector에 대한 내용은 없습니다. Vector가 닷넷 BCL...
[orion] 글 읽고 찾아보니 디자인 타임에는 InitializeCompon...
[orion] 연휴 전에 재현 프로젝트 올리자 생각해 놓고 여의치 않아서 못 ...
[정성태] 아래의 글에 정리했으니 참고하세요. C# - Typed D...
[정성태] 간단한 재현 프로젝트라도 있을까요? 저런 식으로 설명만 해...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>MSTestv2 단위 테스트에 메서드/클래스/어셈블리 수준의 문맥 제공</h1> <p> 그냥 바로 코드를 보는 것이 빠르겠죠? ^^<br /> <br /> 예를 들기 위해, DLL 프로젝트를 만들어 다음의 코드를 추가한 다음,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; namespace ClassLibrary1 { public class Class1 { public int Add(int x, int y) { return x + y; } } } </pre> <br /> Add 메서드에 마우스 우클릭을 해 "Create Unit Tests..." 메뉴를 선택, 이어지는 "Create Unit Tests" 창에서는 그냥 기본값으로 두고 "OK" 버튼을 눌러 단위 테스트 프로젝트와 Add 메서드에서 대한 단위 테스트 코드의 뼈대를 생성합니다.<br /> <br /> 그럼 대충 AddTest 메서드 내의 코드를 다음과 같은 식으로 바꿔 단위 테스트 코드를 완성하게 될 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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 { <span style='color: blue; font-weight: bold'>[TestClass()]</span> public class Class1Tests { <span style='color: blue; font-weight: bold'>[TestMethod()]</span> public void AddTest() { Class1 cl = new Class1(); Assert.AreEqual(4, cl.Add(1, 3)); } } } </pre> <br /> 이 상태에서 Add 메서드뿐만 아니라 Subtract를 추가하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class Class1 { // ...[생략]... public int Subtract(int x, int y) { return x - y; } } </pre> <br /> 단위 테스트도 이렇게 만들어 줄 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestClass()] public class Class1Tests { // ...[생략]... [TestMethod()] public void SubtractTest() { Class1 cl = new Class1(); Assert.AreEqual(-2, cl.Subtract(1, 3)); } } </pre> <br /> 슬슬 중복 코드가 나오는데요, Class1 cl 인스턴스를 매번 생성해야 하는 것입니다. 혹시 이럴 때 "단위 테스트 메서드" 레벨로 매번 수행하는 코드를 작성하면 되지 않을까요?<br /> <br /> 바로 그런 목적으로 나온 것이 "[TestInitialize]" 특성입니다. 그래서, 단위 테스트 코드를 다음과 같은 식으로 바꿀 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 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; <span style='color: blue; font-weight: bold'>[TestInitialize] public void Init_Per_Method() // 반드시 public 접근자에 반환 및 인자는 없음 { _cl = new Class1(); }</span> [TestMethod()] public void AddTest() { Assert.AreEqual(4, _cl.Add(1, 3)); } [TestMethod()] public void SubtractTest() { Assert.AreEqual(-2, _cl.Subtract(1, 3)); } } } </pre> <br /> 즉, 위와 같은 환경에서 단위 테스트의 실행 순서는 이렇게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Init_Per_Method AddTest Init_Per_Method SubtractTest </pre> <br /> (우리가 만든 클래스에서는 필요 없지만) 만약 Class1 타입이 IDisposable도 구현했다면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > public class Class1 <span style='color: blue; font-weight: bold'>: IDisposable</span> { // ...[생략]... <span style='color: blue; font-weight: bold'>public void Dispose() { /* ... */ }</span> } </pre> <br /> 이제 단위 테스트의 메서드마다 수행되는 Cleanup 호출도 있어야 할 것입니다. 그리고 당연히 이것도 TestCleanup 특성을 이용해 제공할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestClass()] public class Class1Tests { // ...[생략]... <span style='color: blue; font-weight: bold'>[TestCleanup] public void Clean_Per_Method() // 반드시 public 접근자에 반환 및 인자는 없음 { _cl.Dispose(); }</span> } </pre> <br /> 예상할 수 있듯이, 이제 실행 순서는 다음과 같게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Init_Per_Method AddTest Clean_Per_Method Init_Per_Method SubtractTest Clean_Per_Method </pre> <br /> <hr style='width: 50%' /><br /> <br /> 그런데, 사실 TestInitialize/TestCleanup이 일반적으로는 크게 필요하지 않습니다. 왜냐하면 이미 단위 테스트를 위한 클래스(위의 경우 Class1Tests)는 메서드 호출마다 새로운 인스턴스가 생성되어 호출이 되기 때문입니다. 실제로 위의 코드는 다음과 같이 변경해도 동일하게 동작합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestClass()] public class Class1Tests<span style='color: blue; font-weight: bold'> : IDisposable</span> { <span style='color: blue; font-weight: bold'>Class1 _cl = new Class1();</span> /* 또는, public Class1Tests() { _cl = new Class1(); } */ <span style='color: blue; font-weight: bold'>public void Dispose() { _cl.Dispose(); }</span> [TestMethod()] public void AddTest() { Assert.AreEqual(4, _cl.Add(1, 3)); } [TestMethod()] public void SubtractTest() { Assert.AreEqual(-2, _cl.Subtract(1, 3)); } } </pre> <br /> 단지, 몇몇 특수한 사례들이 있는데요, 이에 대해서는 아래의 글에 답변이 나옵니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Do you use TestInitialize or the test class constructor to prepare each test? and why? ; <a target='tab' href='https://stackoverflow.com/questions/334515/do-you-use-testinitialize-or-the-test-class-constructor-to-prepare-each-test-an'>https://stackoverflow.com/questions/334515/do-you-use-testinitialize-or-the-test-class-constructor-to-prepare-each-test-an</a> </pre> <br /> 가령, (아래에서 설명하게 될) TestContext 개체가 Class1Tests 생성자 호출 시점에는 아직 초기화가 안 됐다거나, 생성자는 async 메서드로 만들 수 없지만 TestInitialize 특성이 부여된 메서드는 만들 수 있다는 식의 (거의 사용할 것 같지 않은) 차이만 있습니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> "단위 테스트 메서드"마다 수행되는 TestInitialize/TestCleanup가 있다면, 당연히 "단위 테스트 클래스"마다 수행되는 특성도 있을 것입니다. 즉, ClassInitialize, ClassCleanup 특성이 바로 그것입니다.<br /> <br /> 예를 들어, SqlConnection을 클래스 수준에서 공통으로 제공하고 싶다면 다음과 같이 문맥을 제공할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestClass()] public class Class1Tests { Class1 _cl; <span style='color: blue; font-weight: bold'>static SqlConnection _sqlCon;</span> <span style='color: blue; font-weight: bold'>[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(); } </span> // ...[생략]... } </pre> <br /> 그럼 이제 실행 순서는 다음과 같이 조정이 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>Init_Per_Class</span> Init_Per_Method AddTest Clean_Per_Method Init_Per_Method SubtractTest Clean_Per_Method <span style='color: blue; font-weight: bold'>Clean_Per_Class</span> </pre> <br /> <hr style='width: 50%' /><br /> <br /> ClassInitialize 특성이 부여된 메서드의 경우 TestContext 인자를 하나 받는데, 이것은 나중에 보관해 테스트 환경에 대한 정보를 얻고 싶을 때 사용할 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>static TestContext _classContext;</span> [ClassInitialize] public static void Init_Per_Class(<span style='color: blue; font-weight: bold'>TestContext context</span>) { <span style='color: blue; font-weight: bold'>_classContext = context;</span> } [TestMethod] public void ExampleTest() { <span style='color: blue; font-weight: bold'>_classContext.WriteLine(_classContext.TestRunDirectory);</span> } </pre> <br /> 그런데, ClassInitialize를 사용하지 않는다면 TestContext도 사용할 수 없는 걸까요? 이게 좀 재미있습니다. TestContext도 static과 instance 레벨로 각각 제공되는데요, static 범위는 저렇게 ClassInitialize의 인자로 받지만, instance 레벨의 경우에는 일종의 Property Injection과 같은 방식으로 받게 됩니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > <span style='color: blue; font-weight: bold'>TestContext _methodContext;</span> <span style='color: blue; font-weight: bold'>public TestContext TestContext</span> { get { return _testContext; } set { _methodContext = value; } } </pre> <br /> 여기에는 약간 미묘한 정도의 차이가 있는데요, 가령 다음과 같이 테스트 메서드마다 _classContext를 사용한 경우,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public void AddTest() { Assert.AreEqual(4, _cl.Add(1, 3)); <span style='color: blue; font-weight: bold'>_classContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_classContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_classContext</span>.FullyQualifiedTestClassName}"); } [TestMethod()] public void SubtractTest() { Assert.AreEqual(-2, _cl.Subtract(1, 3)); <span style='color: blue; font-weight: bold'>_classContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_classContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_classContext</span>.FullyQualifiedTestClassName}"); } </pre> <br /> 개별 메서드 단위로 단위 테스트를 수행했을 때는 다음과 같이 TestContext.WriteLine의 메시지가 잘 나오지만,<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='vs_unittest_context_1.png' src='/SysWebRes/bbs/vs_unittest_context_1.png' /><br /> <br /> "Run All Tests"로 전체 테스트를 했을 때는 해당 메시지가 나오지 않습니다.<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='vs_unittest_context_2.png' src='/SysWebRes/bbs/vs_unittest_context_2.png' /><br /> <br /> 대신, 위의 경우 첫 번째 테스트에 위치한 "AddTest"의 경우에는 _classContext.WriteLine의 결과가 나옵니다. 이런 이상한 출력 규칙을 해결하려면 (_classContext가 아닌) _methodContext를 사용해야 합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public void AddTest() { Assert.AreEqual(4, _cl.Add(1, 3)); <span style='color: blue; font-weight: bold'>_methodContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_methodContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_methodContext</span>.FullyQualifiedTestClassName}"); } [TestMethod()] public void SubtractTest() { Assert.AreEqual(-2, _cl.Subtract(1, 3)); <span style='color: blue; font-weight: bold'>_methodContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_methodContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_methodContext</span>.FullyQualifiedTestClassName}"); } </pre> <br /> 엄밀히, "Run All Tests" 시의 Context Message 출력은 _methodContext.WriteLine만 사용하면 됩니다. 하지만, 다른 출력값을 _classContext로 사용하면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [TestMethod()] public void AddTest() { Assert.AreEqual(4, _cl.Add(1, 3)); <span style='color: blue; font-weight: bold'>_methodContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_classContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_classContext</span>.FullyQualifiedTestClassName}"); } [TestMethod()] public void SubtractTest() { Assert.AreEqual(-2, _cl.Subtract(1, 3)); <span style='color: blue; font-weight: bold'>_methodContext</span>.WriteLine($"{<span style='color: blue; font-weight: bold'>_classContext</span>.TestName}, {<span style='color: blue; font-weight: bold'>_classContext</span>.FullyQualifiedTestClassName}"); } </pre> <br /> 위와 같은 경우 _methodContext.WriteLine으로 인해 "Run All Tests" 시 모두 결과가 나오지만, _classContext 수준에서는 TestName이 정상적으로 설정이 되지 않아 다음과 같이,<br /> <br /> <img onclick='toggle_img(this)' class='imgView' alt='vs_unittest_context_3.png' src='/SysWebRes/bbs/vs_unittest_context_3.png' /><br /> <br /> 엉뚱한 결과가 나옵니다. (당연히 AddTest의 MessageContxt만 올바른 결과를 냅니다.) 위와 같은 것을 이해하지 못해서 나오는 질문이 바로 아래와 같은 유형입니다. ^^<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Understanding the MSTest TestContext ; <a target='tab' href='https://stackoverflow.com/questions/24249133/understanding-the-mstest-testcontext'>https://stackoverflow.com/questions/24249133/understanding-the-mstest-testcontext</a> </pre> <br /> 현실적으로 봤을 때, 저런 이상한 동작을 고려해 보면 _classContext를 사용할 일은 거의 없을 것 같고, _methodContext로 단일화하는 것이 좋을 것입니다. 게다가 사실 위에서 길게 설명하긴 했지만 TestContext 자체마저도 그다지 쓸 일이 거의 없습니다.<br /> <br /> 참고로, TestContext의 속성 및 메서드는 다음과 같은 것들이 있습니다.<br /> <br /> <ul> <li>DeploymentDirectory</li> <li>ResultsDirectory</li> <li>TestRunResultsDirectory</li> <li>TestResultsDirectory</li> <li>TestDir</li> <li>TestDeploymentDir</li> <li>TestLogsDir</li> <li>FullyQualifiedTestClassName</li> <li>TestName</li> <li>CurrentTestOutcome</li> <li>AddResultFile(string fileName);</li> <li>WriteLine(string format, params object[] args);</li> </ul> <br /> <hr style='width: 50%' /><br /> <br /> 기타, 마지막으로 역시 잘 사용하지는 않지만 어셈블리 수준의 문맥을 제공하는 Init/Clean도 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > AssemblyInitialize, AssemblyCleanup </pre> <br /> 이것 역시 ClassInitialize/ClassCleanup과 메서드 signature에 유사한 제약을 갖는데,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > [AssemblyInitialize] <span style='color: blue; font-weight: bold'>public static void</span> Init_Per_Assembly(<span style='color: blue; font-weight: bold'>TestContext context</span>) { } [AssemblyCleanup] <span style='color: blue; font-weight: bold'>public static void</span> Clean_Per_Assembly() { } </pre> <br /> 거의 쓸 일이 희박할 것 같습니다. ^^<br /> <br /> 그 외, 관련해서 다음의 도움말을 보면 보다 더 자세한 정보를 얻을 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Use the MSTest framework in unit tests ; <a target='tab' href='https://learn.microsoft.com/en-us/visualstudio/test/using-microsoft-visualstudio-testtools-unittesting-members-in-unit-tests'>https://learn.microsoft.com/en-us/visualstudio/test/using-microsoft-visualstudio-testtools-unittesting-members-in-unit-tests</a> </pre> </p><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
1246
(왼쪽의 숫자를 입력해야 합니다.)