다음을 통해 공유


.NET에 대한 단위 테스트 모범 사례

단위 테스트를 작성할 때는 다양한 이점이 있습니다. 회귀를 돕고, 설명서를 제공하고, 좋은 디자인을 용이하게 합니다. 그러나 단위 테스트가 읽기 힘들고 불안정한 경우 코드 베이스에 큰 피해를 줄 수 있습니다. 이 문서에서는 .NET Core 및 .NET Standard 프로젝트를 지원하기 위해 단위 테스트를 디자인하는 몇 가지 모범 사례를 설명합니다. 테스트를 복원력 있고 이해하기 쉽게 유지하는 기술을 알아봅니다.

존 리즈가 특별히 감사의 말씀을 전합니다 로이 오세로브에게

단위 테스트의 이점

다음 섹션에서는 .NET Core 및 .NET Standard 프로젝트에 대한 단위 테스트를 작성하는 몇 가지 이유를 설명합니다.

기능 테스트 수행 시간이 줄어듭니다.

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

회귀 방지

회귀 결함은 애플리케이션을 변경할 때 발생하는 오류입니다. 테스터는 새 기능을 테스트할 뿐만 아니라 기존 기능이 여전히 예상대로 작동하는지 확인하기 위해 미리 존재하는 기능을 테스트하는 것이 일반적입니다. 단위 테스트를 사용하면 모든 빌드 후 또는 코드 줄을 변경한 후에도 전체 테스트 제품군을 다시 실행할 수 있습니다. 이 방법은 새 코드가 기존 기능을 중단하지 않는다는 확신을 높이는 데 도움이 됩니다.

실행 가능한 문서

특정 메서드가 수행하는 작업이나 특정 입력을 통해 동작하는 방식이 항상 명확하지는 않을 수 있습니다. 빈 문자열 또는 null을 전달하면 이 메서드가 어떻게 동작하나요? 잘 명명된 단위 테스트 제품군이 있는 경우 각 테스트는 지정된 입력에 대한 예상 출력을 명확하게 설명해야 합니다. 또한 테스트가 실제로 작동하는지 확인할 수 있어야 합니다.

덜 결합된 코드

코드가 긴밀하게 결합된 경우 단위 테스트가 어려울 수 있습니다. 작성 중인 코드에 대한 단위 테스트를 만들지 않으면 결합이 덜 명백해질 수 있습니다. 코드에 대한 테스트를 작성하면 코드를 자연스럽게 분리할 수 있습니다. 그렇지 않으면 테스트하기가 더 어렵기 때문입니다.

좋은 단위 테스트의 특징

좋은 단위 테스트를 정의하는 몇 가지 중요한 특성이 있습니다.

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

코드 검사 및 코드 품질

높은 코드 검사 비율은 종종 더 높은 코드 품질과 연결됩니다. 그러나 측정 자체는 코드 품질을 확인할 수 없습니다. 지나치게 야심찬 코드 검사 백분율 목표를 설정하는 것은 비생산적일 수 있습니다. 수천 개의 조건부 분기가 있는 복잡한 프로젝트를 고려하고 95개의% 코드 검사 목표를 설정한다고 가정합니다. 현재 이 프로젝트는 90%의% 코드 커버리지를 유지 관리합니다. 나머지 5%의 모든 예외 사례를 고려하는 데 걸리는 시간은 엄청난 작업이 될 수 있으며, 가치 제안이 빠르게 감소합니다.

높은 코드 검사 비율은 성공의 지표가 아니며 높은 코드 품질을 의미하지는 않습니다. 단위 테스트에서 다루는 코드의 양을 나타냅니다. 자세한 내용은 단위 테스트 코드 커버리지참조하세요.

단위 테스트 용어

단위 테스트의 컨텍스트에서 자주 사용되는 용어는 가짜, 모의, 및 스텁입니다. 아쉽게도 이러한 용어는 잘못 적용될 수 있으므로 올바른 사용을 이해하는 것이 중요합니다.

  • Fake: 가짜는 스텁 또는 모의 객체를 설명하는 데 사용할 수 있는 일반적인 용어입니다. 개체가 스텁인지 모의 개체인지 여부는 개체가 사용되는 컨텍스트에 따라 달라집니다. 즉, 모조품은 스텁이나 모의 객체일 수 있습니다.

  • 모의: 모의 개체는 단위 테스트의 통과 여부를 결정하는 시스템의 가짜 개체입니다. 모의 모의는 가짜로 시작하고 Assert 작업에 들어갈 때까지 가짜로 남아 있습니다.

  • 스텁: 스텁은 시스템의 기존 종속성(또는 협력자)을 제어할 수 있는 대체입니다. 스텁을 사용하면 종속성을 직접 처리하지 않고도 코드를 테스트할 수 있습니다. 기본적으로 스텁은 가짜로 시작됩니다.

다음 코드를 고려합니다.

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

이 코드는 '모의 객체'로 불리는 스텁을 보여줍니다. 그러나 이 시나리오에서 스텁은 실제로 스텁입니다. 코드의 목적은 Purchase(테스트 중인 시스템) 개체를 인스턴스화하는 수단으로 순서를 전달하는 것입니다. 순서가 모의가 아닌 스텁이므로 클래스 이름 MockOrder 오해의 소지가 있습니다.

다음 코드는 보다 정확한 디자인을 보여줍니다.

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

purchase.ValidateOrders();

Assert.True(purchase.CanBeShipped);

클래스 이름이 FakeOrder경우 클래스는 더 일반적입니다. 테스트 사례의 요구 사항에 따라 클래스를 모의 또는 스텁으로 사용할 수 있습니다. 첫 번째 예제에서 FakeOrder 클래스는 스텁으로 사용되며 Assert 작업 중에 사용되지 않습니다. 이 코드는 생성자의 요구 사항을 충족하기 위해 FakeOrder 클래스를 Purchase 클래스에 전달합니다.

클래스를 모의 클래스로 사용하려면 코드를 업데이트할 수 있습니다.

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

purchase.ValidateOrders();

Assert.True(mockOrder.Validated);

이 디자인에서 코드는 fake의 속성을 확인하므로(이에 대해 어설션), 따라서 mockOrder 클래스는 모의 클래스입니다.

중요합니다

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

모의 객체와 스텁의 차이점에서 기억해야 할 중요한 점은 모의 객체는 Assert 프로세스를 제외하고 스텁과 같다는 것입니다. 모의 개체에 대해 Assert 작업을 실행하지만 스텁에 대해 실행하지는 않습니다.

모범 사례

단위 테스트를 작성할 때 따라야 할 몇 가지 중요한 모범 사례가 있습니다. 다음 섹션에서는 코드에 모범 사례를 적용하는 방법을 보여 주는 예제를 제공합니다.

인프라 종속성 방지

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

테스트 명명 표준 준수

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

  • 테스트 중인 메서드의 이름
  • 메서드를 테스트하는 시나리오
  • 시나리오가 호출될 때 예상되는 동작

명명 표준은 테스트 목적과 애플리케이션을 표현하는 데 도움이 되므로 중요합니다. 테스트는 코드가 작동하는지 확인하는 것 이상입니다. 또한 설명서도 제공합니다. 단위 테스트 제품군을 살펴보면 코드의 동작을 유추할 수 있어야 하며 코드 자체를 볼 필요가 없습니다. 또한 테스트가 실패하면 예상을 충족하지 않는 시나리오를 정확하게 확인할 수 있습니다.

원래 코드

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

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

    Assert.Equal(0, actual);
}

모범 사례 적용

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

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

    Assert.Equal(0, actual);
}

테스트를 준비하세요

"정렬, 작업, 검증" 패턴은 유닛 테스트를 작성하는 일반적인 방법입니다. 이름에서 알 수 있듯이 패턴은 다음 세 가지 주요 작업으로 구성됩니다.

  • 개체를 정렬하고 필요에 따라 개체를 만들고 구성합니다.
  • 개체에 Act 수행
  • 이 예상대로임을 확인한다

패턴을 따르면 테스트할 항목을 정렬 및 어설션 작업과 명확하게 구분할 수 있습니다. 이 패턴은 또한 어설션이 Act 태스크의 코드와 섞일 기회를 줄이는 데 도움이 됩니다.

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

원래 코드

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

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

모범 사례 적용

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

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

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

최소 통과 테스트 작성

단위 테스트에 대한 입력은 현재 테스트 중인 동작을 확인하는 데 필요한 가장 간단한 정보여야 합니다. 미니멀한 접근 방식을 사용하면 테스트가 코드베이스의 향후 변경 내용에 대한 복원력을 높일 수 있으며 구현을 통해 동작을 확인하는 데 집중할 수 있습니다.

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

원래 코드

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

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

    Assert.Equal(42, actual);
}

모범 사례 적용

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

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

    Assert.Equal(0, actual);
}

매직 문자열 방지

매직 문자열 코드 추가 주석이나 컨텍스트 없이 단위 테스트에서 직접 하드 코딩된 문자열 값입니다. 이러한 값을 사용하면 코드를 읽기 낮게 만들고 유지 관리하기가 더 어려워집니다. 매직 문자열은 테스트 판독기에게 혼동을 일으킬 수 있습니다. 문자열이 일반 값과 다른 경우 매개 변수 또는 반환 값에 대해 특정 값을 선택한 이유가 궁금할 수 있습니다. 이러한 형식의 문자열 값은 테스트에 집중하지 않고 구현 세부 정보를 자세히 살펴볼 수 있습니다.

팁 (조언)

단위 테스트 코드에서 가능한 한 많은 의도를 표현하는 것을 목표로 합니다. 매직 문자열을 사용하는 대신 상수에 하드 코딩된 값을 할당합니다.

원래 코드

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

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

    Assert.Throws<OverflowException>(actual);
}

모범 사례 적용

[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, while, forswitch및 기타 조건)을 사용하지 않습니다. 테스트 도구 모음에 논리를 포함하면 버그를 도입할 가능성이 크게 증가합니다. 버그를 찾으려는 마지막 위치는 테스트 도구 모음 내에 있습니다. 테스트가 작동한다는 높은 수준의 신뢰도를 가져야 합니다. 그렇지 않으면 신뢰할 수 없습니다. 신뢰하지 않는 테스트는 값을 제공하지 않습니다. 테스트가 실패하면 코드에 문제가 있고 무시할 수 없다는 느낌을 주려고 합니다.

팁 (조언)

테스트에 논리를 추가하는 것이 불가피한 경우 논리 요구 사항을 제한하기 위해 테스트를 두 개 이상의 다른 테스트로 분할하는 것이 좋습니다.

원래 코드

[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;
    }
}

모범 사례 적용

[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);
}

설치 및 중단 대신 도우미 메서드 사용

테스트에 유사한 개체 또는 상태가 필요한 경우, SetupTeardown 특성이 존재하면, 대신 도우미 메서드를 사용하세요. 도우미 메서드는 다음과 같은 여러 가지 이유로 이러한 특성보다 선호됩니다.

  • 각 테스트 내에서 모든 코드가 표시되므로 테스트를 읽을 때 혼동이 줄어듭니다.
  • 지정된 테스트에 대해 너무 많이 또는 너무 적게 설정할 가능성이 적습니다.
  • 테스트 간에 상태를 공유할 가능성이 적으므로 테스트 간에 원치 않는 종속성이 생성됩니다.

단위 테스트 프레임워크에서 Setup 특성은 테스트 도구 모음 내의 각 단위 테스트 전에 호출됩니다. 일부 프로그래머는 이 동작이 유용하다고 생각하지만, 종종 비대해지고 읽기 어려운 테스트가 발생합니다. 일반적으로 각 테스트에는 설치 및 실행에 대한 요구 사항이 다릅니다. 아쉽게도 Setup 특성은 각 테스트에 대해 정확히 동일한 요구 사항을 사용해야 합니다.

비고

SetUpTearDown 특성은 xUnit 버전 2.x 이상에서 제거됩니다.

원래 코드

모범 사례 적용

private readonly StringCalculator stringCalculator;
public StringCalculatorTests()
{
    stringCalculator = new StringCalculator();
}
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var stringCalculator = CreateDefaultStringCalculator();

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

    Assert.Equal(1, actual);
}
// More tests...
// More tests...
[Fact]
public void Add_TwoNumbers_ReturnsSumOfNumbers()
{
    var result = stringCalculator.Add("0,1");

    Assert.Equal(1, result);
}
private StringCalculator CreateDefaultStringCalculator()
{
    return new StringCalculator();
}

여러 작업 방지

테스트를 작성할 때는 테스트당 하나의 Act 작업만 포함하려고 합니다. 단일 Act 작업을 구현하기 위한 몇 가지 일반적인 방법은 각 Act에 대해 별도의 테스트를 만들거나 매개 변수가 있는 테스트를 사용하는 것입니다. 각 테스트에 단일 Act 작업을 사용하는 경우 다음과 같은 몇 가지 이점이 있습니다.

  • 테스트가 실패할 경우 어떤 작업 작업이 실패하는지 쉽게 파악할 수 있습니다.
  • 테스트가 단일 사례에만 집중되도록 할 수 있습니다.
  • 테스트가 실패하는 이유에 대한 명확한 그림을 얻을 수 있습니다.

여러 Act 작업을 개별적으로 어설션해야 하며 모든 Assert 작업이 실행되도록 보장할 수 없습니다. 대부분의 단위 테스트 프레임워크에서 단위 테스트에서 Assert 태스크가 실패하면 모든 후속 테스트가 자동으로 실패로 간주됩니다. 일부 작업 기능이 실패로 해석될 수 있으므로 프로세스가 혼동될 수 있습니다.

원래 코드

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

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

모범 사례 적용

[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 메서드를 사용하여 프라이빗 메서드 유효성 검사

대부분의 경우 코드에서 프라이빗 메서드를 테스트할 필요가 없습니다. 프라이빗 메서드는 구현 세부 사항의 일부이며 독립적으로 존재하지 않습니다. 개발 프로세스의 어느 시점에서는 프라이빗 메서드를 구현의 일부로 호출하는 공용 메서드를 도입합니다. 단위 테스트를 작성할 때 관심 있는 것은 프라이빗 메서드를 호출하는 공용 메서드의 최종 결과입니다.

다음 코드 시나리오를 고려합니다.

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);
}

private 메서드가 발견되면 private 메서드를 호출하는 public 메서드를 찾고 public 메서드에 대한 테스트를 작성합니다. 프라이빗 메서드가 예상된 결과를 반환한다고 해서 결국 프라이빗 메서드를 호출하는 시스템이 결과를 올바르게 사용하는 것은 아닙니다.

이음 매커니즘을 사용하여 스텁 정적 참조를 처리하세요.

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

다음 코드 시나리오를 검토합니다.

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

이 코드에 대한 단위 테스트를 작성할 수 있나요? priceAssert 작업을 실행해 볼 수 있습니다.

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 값을 완전히 제어하고 메서드를 호출할 때 모든 값을 스텁할 수 있습니다.