.NET 단위 테스트: 나중에 비용을 절감하기 위해 선불 지출
게시 됨: 2022-03-11이해 관계자 및 클라이언트와 논의할 때 단위 테스트와 관련하여 종종 많은 혼란과 의심이 있습니다. 단위 테스트는 때때로 치실이 아이에게 하는 것처럼 들립니다. "이미 양치질을 하는데 왜 이것을 해야 합니까?"
단위 테스트를 제안하는 것은 종종 테스트 방법과 사용자 승인 테스트가 충분히 강력하다고 생각하는 사람들에게 불필요한 비용처럼 들립니다.
그러나 단위 테스트는 매우 강력한 도구이며 생각보다 간단합니다. 이 기사에서는 단위 테스트와 Microsoft.VisualStudio.TestTools 및 Moq 와 같은 DotNet에서 사용할 수 있는 도구를 살펴보겠습니다.
우리는 피보나치 수열에서 n번째 항을 계산할 간단한 클래스 라이브러리를 만들려고 노력할 것입니다. 그렇게 하려면 숫자를 더하는 사용자 지정 수학 클래스에 의존하는 피보나치 수열을 계산하기 위한 클래스를 만들고 싶습니다. 그런 다음 .NET Testing Framework를 사용하여 프로그램이 예상대로 실행되는지 확인할 수 있습니다.
단위 테스트 란 무엇입니까?
단위 테스트는 프로그램을 가장 작은 코드 비트(일반적으로 함수 수준)로 나누고 함수가 예상한 값을 반환하는지 확인합니다. 단위 테스트 프레임워크를 사용하면 단위 테스트가 별도의 엔터티가 되어 프로그램을 빌드할 때 자동화된 테스트를 실행할 수 있습니다.
[TestClass] public class FibonacciTests { [TestMethod] //Check the first value we calculate public void Fibonacci_GetNthTerm_Input2_AssertResult1() { //Arrange int n = 2; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert Assert.AreEqual(result, 1); } }
Arrange, Act, Assert 방법론을 사용한 간단한 단위 테스트로 수학 라이브러리가 2 + 2를 올바르게 추가할 수 있습니다.
단위 테스트가 설정되면 코드가 변경되면 프로그램이 처음 개발될 때 알려지지 않은 추가 조건을 설명하기 위해 예를 들어 모든 경우가 예상 값과 일치하는지 표시합니다. 함수에 의해 출력됩니다.
단위 테스트는 통합 테스트가 아닙니다 . 종단 간 테스트가 아닙니다 . 이 두 가지 모두 강력한 방법론이지만 대체가 아니라 단위 테스트와 함께 작동해야 합니다.
단위 테스트의 이점과 목적
단위 테스트의 가장 어려운 이점은 이해하기 어렵지만 가장 중요한 것은 변경된 코드를 즉석에서 다시 테스트할 수 있다는 것입니다. 이해하기 어려울 수 있는 이유는 너무 많은 개발자가 "이 기능은 다시는 건드리지 않을 것" 또는 "다 끝나면 다시 테스트할 것"이라고 스스로 생각하기 때문입니다. 그리고 이해 관계자 는 "이미 작성된 부분이 있다면 왜 다시 테스트해야 합니까?"라는 관점에서 생각합니다.
개발 스펙트럼의 양면에 있었던 사람으로서 저는 이 두 가지를 모두 말했습니다. 내 안의 개발자는 왜 다시 테스트해야 하는지 알고 있습니다.
우리가 매일 하는 변화는 엄청난 영향을 미칠 수 있습니다. 예를 들어:
- 귀하의 스위치는 귀하가 입력한 새로운 가치를 적절하게 설명합니까?
- 그 스위치를 몇 번이나 사용했는지 아십니까?
- 대소문자를 구분하지 않는 문자열 비교를 적절하게 고려했습니까?
- null을 적절하게 확인하고 있습니까?
- throw 예외가 예상대로 처리됩니까?
단위 테스트는 이러한 질문을 받아 코드와 프로세스 에 기억하여 이러한 질문에 항상 답하도록 합니다. 새로운 버그가 발생하지 않았는지 확인하기 위해 빌드 전에 단위 테스트를 실행할 수 있습니다. 단위 테스트는 원자성으로 설계되었기 때문에 테스트당 일반적으로 10밀리초 미만으로 매우 빠르게 실행됩니다. 매우 큰 응용 프로그램에서도 전체 테스트 제품군을 1시간 이내에 수행할 수 있습니다. UAT 프로세스가 일치할 수 있습니까?
Fibonacci_GetNthTerm_Input2_AssertResult1
을 제외하고 모든 단위 테스트는 5ms 미만으로 실행됩니다. 여기에서 내 명명 규칙은 테스트하려는 클래스 또는 클래스 내 메서드를 쉽게 검색하도록 설정되었습니다.
하지만 개발자로서 이것이 더 많은 작업처럼 들릴 수 있습니다. 예, 릴리스한 코드가 훌륭하다는 사실에 안심할 수 있습니다. 그러나 단위 테스트는 또한 설계가 취약한 부분을 확인할 수 있는 기회를 제공합니다. 두 개의 코드에 대해 동일한 단위 테스트를 작성하고 있습니까? 대신 한 조각의 코드에 있어야 합니까?
코드 자체를 단위 테스트 가능하게 만드는 것은 디자인을 개선하는 방법입니다. 그리고 단위 테스트를 한 번도 해본 적이 없거나 코딩하기 전에 디자인을 고려하는 데 많은 시간을 들이지 않는 대부분의 개발자는 단위 테스트를 준비하면 디자인이 얼마나 향상되는지 알 수 있습니다.
코드 단위를 테스트할 수 있습니까?
DRY 외에 다른 고려 사항도 있습니다.
당신의 방법이나 기능이 너무 많은 일을 하려고 합니까?
예상보다 오래 실행되는 지나치게 복잡한 단위 테스트를 작성해야 하는 경우 메서드가 너무 복잡하고 여러 메서드에 더 적합할 수 있습니다.
의존성 주입을 적절히 활용하고 있습니까?
테스트 중인 메서드에 다른 클래스나 함수가 필요한 경우 이를 종속성이라고 합니다. 단위 테스트에서 우리는 종속성이 후드 아래에서 무엇을 하는지 신경 쓰지 않습니다. 테스트 중인 방법의 목적을 위해 블랙박스입니다. 종속성에는 동작이 제대로 작동하는지 확인하는 고유한 단위 테스트 세트가 있습니다.
테스터는 해당 종속성을 시뮬레이션하고 특정 인스턴스에서 반환할 값을 알려주고 싶습니다. 이렇게 하면 테스트 케이스를 더 잘 제어할 수 있습니다. 이렇게 하려면 해당 종속성의 더미(또는 나중에 보게 될 조롱) 버전을 주입해야 합니다.
구성 요소가 예상대로 서로 상호 작용합니까?
종속성과 종속성 주입을 해결하고 나면 코드에 순환 종속성을 도입했음을 알 수 있습니다. 클래스 A가 클래스 B에 종속되고 클래스 B가 클래스 A에 종속되는 경우 설계를 재고해야 합니다.
의존성 주입의 아름다움
피보나치 예를 살펴보겠습니다. 상사는 C#에서 사용할 수 있는 현재 추가 연산자보다 더 효율적이고 정확한 새 클래스가 있다고 말합니다.
이 특정 예는 실제 세계에서는 거의 발생하지 않지만 인증, 개체 매핑 및 거의 모든 알고리즘 프로세스와 같은 다른 구성 요소에서 유사한 예를 볼 수 있습니다. 이 기사의 목적을 위해 클라이언트의 새로운 추가 기능이 컴퓨터가 발명된 이후 가장 최신의 기능이라고 가정해 보겠습니다.
따라서 상사는 단일 클래스 Math
가 포함된 블랙박스 라이브러리를 제공하고 해당 클래스에서 단일 함수 Add
를 제공합니다. 피보나치 계산기를 구현하는 작업은 다음과 같습니다.
public int GetNthTerm(int n) { Math math = new Math(); int nMinusTwoTerm = 1; int nMinusOneTerm = 1; int newTerm = 0; for (int i = 2; i < n; i++) { newTerm = math.Add(nMinusOneTerm, nMinusTwoTerm); nMinusTwoTerm = nMinusOneTerm; nMinusOneTerm = newTerm; } return newTerm; }
이것은 끔찍하지 않습니다. 새 Math
클래스를 인스턴스화하고 이를 사용하여 이전 두 항을 추가하여 다음 항을 얻습니다. 일반적인 테스트 배터리를 통해 이 방법을 실행하고, 100항까지 계산하고, 1000번째 항목, 10,000번째 항목을 계산하는 등 방법론이 제대로 작동한다고 만족할 때까지 계속합니다. 그런 다음 미래의 언젠가 사용자가 501번째 용어가 예상대로 작동하지 않는다고 불평합니다. 저녁에는 코드를 살펴보고 이 코너 케이스가 작동하지 않는 이유를 알아내려고 노력합니다. 당신은 최신의 가장 훌륭한 Math
수업이 당신의 상사가 생각하는 것만큼 훌륭하지 않다는 것을 의심하기 시작합니다. 그러나 그것은 블랙박스이며 실제로 그것을 증명할 수는 없습니다. 내부적으로 난관에 봉착하게 됩니다.
여기서 문제는 종속성 Math
이 피보나치 계산기에 주입되지 않는다는 것입니다. 따라서 테스트에서 항상 Math
의 기존, 테스트되지 않은, 알려지지 않은 결과에 의존하여 피보나치를 테스트합니다. Math
에 문제가 있으면 피보나치는 항상 틀릴 것입니다(501번째 항에 대한 특별한 경우를 코딩하지 않음).
이 문제를 해결하기 위한 아이디어는 Math
클래스를 피보나치 계산기에 삽입하는 것입니다. 그러나 더 나은 방법은 공용 메서드(이 경우 Add
)를 정의하는 Math
클래스용 인터페이스를 만들고 Math
클래스에서 인터페이스를 구현하는 것입니다.
public interface IMath { int Add(int x, int y); } public class Math : IMath { public int Add(int x, int y) { //super secret implementation here } } }
Math
클래스를 피보나치에 주입하는 대신 IMath
인터페이스를 피보나치에 주입할 수 있습니다. 여기서의 이점은 정확하다고 알고 있는 OurMath
클래스를 정의하고 이에 대해 계산기를 테스트할 수 있다는 것입니다. 더 좋은 점은 Moq를 사용하여 Math.Add
가 반환하는 내용을 간단히 정의할 수 있다는 것입니다. 많은 합계를 정의하거나 Math.Add
에 x + y를 반환하도록 지시할 수 있습니다.
private IMath _math; public Fibonacci(IMath math) { _math = math; }
피보나치 클래스에 IMath 인터페이스 삽입
//setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y);
Moq를 사용하여 Math.Add
가 반환하는 내용을 정의합니다.

이제 우리는 두 개의 숫자를 추가하기 위한 시도되고 사실(글쎄, + 연산자가 C#에서 잘못된 경우 더 큰 문제가 있음) 방법을 가지고 있습니다. 새로운 Mocked IMath
를 사용하여 501번째 용어에 대한 단위 테스트를 코딩하고 구현을 바보짓했는지 또는 사용자 지정 Math
클래스에 작업이 조금 더 필요한지 확인할 수 있습니다.
한 방법이 너무 많은 일을 하도록 내버려 두지 마십시오.
이 예는 또한 너무 많은 작업을 수행하는 방법의 아이디어를 지적합니다. 물론 추가는 GetNthTerm
메서드에서 기능을 추상화할 필요가 없는 매우 간단한 작업입니다. 하지만 수술이 조금 더 복잡하다면? 추가 대신에 아마도 모델 유효성 검사, 작업할 개체를 얻기 위해 공장을 호출하거나 저장소에서 필요한 추가 데이터를 수집하는 것이었습니다.
대부분의 개발자는 하나의 방법에 하나의 목적이 있다는 아이디어를 고수하려고 노력할 것입니다. 단위 테스트에서 우리는 단위 테스트가 원자적 방법에 적용되어야 한다는 원칙을 고수하려고 노력하고 방법에 너무 많은 작업을 도입하여 테스트할 수 없게 만듭니다. 우리는 종종 우리의 기능을 적절하게 테스트하기 위해 너무 많은 테스트를 작성해야 하는 문제를 만들 수 있습니다.
메소드에 추가하는 각 매개변수는 매개변수의 복잡성에 따라 기하급수적으로 작성해야 하는 테스트의 수를 증가시킵니다. 논리에 부울을 추가하면 현재 테스트와 함께 참 및 거짓 사례를 확인해야 하므로 작성할 테스트 수를 두 배로 늘려야 합니다. 모델 검증의 경우 단위 테스트의 복잡성이 매우 빠르게 증가할 수 있습니다.
우리는 모두 메소드에 약간의 추가 기능을 추가한 죄를 범하고 있습니다. 그러나 이러한 더 크고 복잡한 방법으로 인해 너무 많은 단위 테스트가 필요합니다. 그리고 단위 테스트를 작성할 때 메서드가 너무 많은 작업을 수행하려고 한다는 것이 빠르게 드러납니다. 입력 매개변수에서 가능한 결과를 너무 많이 테스트하려고 하는 것 같으면 방법을 일련의 더 작은 것으로 분할해야 한다는 사실을 고려하십시오.
자신을 반복하지 마십시오
우리가 가장 좋아하는 프로그래밍 테넌트 중 하나입니다. 이것은 상당히 직선적이어야 합니다. 동일한 테스트를 두 번 이상 작성하는 자신을 발견했다면 코드를 두 번 이상 도입한 것입니다. 사용하려는 두 인스턴스 모두에 액세스할 수 있는 공통 클래스로 작업을 리팩토링하는 것이 도움이 될 수 있습니다.
어떤 단위 테스트 도구를 사용할 수 있습니까?
DotNet은 기본적으로 매우 강력한 단위 테스트 플랫폼을 제공합니다. 이를 사용하여 Arrange, Act, Assert 방법론을 구현할 수 있습니다. 초기 고려 사항을 정리하고 테스트 중인 방법으로 해당 조건에 따라 행동한 다음 어떤 일이 발생했다고 주장합니다. 무엇이든 주장할 수 있어 이 도구가 더욱 강력해집니다. 메서드가 특정 횟수만큼 호출되었는지, 메서드가 특정 값을 반환했는지, 특정 유형의 예외가 발생했는지 등 생각할 수 있는 모든 것을 주장할 수 있습니다. 보다 고급 프레임워크를 찾는 사람들에게는 NUnit과 이에 상응하는 Java JUnit이 실행 가능한 옵션입니다.
[TestMethod] //Test To Verify Add Never Called on the First Term public void Fibonacci_GetNthTerm_Input0_AssertAddNeverCalled() { //Arrange int n = 0; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Never); }
피보나치 방법이 예외를 throw하여 음수를 처리하는지 테스트합니다. 단위 테스트는 예외가 발생했는지 확인할 수 있습니다.
의존성 주입을 처리하기 위해 Ninject와 Unity가 모두 DotNet 플랫폼에 존재합니다. 둘 사이에는 거의 차이가 없으며 Fluent Syntax 또는 XML Configuration으로 구성을 관리하려는 경우가 중요합니다.
종속성을 시뮬레이션하려면 Moq를 권장합니다. Moq는 다루기가 어려울 수 있지만 요점은 종속성의 모의 버전을 만드는 것입니다. 그런 다음 특정 조건에서 반환할 항목을 종속성에 알려줍니다. 예를 들어, 정수를 제곱한 Square(int x)
라는 메서드가 있는 경우 x = 2일 때 4를 반환할 수 있습니다. 임의의 정수에 대해 x^2를 반환하도록 지정할 수도 있습니다. 또는 x = 2일 때 5를 반환하도록 지시할 수 있습니다. 마지막 경우를 수행하는 이유는 무엇입니까? 테스트의 역할 아래에 있는 메서드가 종속성에서 응답을 확인하는 것인 경우 잘못된 응답을 강제로 반환하여 버그를 제대로 포착하고 있는지 확인할 수 있습니다.
[TestMethod] //Test To Verify Add Called Three times on the fifth Term public void Fibonacci_GetNthTerm_Input4_AssertAddCalledThreeTimes() { //Arrange int n = 4; //setup Mock<UnitTests.IMath> mockMath = new Mock<UnitTests.IMath>(); mockMath .Setup(r => r.Add(It.IsAny<int>(), It.IsAny<int>())) .Returns((int x, int y) => x + y); UnitTests.Fibonacci fibonacci = new UnitTests.Fibonacci(mockMath.Object); //Act int result = fibonacci.GetNthTerm(n); //Assert mockMath.Verify(r => r.Add(It.IsAny<int>(), It.IsAny<int>()), Times.Exactly(3)); }
Moq를 사용하여 모의 IMath
인터페이스에 Add
under test를 처리하는 방법을 알려줍니다. It.Is로 명시적 케이스를 설정하거나 It.Is
로 범위를 It.IsInRange
수 있습니다.
DotNet용 단위 테스트 프레임워크
마이크로소프트 유닛 테스팅 프레임워크
Microsoft 단위 테스트 프레임워크는 Microsoft의 즉시 사용 가능한 단위 테스트 솔루션이며 Visual Studio에 포함되어 있습니다. VS와 함께 제공되기 때문에 VS와 잘 통합됩니다. 프로젝트를 시작하면 Visual Studio에서 응용 프로그램과 함께 단위 테스트 라이브러리를 만들 것인지 묻습니다.
Microsoft Unit Testing Framework에는 테스트 절차를 더 잘 분석하는 데 도움이 되는 여러 도구도 함께 제공됩니다. 또한 마이크로소프트가 소유, 작성하고 있기 때문에 앞으로 존재감에 어느정도 안정감이 있습니다.
그러나 Microsoft 도구로 작업할 때 제공되는 것을 얻을 수 있습니다. Microsoft 단위 테스트 프레임워크는 통합하기가 번거로울 수 있습니다.
엔단위
NUnit을 사용할 때 가장 큰 장점은 매개변수화된 테스트입니다. 위의 피보나치 예에서 여러 테스트 사례를 입력하고 해당 결과가 참인지 확인할 수 있습니다. 그리고 501번째 문제의 경우 항상 새 매개변수 세트를 추가하여 새 테스트 방법 없이도 테스트가 항상 실행되도록 할 수 있습니다.
NUnit의 주요 단점은 Visual Studio에 통합한다는 것입니다. Microsoft 버전과 함께 제공되는 간단한 기능이 없으므로 고유한 도구 집합을 다운로드해야 합니다.
xUnit.Net
xUnit은 기존 .NET 에코시스템과 매우 잘 통합되기 때문에 C#에서 매우 인기가 있습니다. Nuget에는 사용 가능한 xUnit의 많은 확장이 있습니다. 또한 Team Foundation Server와도 잘 통합되지만 다양한 Git 구현에서 TFS를 사용하는 .NET 개발자가 몇 명인지는 잘 모르겠습니다.
단점으로 많은 사용자가 xUnit의 문서가 약간 부족하다고 불평합니다. 새로운 사용자가 단위 테스트를 하는 경우 이는 엄청난 골칫거리가 될 수 있습니다. 또한 xUnit의 확장성과 적응성은 학습 곡선을 NUnit 또는 Microsoft의 단위 테스팅 프레임워크보다 더 가파르게 만듭니다.
테스트 주도 설계/개발
테스트 주도 설계/개발(TDD)은 자체 게시물을 올릴 가치가 있는 좀 더 고급 주제입니다. 그러나 나는 소개를 제공하고 싶었습니다.
아이디어는 단위 테스트로 시작하여 단위 테스트에 무엇이 올바른지 알려주는 것입니다. 그런 다음 해당 테스트를 중심으로 코드를 작성할 수 있습니다. 이론적으로 개념은 간단해 보이지만 실제로는 응용 프로그램에 대해 거꾸로 생각하도록 두뇌를 훈련시키는 것이 매우 어렵습니다. 그러나 이 접근 방식에는 사후에 단위 테스트를 작성할 필요가 없다는 기본 제공 이점이 있습니다. 이것은 리팩토링, 재작성 및 클래스 혼동을 줄입니다.
TDD는 최근 몇 년 동안 유행어가 되었지만 채택 속도가 더뎠습니다. 개념적 특성이 이해 관계자에게 혼동을 주어 승인을 받기 어렵습니다. 그러나 개발자로서 프로세스에 익숙해지기 위해 TDD 방식을 사용하여 작은 응용 프로그램을 작성하는 것이 좋습니다.
너무 많은 단위 테스트를 가질 수 없는 이유
단위 테스트는 개발자가 마음대로 사용할 수 있는 가장 강력한 테스트 도구 중 하나입니다. 응용 프로그램의 전체 테스트에는 충분하지 않지만 회귀 테스트, 코드 디자인 및 목적 문서화에 대한 이점은 타의 추종을 불허합니다.
너무 많은 단위 테스트를 작성하는 것과 같은 것은 없습니다. 각 엣지 케이스는 소프트웨어에서 큰 문제를 제시할 수 있습니다. 발견된 버그를 단위 테스트로 기념하면 나중에 코드를 변경하는 동안 해당 버그가 소프트웨어에 다시 침투하는 방법을 찾지 않도록 할 수 있습니다. 프로젝트의 선행 예산에 10-20%를 추가할 수 있지만 교육, 버그 수정 및 문서화에서 훨씬 더 많이 절약할 수 있습니다.
이 기사에서 사용된 Bitbucket 리포지토리는 여기에서 찾을 수 있습니다.