단위테스트 시나리오 - dan-witeseuteu sinalio

주요 콘텐츠로 건너뛰기

이 브라우저는 더 이상 지원되지 않습니다.

최신 기능, 보안 업데이트, 기술 지원을 이용하려면 Microsoft Edge로 업그레이드하세요.

.NET Core 및.NET 표준을 사용하는 단위 테스트 모범 사례

  • 아티클
  • 10/05/2022
  • 읽는 데 28분 걸림

이 문서의 내용

단위 테스트를 작성할 때는 다양한 이점이 있습니다. 회귀를 돕고, 설명서를 제공하고, 좋은 디자인을 용이하게 합니다. 그러나 읽기 어렵고 불안정한 단위 테스트는 코드 기반을 파괴할 수 있습니다. 이 문서에서는.NET Core 및 .NET 표준 프로젝트용 단위 테스트 디자인과 관련된 몇 가지 모범 사례를 설명합니다.

이 가이드에서는 복원력과 이해하기 쉬운 테스트를 유지하기 위해 단위 테스트를 작성할 때 몇 가지 모범 사례를 알아봅니다.

John Reese, Roy Osherove에 대한 특별한 감사

단위 테스트하는 이유는?

기능 테스트 수행 시간 단축

기능 테스트는 비용이 많이 듭니다. 일반적으로 애플리케이션을 열고 예상 동작의 유효성을 검사하기 위해 사용자(또는 다른 사람)가 따라야 하는 일련의 단계를 수행하는 작업이 포함됩니다. 이러한 단계는 테스터에게 항상 알려지지 않을 수 있습니다. 그들은 테스트를 수행하기 위해 지역에서 더 지식이 있는 사람에게 손을 내밀어야 합니다. 테스트 자체는 사소한 변경인 경우에는 몇 초가 걸리거나 큰 변경의 경우에는 몇 분 정도 걸릴 수 있습니다. 마지막으로, 이 프로세스는 시스템에서 수행하는 모든 변경 사항에 대해 반복되어야 합니다.

반면에 단위 테스트는 밀리초가 소요되고 단추를 눌러 실행할 수 있으며 시스템 전체에 대한 정보가 반드시 필요하지는 않습니다. 테스트 통과 또는 실패 여부는 개인이 아닌 test runner의 몫입니다.

회귀에 대한 보호

회귀 오류는 애플리케이션이 변경될 때 도입된 결함입니다. 테스터는 새 기능을 테스트할 뿐만 아니라 이전에 구현된 기능이 여전히 예상대로 작동하는지 확인하기 위해 기존 기능을 테스트하는 것이 일반적입니다.

단위 테스트를 사용하면 모든 빌드 후에 또는 코드 줄을 변경한 후에도 전체 테스트 도구 모음을 다시 실행할 수 있습니다. 새 코드가 기존 기능을 중단하지 않는다는 확신을 줍니다.

실행 가능한 설명서

특정 입력이 제공된 경우 특정 메서드가 수행하는 작업이나 동작 방식이 항상 명확하지는 않을 수 있습니다. 빈 문자열을 전달하면 이 메서드가 어떻게 동작하나요? Null?

이름이 잘 지정된 단위 테스트의 도구 모음이 있는 경우 각 테스트는 지정된 입력에 대해 예상되는 출력을 명확하게 설명할 수 있어야 합니다. 또한 실제로 작동하는지 확인할 수 있어야 합니다.

적은 결합 코드

코드가 밀접하게 결합되면 단위 테스트하기가 어려울 수 있습니다. 작성하는 코드에 대한 단위 테스트를 만들지 않으면 결합이 덜 분명해질 수 있습니다.

코드 테스트를 작성하면 자연스럽게 분리됩니다. 그렇지 않으면 테스트하기가 더 어려워지기 때문입니다.

좋은 단위 테스트의 특징

  • 빠른 속도: 성숙한 프로젝트에서 수천 개의 단위 테스트를 하는 것은 드문 일이 아닙니다. 단위 테스트는 실행하는 데 약간의 시간이 소요됩니다. 밀리초.
  • 격리됨: 단위 테스트는 독립 실행형이고, 격리된 상태로 실행할 수 있으며, 파일 시스템 또는 데이터베이스와 같은 외부 요인에 종속되지 않습니다.
  • 반복 가능: 단위 테스트 실행은 결과와 일치해야 합니다. 즉, 실행 간에 아무것도 변경하지 않으면 항상 동일한 결과를 반환합니다.
  • 자체 검사: 테스트는 사람의 상호 작용 없이 통과했는지 또는 실패했는지 자동으로 검색할 수 있어야 합니다.
  • 시기 적절하게: 단위 테스트는 테스트 중인 코드에 비해 쓰기에 불균형적으로 오랜 시간이 걸리지 않아야 합니다. 코드를 작성하는 데 비해 많은 시간이 걸리는 코드를 테스트하는 경우 더 많은 테스트가 가능한 디자인을 고려해 보세요.

코드 검사

높은 코드 검사 비율이 높으면 코드 품질이 높아지는 경우가 많습니다. 그러나 측정 자체는 코드 품질을 확인할 수 없습니다 . 코드 검사 비율 목표를 지나치게 욕심을 부려 높게 설정하면 역효과가 날 수 있습니다. 수천 개의 조건부 분기가 포함된 복잡한 프로젝트에서 95% 코드 검사 목표를 설정한다고 가정해 보세요. 현재 프로젝트에서는 90% 코드 검사를 유지합니다. 나머지 5%의 극단적인 경우를 모두 고려하는 데에는 엄청난 시간이 들 수 있으며 가치 제안의 중요성이 곧 낮아집니다.

높은 코드 검사 비율은 성공의 지표가 아니며 높은 코드 품질을 의미하지도 않습니다. 단위 테스트에서 검사되는 코드의 양을 나타낼 뿐입니다. 자세한 내용은 유닛 테스트 코드 검사를 참조하세요.

동일한 언어 사용

mock이라는 용어는 불행히도 테스트에 대해 이야기할 때 종종 잘못 사용됩니다. 다음은 단위 테스트를 작성할 때 fakes의 가장 일반적인 유형을 정의합니다.

Fake - fake는 스텁 또는 모의 개체를 설명하는 데 사용할 수 있는 일반적인 용어입니다. stub 또는 mock인지 여부는 사용되는 컨텍스트에 따라 달라집니다. 즉, fake는 stub 또는 mock이 될 수 있습니다.

Mock - mock 개체는 단위 테스트가 통과되었는지 여부를 결정하는 시스템에서 fake 개체입니다. mock은 어설션될 때까지 fake로 시작합니다.

Stub - stub은 시스템의 기존 종속성(또는 협력자)을 제어할 수 있는 대체품입니다. stub을 사용하여 종속성을 직접 처리하지 않고 코드를 테스트할 수 있습니다. 기본적으로 stub은 fake로 시작합니다.

다음 코드 조각을 살펴봅니다.

var mockOrder = new MockOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

앞의 예제는 모의 스텁이라고 하는 스텁입니다. 이 경우 스텁입니다. Purchase(테스트 중인 시스템)를 인스턴스화할 수 있는 수단으로 Order를 전달합니다. 다시 주문이 모의 주문이 아니기 때문에 이름은 MockOrder 오해의 소지가 있습니다.

더 나은 방법은 다음과 같습니다.

var stubOrder = new FakeOrder();
var purchase = new Purchase(stubOrder);

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

클래스의 이름을 변경하여 클래스 FakeOrder를 훨씬 더 일반화했습니다. 클래스는 모의 또는 스텁으로 사용할 수 있으며 테스트 사례에 더 적합합니다. 앞의 예제 FakeOrder 에서는 스텁으로 사용됩니다. 어설션 중에는 도형이나 폼을 사용하지 FakeOrder 않습니다. FakeOrder는 방금 Purchase 클래스에 전달되어 생성자의 요구 사항을 충족했습니다.

모의 코드로 사용하려면 다음 코드와 같은 작업을 수행할 수 있습니다.

var mockOrder = new FakeOrder();
var purchase = new Purchase(mockOrder);

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

이 경우 Fake에서 속성을 확인하므로(이에 대해 어설션), 앞의 코드 조각 mockOrder 에서 모의 속성입니다.

중요

이 용어를 올바르게 사용하는 것이 중요합니다. 스텁을 "모의 항목"이라고 부르는 경우 다른 개발자는 의도에 대해 거짓 가정을 할 것입니다.

모의 및 스텁에 대해 기억해야 할 중요한 점은 모의 항목이 스텁과 비슷하지만 모의 개체에 대해 어설션하는 반면 스텁에 대해 어설션하지 않는다는 것입니다.

최선의 구현 방법

단위 테스트를 작성하는 경우 인프라에 대한 종속성을 도입하지 않습니다. 종속성을 사용하면 테스트 속도가 느리고 부서지기 쉬우며 통합 테스트를 위해 예약되어야 합니다. 명시적 종속성 원칙을 따르고 종속성 주입을 사용하여 애플리케이션에서 이러한 종속성을 방지할 수 있습니다. 통합 테스트의 개별 프로젝트에서 단위 테스트를 유지할 수도 있습니다. 이 방법을 사용하면 단위 테스트 프로젝트에 인프라 패키지에 대한 참조 또는 종속성이 없습니다.

테스트 이름 지정

테스트의 이름은 다음 세 부분으로 구성되어야 합니다.

  • 테스트할 메서드의 이름입니다.
  • 테스트 중인 시나리오입니다.
  • 시나리오에서 호출될 때 예상되는 동작입니다.

이유

이름 지정 표준은 테스트의 의도를 명시적으로 표현하기 때문에 중요합니다. 테스트는 단순히 코드가 작동하는지 확인하는 것 이상이며, 문서도 제공합니다. 단위 테스트 도구 모음을 살펴봄으로써 코드 자체를 조회하지 않고도 코드의 동작을 유추할 수 있습니다. 또한 테스트가 실패하면 예상을 충족하지 않는 시나리오를 정확하게 확인할 수 있습니다.

Bad:

[Fact]
public void Test_Single()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

Better:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

테스트 정렬

정렬, 동작, 어설션은 단위 테스트 시 일반적인 패턴입니다. 이름에서 알 수 있듯이 세 가지 주요 작업으로 구성됩니다.

  • 필요에 따라 개체를 정렬하고 만들고 설정합니다.
  • 개체의 동작입니다.
  • 특정 항목이 예상대로라고 assert합니다.

이유

  • 테스트할 항목을 정렬 및 assert 단계에서 명확하게 구분합니다.
  • 어설션과 "Act" 코드를 혼합할 기회가 적습니다.

가독성은 테스트를 작성할 때 가장 중요한 측면 중 하나입니다. 테스트 내에서 이러한 각 작업을 구분하면 코드를 호출하는 데 필요한 종속성, 코드가 호출되는 방법 및 어설션하려는 항목이 명확하게 강조 표시됩니다. 몇 가지 단계를 결합하고 테스트 크기를 줄일 수 있지만, 주요 목표는 테스트를 가능한 한 읽을 수 있도록 하는 것입니다.

Bad:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Assert
    Assert.Equal(0, stringCalculator.Add(""));
}

Better:

[Fact]
public void Add_EmptyString_ReturnsZero()
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add("");

    // Assert
    Assert.Equal(0, actual);
}

최소한의 테스트 통과 작성

단위 테스트에 사용할 입력은 현재 테스트 중인 동작을 확인하기 위해 가능한 가장 간단해야 합니다.

이유

  • 테스트는 코드베이스의 향후 변화에 대한 복원력이 향상됩니다.
  • 구현에 대한 테스트 동작에 근접합니다.

테스트를 통과하는 데 필요한 것보다 많은 정보를 포함하는 테스트는 테스트에 오류가 발생할 가능성이 높으며 테스트의 의도를 덜 명확하게 할 수 있습니다. 테스트를 작성할 때 동작에 집중하려고 합니다. 모델의 추가 속성을 설정하거나 필요하지 않을 때 0이 아닌 값을 사용하면 증명하려고 시도한 것만 손상됩니다.

Bad:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("42");

    Assert.Equal(42, actual);
}

Better:

[Fact]
public void Add_SingleNumber_ReturnsSameNumber()
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add("0");

    Assert.Equal(0, actual);
}

매직 문자열 방지

단위 테스트에서 변수 이름을 지정하는 것은 프로덕션 코드에서 변수 이름을 지정하는 것보다 더 중요하지 않은 경우 중요합니다. 단위 테스트에는 매직 문자열이 포함되어서는 안 됩니다.

이유

  • 값을 특별하게 만드는 요인을 파악하기 위해 테스트의 판독기가 프로덕션 코드를 검사할 필요가 없도록 합니다.
  • 성취하려는 것보다는 증명하려는 것을 명시적으로 보여 줍니다.

매직 문자열은 테스트 판독기에 혼동을 일으킬 수 있습니다. 문자열이 일반 값을 벗어나는 경우 매개 변수 또는 반환 값에 대해 특정 값을 선택한 이유가 궁금할 수 있습니다. 이러한 형식의 문자열 값은 테스트에 집중하지 않고 구현 세부 정보를 자세히 살펴볼 수 있습니다.

테스트를 작성할 때는 가능한 한 많은 의도를 표현하는 것을 목표로 해야 합니다. 매직 문자열의 경우 좋은 방법은 이러한 값을 상수에 할당하는 것입니다.

Bad:

[Fact]
public void Add_BigNumber_ThrowsException()
{
    var stringCalculator = new StringCalculator();

    Action actual = () => stringCalculator.Add("1001");

    Assert.Throws<OverflowException>(actual);
}

Better:

[Fact]
void Add_MaximumSumResult_ThrowsOverflowException()
{
    var stringCalculator = new StringCalculator();
    const string MAXIMUM_RESULT = "1001";

    Action actual = () => stringCalculator.Add(MAXIMUM_RESULT);

    Assert.Throws<OverflowException>(actual);
}

테스트에서 논리 방지

단위 테스트를 작성할 때는 수동 문자열 연결, 논리 조건(예: if, whileforswitch기타 조건)을 사용하지 않습니다.

이유

  • 테스트 중에 버그가 발생할 가능성이 줄어듭니다.
  • 구현 세부 정보보다는 최종 결과에 집중합니다.

테스트 도구 모음에 논리를 도입하면 버그가 발생할 가능성이 크게 증가합니다. 버그를 찾고자 하는 마지막 위치는 테스트 도구 모음 내에 있습니다. 테스트가 작동한다는 높은 수준의 신뢰가 있어야 합니다. 그렇지 않으면 테스트를 신뢰하지 않습니다. 신뢰하지 않는 테스트는 값을 제공하지 않습니다. 테스트가 실패할 때 코드에 문제가 있고 무시할 수 없다는 느낌을 주려고 합니다.

테스트의 논리가 불가피한 경우 테스트를 두 개 이상의 다른 테스트로 분할하는 것이 좋습니다.

Bad:

[Fact]
public void Add_MultipleNumbers_ReturnsCorrectResults()
{
    var stringCalculator = new StringCalculator();
    var expected = 0;
    var testCases = new[]
    {
        "0,0,0",
        "0,1,2",
        "1,2,3"
    };

    foreach (var test in testCases)
    {
        Assert.Equal(expected, stringCalculator.Add(test));
        expected += 3;
    }
}

Better:

[Theory]
[InlineData("0,0,0", 0)]
[InlineData("0,1,2", 3)]
[InlineData("1,2,3", 6)]
public void Add_MultipleNumbers_ReturnsSumOfNumbers(string input, int expected)
{
    var stringCalculator = new StringCalculator();

    var actual = stringCalculator.Add(input);

    Assert.Equal(expected, actual);
}

설정 및 해제할 도우미 방법 선호

테스트에 유사한 개체 또는 상태가 필요한 경우 사용 Setup 보다 도우미 메서드와 Teardown 특성이 있는 경우 사용하는 것이 좋습니다.

이유

  • 모든 코트가 각 테스트 내에서 볼 수 있기 때문에 테스트를 읽을 때 혼동이 적습니다.
  • 지정된 테스트에 대해 너무 많거나 너무 적게 설정될 가능성이 줄어듭니다.
  • 테스트 간 상태 공유로 테스트 간에 원치 않는 종속성이 생길 가능성이 줄어듭니다.

단위 테스트 프레임워크에서 Setup은 단위 테스트 도구 모음 내의 각각의 모든 단위 테스트 전에 호출됩니다. 일부는 유용한 도구로 볼 수 있지만, 일반적으로 부풀어 오르고 테스트를 읽기 어렵게 만듭니다. 각 테스트는 일반적으로 테스트를 시작하고 실행하기 위한 요구 사항이 다릅니다. 아쉽게도 Setup에서는 각 테스트에 대해 정확히 동일한 요구 사항을 사용해야 합니다.

참고

xUnit은 버전 2.x에서 SetUp 및 TearDown을 모두 제거했습니다.

Bad:

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
// more tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}

Better:

[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

    var actual = stringCalculator.Add("0,1");

    Assert.Equal(1, actual);
}
// more tests...
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

다중 동작 방지

테스트를 작성할 때는 테스트당 하나의 작업만 포함하려고 합니다. 하나의 동작만 사용하는 일반적인 방법은 다음과 같습니다.

  • 각 동작에 대한 별도의 테스트를 만듭니다.
  • 매개 변수화된 테스트를 사용합니다.

이유

  • 테스트가 실패하는 경우 어떤 작업이 실패하고 있는지 명확하지 않습니다.
  • 테스트가 단일 사례에만 집중되도록 합니다.
  • 테스트가 실패한 이유에 대한 전체 그림을 제공합니다.

여러 행위는 개별적으로 어설션되어야 하며 모든 어설션이 실행될 것이라고 보장되지는 않습니다. 대부분의 단위 테스트 프레임워크에서 단위 테스트에 어설션이 실패하면 절차 테스트는 자동으로 실패한 것으로 간주됩니다. 이러한 종류의 프로세스는 실제로 작동하는 기능이 실패로 표시되므로 혼동될 수 있습니다.

Bad:

[Fact]
public void Add_EmptyEntries_ShouldBeTreatedAsZero()
{
    // Act
    var actual1 = stringCalculator.Add("");
    var actual2 = stringCalculator.Add(",");

    // Assert
    Assert.Equal(0, actual1);
    Assert.Equal(0, actual2);
}

Better:

[Theory]
[InlineData("", 0)]
[InlineData(",", 0)]
public void Add_EmptyEntries_ShouldBeTreatedAsZero(string input, int expected)
{
    // Arrange
    var stringCalculator = new StringCalculator();

    // Act
    var actual = stringCalculator.Add(input);

    // Assert
    Assert.Equal(expected, actual);
}

공용 메서드를 테스트하여 전용 메서드 유효성 검사

대부분의 경우 프라이빗 메서드를 테스트할 필요가 없습니다. 프라이빗 메서드는 구현 세부 정보이며 격리된 상태로 존재하지 않습니다. 어떤 시점에서는 프라이빗 메서드를 구현의 일부로 호출하는 공용 연결 메서드가 있을 것입니다. 주의해야 할 것은 사적인 것을 호출하는 공용 메서드의 최종 결과입니다.

다음 경우를 참조하십시오.

public string ParseLogLine(string input)
{
    var sanitizedInput = TrimInput(input);
    return sanitizedInput;
}

private string TrimInput(string input)
{
    return input.Trim();
}

첫 번째 반응은 메서드가 예상대로 작동하는지 확인하려고 하기 때문에 테스트 TrimInput 작성을 시작하는 것일 수 있습니다. 그러나 ParseLogLine 전혀 예상하지 못한 방식으로 조작 sanitizedInput 하여 쓸모없는 테스트를 TrimInput 렌더링할 수 있습니다.

실제 테스트는 공용 연결 메서드 ParseLogLine에 대해 수행되어야 합니다. 이는 최종적으로 주의를 기울여야 하는 것이기 때문이다.

public void ParseLogLine_StartsAndEndsWithSpace_ReturnsTrimmedResult()
{
    var parser = new Parser();

    var result = parser.ParseLogLine(" a ");

    Assert.Equals("a", result);
}

이 관점에서, 전용 메서드를 표시하는 경우 공용 메서드를 찾아 해당 메서드에 대해 테스트를 작성합니다. 프라이빗 메서드가 예상된 결과를 반환한다고 해서 결국 프라이빗 메서드를 호출하는 시스템이 결과를 올바르게 사용하는 것은 아닙니다.

Stub 정적 참조

단위 테스트의 원칙 중 하나는 테스트 중인 시스템을 완전히 제어해야 한다는 것입니다. 프로덕션 코드에 정적 참조(예 DateTime.Now: )에 대한 호출이 포함된 경우 이 원칙은 문제가 될 수 있습니다. 다음 코드를 살펴보세요.

public int GetDiscountedPrice(int price)
{
    if (DateTime.Now.DayOfWeek == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

이 코드를 어떻게 단위 테스트할 수 있나요? 다음과 같은 방법을 시도할 수 있습니다.

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(2, actual)
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();

    var actual = priceCalculator.GetDiscountedPrice(2);

    Assert.Equals(1, actual);
}

아쉽게도 테스트에 몇 가지 문제가 있다는 것을 빠르게 알게 될 것입니다.

  • 테스트 도구 모음을 화요일에 실행하면 두 번째 테스트는 통과하지만 첫 번째 테스트는 실패합니다.
  • 테스트 도구 모음을 다른 날에 실행하면 첫 번째 테스트는 통과하지만 두 번째 테스트는 실패합니다.

이러한 문제를 해결하려면 프로덕션 코드에 이음새를 도입해야 합니다. 한 가지 방법은 인터페이스에서 제어해야 하는 코드를 래핑하여 프로덕션 코드가 해당 인터페이스에 종속되도록 하는 것입니다.

public interface IDateTimeProvider
{
    DayOfWeek DayOfWeek();
}

public int GetDiscountedPrice(int price, IDateTimeProvider dateTimeProvider)
{
    if (dateTimeProvider.DayOfWeek() == DayOfWeek.Tuesday)
    {
        return price / 2;
    }
    else
    {
        return price;
    }
}

이제 테스트 도구 모음이 다음과 같이 됩니다.

public void GetDiscountedPrice_NotTuesday_ReturnsFullPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Monday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(2, actual);
}

public void GetDiscountedPrice_OnTuesday_ReturnsHalfPrice()
{
    var priceCalculator = new PriceCalculator();
    var dateTimeProviderStub = new Mock<IDateTimeProvider>();
    dateTimeProviderStub.Setup(dtp => dtp.DayOfWeek()).Returns(DayOfWeek.Tuesday);

    var actual = priceCalculator.GetDiscountedPrice(2, dateTimeProviderStub);

    Assert.Equals(1, actual);
}

이제 테스트 도구 모음은 DateTime.Now에 대한 완전한 제어권을 가지며 메서드를 호출할 때 모든 값을 스텁할 수 있습니다.