การทดสอบหน่วย: DateTime.Now


164

ฉันมีการทดสอบหน่วยที่คาดว่า 'เวลาปัจจุบัน' จะแตกต่างจาก DateTime ตอนนี้และฉันไม่ต้องการเปลี่ยนเวลาของคอมพิวเตอร์อย่างชัดเจน

กลยุทธ์ที่ดีที่สุดในการบรรลุเป้าหมายนี้คืออะไร?


เก็บค่าเก่าไว้ในเวลาปัจจุบันและเปรียบเทียบกับ datetime ในขณะที่การทดสอบหน่วยใช้ข้อมูลปลอมคุณสามารถทำสิ่งนั้นได้อย่างง่ายดายใช่ไหม
Prashant Lakhlani

3
การให้วันที่ปัจจุบันเป็นนามธรรมนั้นมีประโยชน์ไม่เพียง แต่สำหรับการทดสอบและการดีบักเท่านั้น แต่ยังอยู่ในรหัสการผลิตด้วย - ขึ้นอยู่กับความต้องการของแอปพลิเคชัน
Pavel Hodek

1
อย่าใช้คลาสคงที่เพื่อรับ DateTime
Daniel Little

และอีกวิธีง่ายๆในการใช้ VirtualTime ด้านล่าง
Samuel

DateTimeทางเลือกหนึ่งซึ่งอาจจะทำงานและคุณสามารถที่จะเพิ่มเกินคือการสร้างเกินพิกัดของวิธีการที่ยอมรับ nowนี้จะเป็นคนที่คุณทดสอบวิธีการที่มีอยู่ก็จะเรียกเกินด้วย
Phil Cooper

คำตอบ:


219

ที่ดีที่สุดกลยุทธ์คือการห่อเวลาปัจจุบันในนามธรรมและฉีดนามธรรมที่เป็นผู้บริโภค


อีกทางหนึ่งคุณสามารถกำหนดเวลานามธรรมให้เป็นบริบทแวดล้อม :

public abstract class TimeProvider
{
    private static TimeProvider current =
        DefaultTimeProvider.Instance;

    public static TimeProvider Current
    {
       get { return TimeProvider.current; }
       set 
       {
           if (value == null)
           {
               throw new ArgumentNullException("value");
           }
           TimeProvider.current = value; 
       }
   }

   public abstract DateTime UtcNow { get; }

   public static void ResetToDefault()
   {    
       TimeProvider.current = DefaultTimeProvider.Instance;
   }            
}

สิ่งนี้จะช่วยให้คุณบริโภคได้เช่นนี้:

var now = TimeProvider.Current.UtcNow;

ในการทดสอบหน่วยคุณสามารถแทนที่TimeProvider.Currentด้วยวัตถุทดสอบคู่ / จำลอง ตัวอย่างการใช้ Moq:

var timeMock = new Mock<TimeProvider>();
timeMock.SetupGet(tp => tp.UtcNow).Returns(new DateTime(2010, 3, 11));
TimeProvider.Current = timeMock.Object;

แต่เมื่อหน่วยทดสอบกับรัฐคงจำไว้เสมอที่จะฉีกลงประจำที่ของคุณTimeProvider.ResetToDefault()โดยการเรียก


7
ในกรณีที่มีบริบทแวดล้อมแม้ว่าคุณจะทรุดลงคุณจะทำให้การทดสอบของคุณทำงานในแบบคู่ขนานได้อย่างไร
Ilya Chernomordik

2
@IlyaChernomordik คุณไม่ควรฉีด ILogger หรือข้อกังวลอื่น ๆ ข้ามดังนั้นการฉีดผู้ให้เวลาจึงเป็นตัวเลือกที่ดีที่สุด
Mark Seemann

5
@MikeK ในฐานะที่เป็นประโยคแรกในคำตอบของฉันพูดว่า: กลยุทธ์ที่ดีที่สุดคือการห่อเวลาปัจจุบันในสิ่งที่เป็นนามธรรมและฉีดสิ่งที่เป็นนามธรรมให้กับผู้บริโภค ทุกอย่างอื่นในคำตอบนี้เป็นทางเลือกที่ดีที่สุดอันดับสอง
Mark Seemann

3
สวัสดี ฉัน Noob วิธีหนึ่งที่จะป้องกันไม่ให้ผู้คนเข้าถึง DateTimeUtcNow กับวิธีแก้ปัญหานี้? มันจะง่ายสำหรับคนที่จะพลาดการใช้ TimeProvider จึงนำไปสู่ข้อบกพร่องในการทดสอบ?
Obbles

1
ความคิดเห็น @Obbles รหัส (หรือการเขียนโปรแกรมคู่ซึ่งเป็นรูปแบบของการทบทวนรหัสสด) ฉันคิดว่าสามารถเขียนเครื่องมือที่สแกนฐานของรหัสสำหรับDateTime.UtcNowและคล้ายกัน แต่ความคิดเห็นรหัสเป็นความคิดที่ดีอยู่แล้ว
Mark Seemann

58

ทั้งหมดนี้เป็นคำตอบที่ดีนี่คือสิ่งที่ฉันทำในโครงการอื่น:

การใช้งาน:

รับวันที่ที่แท้จริงของวันนี้เวลา

var today = SystemTime.Now().Date;

แทนที่จะใช้ DateTime ตอนนี้คุณต้องใช้SystemTime.Now()... มันไม่ได้เปลี่ยนแปลงอย่างหนัก แต่วิธีนี้อาจไม่เหมาะกับทุกโครงการ

การเดินทางข้ามเวลา (ไปได้อีก 5 ปีข้างหน้า)

SystemTime.SetDateTime(today.AddYears(5));

รับของปลอม "วันนี้" (จะเป็น 5 ปีจาก 'วันนี้')

var fakeToday = SystemTime.Now().Date;

รีเซ็ตวันที่

SystemTime.ResetDateTime();

/// <summary>
/// Used for getting DateTime.Now(), time is changeable for unit testing
/// </summary>
public static class SystemTime
{
    /// <summary> Normally this is a pass-through to DateTime.Now, but it can be overridden with SetDateTime( .. ) for testing or debugging.
    /// </summary>
    public static Func<DateTime> Now = () => DateTime.Now;

    /// <summary> Set time to return when SystemTime.Now() is called.
    /// </summary>
    public static void SetDateTime(DateTime dateTimeNow)
    {
        Now = () =>  dateTimeNow;
    }

    /// <summary> Resets SystemTime.Now() to return DateTime.Now.
    /// </summary>
    public static void ResetDateTime()
    {
        Now = () => DateTime.Now;
    }
}

1
ขอบคุณฉันชอบโปรแกรมนี้ aproach วันนี้ (สองปีต่อมา) ฉันยังคงใช้คลาส TimeProvider รุ่นที่แก้ไข (ตรวจสอบคำตอบที่ยอมรับ) มันใช้งานได้ดีจริงๆ
Pedro

7
@crabCRUSHERclamCOLI: หากคุณมีความคิดจากayende.com/blog/3408/dealing-with-time-in-test-th-th-th-th-th-test-tool-tool-china-device.htmการเชื่อมโยงกับมัน
Johann Gerell

3
แนวคิดนี้ยังใช้งานได้ดีในการจำลอง Guid generation (public static Func <Guid> NewGuid = () => Guid.NewGuid ();
mdwhatcott

2
คุณยังสามารถเพิ่มวิธีที่มีประโยชน์นี้ได้: โมฆะคงที่สาธารณะ ResetDateTime (DateTime dateTimeNow) {var timespanDiff = TimeSpan.FromTicks (DateTime.Now.Ticks - dateTimeNow.Ticks); ตอนนี้ = () => DateTime.Now - timespanDiff; }
Pavel Hodek

17
อันตรายมากซึ่งอาจส่งผลต่อการทดสอบหน่วยอื่น ๆ ที่ทำงานแบบขนาน
Amete ได้รับพร

24

โมล:

[Test]  
public void TestOfDateTime()  
{  
      var firstValue = DateTime.Now;
      MDateTime.NowGet = () => new DateTime(2000,1,1);
      var secondValue = DateTime.Now;
      Assert(firstValue > secondValue); // would be false if 'moleing' failed
}

ข้อจำกัดความรับผิดชอบ - ฉันทำงานกับตุ่น


1
น่าเสียดายที่ไม่สามารถใช้กับ nunit และ resharper ได้
58

ดูเหมือนว่าไฝไม่ได้รับการสนับสนุนในขณะนี้แทนที่ด้วย Fakes msdn.microsoft.com/en-us/library/…ไม่ต้องสงสัยเลยว่า MS จะหยุดการทำงานในไม่ช้านี้
danio

17

คุณมีตัวเลือกสำหรับการทำ:

  1. ใช้กรอบการเยาะเย้ยและใช้ DateTimeService (ใช้คลาส wrapper ขนาดเล็กและฉีดให้เป็นรหัสการผลิต) การประยุกต์ใช้ wrapper จะเข้าถึง DateTime และในการทดสอบคุณจะสามารถจำลองคลาส wrapper ได้

  2. ใช้Typemock Isolatorมันสามารถปลอม DateTime ตอนนี้และคุณจะไม่ต้องเปลี่ยนรหัสภายใต้การทดสอบ

  3. ใช้ไฝมันสามารถปลอม DateTime ตอนนี้และไม่จำเป็นต้องเปลี่ยนรหัสการผลิต

ตัวอย่างบางส่วน:

คลาส Wrapper โดยใช้ Moq:

[Test]
public void TestOfDateTime()
{
     var mock = new Mock<IDateTime>();
     mock.Setup(fake => fake.Now)
         .Returns(new DateTime(2000, 1, 1));

     var result = new UnderTest(mock.Object).CalculateSomethingBasedOnDate();
}

public class DateTimeWrapper : IDateTime
{
      public DateTime Now { get { return DateTime.Now; } }
}

แกล้ง DateTime โดยตรงโดยใช้ Isolator:

[Test]
public void TestOfDateTime()
{
     Isolate.WhenCalled(() => DateTime.Now).WillReturn(new DateTime(2000, 1, 1));

     var result = new UnderTest().CalculateSomethingBasedOnDate();
}

ข้อจำกัดความรับผิดชอบ - ฉันทำงานที่ Typemock


12

เพิ่มชุดประกอบปลอมสำหรับระบบ (คลิกขวาที่การอ้างอิงระบบ => เพิ่มชุดประกอบปลอม)

และเขียนวิธีการทดสอบของคุณ:

using (ShimsContext.Create())
{
   System.Fakes.ShimDateTime.NowGet = () => new DateTime(2014, 3, 10);
   MethodThatUsesDateTimeNow();
}

หากคุณสามารถเข้าถึง Vs Premium หรือ ultimate นี่คือวิธีที่ง่ายที่สุด / เร็วที่สุดในการทำ
Rugdr

7

เกี่ยวกับ @crabcrusherclamcollector คำตอบมีปัญหาเมื่อใช้วิธีการดังกล่าวในการสืบค้นของ EF (System.NotSupportedException: ประเภทโหนดนิพจน์ LINQ 'เรียกใช้' ไม่รองรับใน LINQ ไปยังเอนทิตี) ฉันแก้ไขการใช้งานเป็น:

public static class SystemTime
    {
        private static Func<DateTime> UtcNowFunc = () => DateTime.UtcNow;

        public static void SetDateTime(DateTime dateTimeNow)
        {
            UtcNowFunc = () => dateTimeNow;
        }

        public static void ResetDateTime()
        {
            UtcNowFunc = () => DateTime.UtcNow;
        }

        public static DateTime UtcNow
        {
            get
            {
                DateTime now = UtcNowFunc.Invoke();
                return now;
            }
        }
    }

ฉันเดาว่าคุณหมายถึง Entity Framework หรือไม่ วัตถุที่คุณกำลังใช้ SystemTime มีลักษณะอย่างไร ฉันเดาว่าคุณมีการอ้างอิงไปยัง SYSTEMTIME ในกิจการที่เกิดขึ้นจริง แต่คุณควรใช้วัตถุ DateTime ปกติในนิติบุคคลของคุณและตั้งใช้ SYSTEMTIME ใดก็ตามที่มันทำให้ความรู้สึกในการใช้งานของคุณ
crabCRUSHERclamCOLLECTOR

1
ใช่ฉันได้ทำการทดสอบเมื่อสร้างเคียวรี linq และ EntityFramework6 และการอ้างอิงในเคียวรีนั้น UtcNow จาก SystemTime มีข้อยกเว้นตามที่อธิบายไว้ หลังจากเปลี่ยนการใช้งานมันทำงานได้ดี
marcinn

ฉันชอบวิธีนี้! ที่ดี!
mirind4

7

การทดสอบรหัสที่ขึ้นอยู่กับการSystem.DateTimeที่system.dllจะต้องเยาะเย้ย

มีสองกรอบที่ฉันรู้ว่าทำสิ่งนี้ ปลอมไมโครซอฟท์และเสื้อคลุม

Microsoft fakes ต้องการ visual Studio 2012 ultimatum และใช้งานได้โดยตรงจากคอมพ์ตัน

เสื้อคลุมเป็นโอเพนซอร์สและใช้งานง่ายมาก สามารถดาวน์โหลดได้โดยใช้ NuGet

ต่อไปนี้แสดงให้เห็นถึงการเยาะเย้ยของSystem.DateTime:

Smock.Run(context =>
{
  context.Setup(() => DateTime.Now).Returns(new DateTime(2000, 1, 1));

   // Outputs "2000"
   Console.WriteLine(DateTime.Now.Year);
});

5

หัวข้อความปลอดภัยในการSystemClockใช้ThreadLocal<T>งานได้ดีสำหรับฉัน

ThreadLocal<T> มีอยู่ใน. Net Framework v4.0 และสูงกว่า

/// <summary>
/// Provides access to system time while allowing it to be set to a fixed <see cref="DateTime"/> value.
/// </summary>
/// <remarks>
/// This class is thread safe.
/// </remarks>
public static class SystemClock
{
    private static readonly ThreadLocal<Func<DateTime>> _getTime =
        new ThreadLocal<Func<DateTime>>(() => () => DateTime.Now);

    /// <inheritdoc cref="DateTime.Today"/>
    public static DateTime Today
    {
        get { return _getTime.Value().Date; }
    }

    /// <inheritdoc cref="DateTime.Now"/>
    public static DateTime Now
    {
        get { return _getTime.Value(); }
    }

    /// <inheritdoc cref="DateTime.UtcNow"/>
    public static DateTime UtcNow
    {
        get { return _getTime.Value().ToUniversalTime(); }
    }

    /// <summary>
    /// Sets a fixed (deterministic) time for the current thread to return by <see cref="SystemClock"/>.
    /// </summary>
    public static void Set(DateTime time)
    {
        if (time.Kind != DateTimeKind.Local)
            time = time.ToLocalTime();

        _getTime.Value = () => time;
    }

    /// <summary>
    /// Resets <see cref="SystemClock"/> to return the current <see cref="DateTime.Now"/>.
    /// </summary>
    public static void Reset()
    {
        _getTime.Value = () => DateTime.Now;
    }
}

ตัวอย่างการใช้งาน:

[TestMethod]
public void Today()
{
    SystemClock.Set(new DateTime(2015, 4, 3));

    DateTime expectedDay = new DateTime(2015, 4, 2);
    DateTime yesterday = SystemClock.Today.AddDays(-1D);
    Assert.AreEqual(expectedDay, yesterday);

    SystemClock.Reset();
}

1
+! เธรดโลคัลควรหลีกเลี่ยงปัญหาเกี่ยวกับการดำเนินการทดสอบแบบขนานและการทดสอบหลายรายการอาจแทนที่Nowอินสแตนซ์ปัจจุบัน
Hux

1
พยายามใช้สิ่งนี้ แต่มีปัญหาข้ามเธรดดังนั้นจึงใช้หลักการที่คล้ายกัน แต่ใช้AsyncLocalแทนThreadLocal
rrrr

@rrrr: คุณสามารถอธิบายปัญหาที่คุณมีหรือไม่
Henk van Boeijen

@HenkvanBoeijen แน่นอนว่าเราพบปัญหาเมื่อถึงเวลาที่กำหนดและจากนั้นเรากำลังรอวิธีการซิงค์ Console.WriteLine(SystemClock.Now); SystemClock.Set(new DateTime(2017, 01, 01)); Console.WriteLine(SystemClock.Now); await Task.Delay(1000); Console.WriteLine(SystemClock.Now);
rrrr

@HenkvanBoeijen ฉันได้เพิ่มคำตอบกับสิ่งที่ฉันสิ้นสุดที่ใช้การเชื่อมโยง
rrrr

3

ฉันพบปัญหาเดียวกันนี้ แต่พบโครงการวิจัยจาก Microsoft ที่แก้ปัญหานี้ได้

http://research.microsoft.com/en-us/projects/moles/

โมลเป็นเฟรมเวิร์กที่มีน้ำหนักเบาสำหรับการทดสอบสตับและการออกนอกเส้นทางใน. NET ซึ่งขึ้นอยู่กับผู้รับมอบสิทธิ์ โมลอาจถูกใช้เพื่ออ้อมใด ๆ วิธี. NET รวมถึงวิธีการที่ไม่ใช่เสมือน / คงที่ในประเภทที่ปิดผนึก

// Let's detour DateTime.Now
MDateTime.NowGet = () => new DateTime(2000,1, 1);

if (DateTime.Now == new DateTime(2000, 1, 1);
{
    throw new Exception("Wahoo we did it!");
}

โค้ดตัวอย่างถูกแก้ไขจากต้นฉบับ

ฉันทำสิ่งที่คนอื่นแนะนำและทำให้เป็นนามธรรม DateTime เป็นผู้ให้บริการ มันรู้สึกผิดและฉันรู้สึกว่ามันมากเกินไปสำหรับการทดสอบ ฉันจะนำสิ่งนี้ไปใช้ในโครงการส่วนตัวของฉันในเย็นวันนี้


2
BTW ใช้งานได้กับ VS2010 เท่านั้น ฉันรู้สึกไม่สบายใจเมื่อพบว่าไฝเป็นของปลอมใน VS2012 และมีให้เฉพาะกับ Premium และ Ultimate Visual Studios ไม่มีการปลอมแปลงสำหรับ VS 2012 Professional ดังนั้นฉันจึงไม่ต้องใช้สิ่งนี้จริงๆ :(
Bobby Cannon

3

หมายเหตุพิเศษหนึ่งในการเยาะเย้ยDateTime.Nowด้วยTypeMock ...

ค่าของDateTime.Nowต้องถูกวางลงในตัวแปรเพื่อให้ถูกเยาะเย้ยอย่างถูกต้อง ตัวอย่างเช่น:

สิ่งนี้ไม่ทำงาน:

if ((DateTime.Now - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))

อย่างไรก็ตามสิ่งนี้จะ:

var currentDateTime = DateTime.Now;
if ((currentDateTime - message.TimeOpened.Value) > new TimeSpan(1, 0, 0))

2

วัตถุจำลอง

DateTime จำลองที่คืนค่าทันทีซึ่งเหมาะสมกับการทดสอบของคุณ


2

ฉันประหลาดใจที่ไม่มีใครแนะนำวิธีที่ชัดเจนที่สุดในการไป:

public class TimeDependentClass
{
    public void TimeDependentMethod(DateTime someTime)
    {
        if (GetCurrentTime() > someTime) DoSomething();
    }

    protected virtual DateTime GetCurrentTime()
    {
        return DateTime.Now; // or UtcNow
    }
}

จากนั้นคุณสามารถแทนที่วิธีนี้ในการทดสอบสองครั้ง

ฉันก็ชอบฉีดTimeProviderคลาสในบางกรณี แต่สำหรับคนอื่น ๆ นี่มันเกินพอ ฉันอาจจะชอบTimeProviderเวอร์ชั่นนี้ถ้าคุณจำเป็นต้องนำมันกลับมาใช้ใหม่ในหลาย ๆ คลาส

แก้ไข: สำหรับทุกคนที่สนใจสิ่งนี้เรียกว่าการเพิ่ม "ตะเข็บ" ให้กับชั้นเรียนของคุณจุดที่คุณสามารถเชื่อมต่อกับพฤติกรรมของมันเพื่อแก้ไข (สำหรับวัตถุประสงค์ในการทดสอบหรืออื่น ๆ ) โดยไม่ต้องเปลี่ยนรหัสในชั้นเรียนจริง


1

แนวปฏิบัติที่ดีคือเมื่อ DateTimeProvider ใช้ IDisposable

public class DateTimeProvider : IDisposable 
{ 
    [ThreadStatic] 
    private static DateTime? _injectedDateTime; 

    private DateTimeProvider() 
    { 
    } 

    /// <summary> 
    /// Gets DateTime now. 
    /// </summary> 
    /// <value> 
    /// The DateTime now. 
    /// </value> 
    public static DateTime Now 
    { 
        get 
        { 
            return _injectedDateTime ?? DateTime.Now; 
        } 
    } 

    /// <summary> 
    /// Injects the actual date time. 
    /// </summary> 
    /// <param name="actualDateTime">The actual date time.</param> 
    public static IDisposable InjectActualDateTime(DateTime actualDateTime) 
    { 
        _injectedDateTime = actualDateTime; 

        return new DateTimeProvider(); 
    } 

    public void Dispose() 
    { 
        _injectedDateTime = null; 
    } 
} 

ถัดไปคุณสามารถฉีด DateTime ปลอมสำหรับการทดสอบหน่วย

    using (var date = DateTimeProvider.InjectActualDateTime(expectedDateTime)) 
    { 
        var bankAccount = new BankAccount(); 

        bankAccount.DepositMoney(600); 

        var lastTransaction = bankAccount.Transactions.Last(); 

        Assert.IsTrue(expectedDateTime.Equals(bankAccount.Transactions[0].TransactionDate)); 
    } 

ดูตัวอย่างตัวอย่างของ DateTimeProvider


1

วิธีหนึ่งที่สะอาดในการทำเช่นนี้คือการฉีด VirtualTime ช่วยให้คุณสามารถควบคุมเวลา ก่อนติดตั้ง VirtualTime

Install-Package VirtualTime

ที่ช่วยให้ตัวอย่างเช่นทำให้เวลาที่เคลื่อนที่เร็วขึ้น 5 เท่าสำหรับการโทรทั้งหมดไปยัง DateTime.Now หรือ UtcNow

var DateTime = DateTime.Now.ToVirtualTime(5);

หากต้องการให้เวลาเคลื่อนที่ช้าลงเช่นทำช้าลง 5 เท่า

var DateTime = DateTime.Now.ToVirtualTime(0.5);

เพื่อให้เวลาหยุดนิ่งทำ

var DateTime = DateTime.Now.ToVirtualTime(0);

การย้อนกลับไปในเวลายังไม่ได้รับการทดสอบ

นี่คือตัวอย่างทดสอบ:

[TestMethod]
public void it_should_make_time_move_faster()
{
    int speedOfTimePerMs = 1000;
    int timeToPassMs = 3000;
    int expectedElapsedVirtualTime = speedOfTimePerMs * timeToPassMs;
    DateTime whenTimeStarts = DateTime.Now;
    ITime time = whenTimeStarts.ToVirtualTime(speedOfTimePerMs);
    Thread.Sleep(timeToPassMs);
    DateTime expectedTime = DateTime.Now.AddMilliseconds(expectedElapsedVirtualTime - timeToPassMs);
    DateTime virtualTime = time.Now;

    Assert.IsTrue(TestHelper.AreEqualWithinMarginOfError(expectedTime, virtualTime, MarginOfErrorMs));
}

คุณสามารถตรวจสอบการทดสอบเพิ่มเติมได้ที่นี่:

https://github.com/VirtualTime/VirtualTime/blob/master/VirtualTimeLib.Tests/when_virtual_time_is_used.cs

ส่วนขยาย DateTime.Now.ToVirtualTime ใดให้คุณเป็นตัวอย่างของ ITime ที่คุณส่งผ่านไปยังเมธอด / คลาสที่ขึ้นอยู่กับ ITime DateTime.Now.ToVirtualTime บางตัวได้รับการตั้งค่าในคอนเทนเนอร์ DI ที่คุณเลือก

นี่คืออีกตัวอย่างหนึ่งที่ฉีดเข้าไปในผู้ควบคุมชั้นเรียน

public class AlarmClock
{
    private ITime DateTime;
    public AlarmClock(ITime dateTime, int numberOfHours)
    {
        DateTime = dateTime;
        SetTime = DateTime.UtcNow.AddHours(numberOfHours);
        Task.Run(() =>
        {
            while (!IsAlarmOn)
            {
                IsAlarmOn = (SetTime - DateTime.UtcNow).TotalMilliseconds < 0;
            }
        });
    }
    public DateTime SetTime { get; set; }
    public bool IsAlarmOn { get; set; }
}

[TestMethod]
public void it_can_be_injected_as_a_dependency()
{
    //virtual time has to be 1000*3.75 faster to get to an hour 
    //in 1000 ms real time
    var dateTime = DateTime.Now.ToVirtualTime(1000 * 3.75);
    var numberOfHoursBeforeAlarmSounds = 1;
    var alarmClock = new AlarmClock(dateTime, numberOfHoursBeforeAlarmSounds);
    Assert.IsFalse(alarmClock.IsAlarmOn);
    System.Threading.Thread.Sleep(1000);
    Assert.IsTrue(alarmClock.IsAlarmOn);
}

นอกจากนี้ยังมีการอ่านที่ดีเกี่ยวกับการทดสอบรหัสที่ขึ้นอยู่กับวันที่และเวลาโดย Ayende นี่ayende.com/blog/3408/dealing-with-time-in-tests
ซามูเอล

1

เรากำลังใช้วัตถุ SystemTime แบบคงที่ แต่พบปัญหาในการทดสอบหน่วยแบบขนาน ฉันพยายามใช้วิธีแก้ปัญหาของ Henk van Boeijen แต่มีปัญหาในเธรดแบบอะซิงโครนัสที่วางไข่เกิดปัญหาโดยใช้ AsyncLocal ในลักษณะคล้ายกับด้านล่างนี้:

public static class Clock
{
    private static Func<DateTime> _utcNow = () => DateTime.UtcNow;

    static AsyncLocal<Func<DateTime>> _override = new AsyncLocal<Func<DateTime>>();

    public static DateTime UtcNow => (_override.Value ?? _utcNow)();

    public static void Set(Func<DateTime> func)
    {
        _override.Value = func;
    }

    public static void Reset()
    {
        _override.Value = null;
    }
}

แหล่งที่มาจากhttps://gist.github.com/CraftyFella/42f459f7687b0b8b268fc311e6b4af08


1

คำถามเก่า แต่ก็ยังใช้ได้

วิธีการของฉันคือการสร้างอินเทอร์เฟซและคลาสใหม่เพื่อตัดการSystem.DateTime.Nowโทร

public interface INow
{
    DateTime Execute();
}

public sealed class Now : INow
{
    public DateTime Execute()
    {
        return DateTime.Now
    }
}

อินเทอร์เฟซนี้สามารถถูกแทรกลงในคลาสใด ๆ ที่ต้องการรับวันและเวลาปัจจุบัน ในตัวอย่างนี้ฉันมีคลาสที่เพิ่มช่วงเวลาให้กับวันที่และเวลาปัจจุบัน (หน่วยที่ทดสอบได้ System.DateTime.Now.Now.Add (TimeSpan))

public interface IAddTimeSpanToCurrentDateAndTime
{
    DateTime Execute(TimeSpan input);
}

public class AddTimeSpanToCurrentDateAndTime : IAddTimeSpanToCurrentDateAndTime
{
    private readonly INow _now;

    public AddTimeSpanToCurrentDateAndTime(INow now)
    {
        this._now = now;
    }

    public DateTime Execute(TimeSpan input)
    {
        var currentDateAndTime = this._now.Execute();

        return currentDateAndTime.Add(input);
    }
}

และสามารถเขียนการทดสอบเพื่อให้แน่ใจว่าทำงานได้อย่างถูกต้อง ฉันใช้ NUnit และ Moq แต่กรอบการทดสอบใด ๆ ที่จะทำ

public class Execute
{
    private Moq.Mock<INow> _nowMock;

    private AddTimeSpanToCurrentDateAndTime _systemUnderTest;

    [SetUp]
    public void Initialize()
    {
        this._nowMock = new Moq.Mock<INow>(Moq.MockBehavior.Strict);

        this._systemUnderTest = AddTimeSpanToCurrentDateAndTime(
            this._nowMock.Object);
    }

    [Test]
    public void AddTimeSpanToCurrentDateAndTimeExecute0001()
    {
        // arrange

        var input = new TimeSpan(911252);

        // arrange : mocks

        this._nowMock
            .Setup(a => a.Execute())
            .Returns(new DateTime(348756););

        // arrange : expected

        var expected = new DateTime(911252 + 348756);

        // act

        var actual = this._systemUnderTest.Execute(input).Result;

        // assert

        Assert.Equals(actual, expected);
    }
}

รูปแบบนี้จะทำงานสำหรับฟังก์ชั่นใด ๆ ซึ่งขึ้นอยู่กับปัจจัยภายนอกเช่นSystem.Random.Next(), System.DateTime.Now.UtcNow, System.Guid.NewGuid()ฯลฯ

ดูhttps://loadlimited.visualstudio.com/Stamina/_git/Stamina.Coreสำหรับตัวอย่างเพิ่มเติมหรือรับhttps://www.nuget.org/packages/Stamina.Coreแพคเกจ nuget


1

คุณสามารถเปลี่ยนคลาสที่คุณกำลังทดสอบเพื่อใช้Func<DateTime>ซึ่งจะถูกส่งผ่านมันเป็นพารามิเตอร์คอนสตรัคเตอร์ดังนั้นเมื่อคุณสร้างอินสแตนซ์ของคลาสในรหัสจริงคุณสามารถส่ง() => DateTime.UtcNowไปที่Func<DateTime>พารามิเตอร์และในการทดสอบคุณสามารถผ่านเวลาที่คุณ ต้องการทดสอบ

ตัวอย่างเช่น:

    [TestMethod]
    public void MyTestMethod()
    {
        var instance = new MyClass(() => DateTime.MinValue);
        Assert.AreEqual(instance.MyMethod(), DateTime.MinValue);
    } 

    public void RealWorldInitialization()
    {
        new MyClass(() => DateTime.UtcNow);
    }

    class MyClass
    {
        private readonly Func<DateTime> _utcTimeNow;

        public MyClass(Func<DateTime> UtcTimeNow)
        {
            _utcTimeNow = UtcTimeNow;
        }

        public DateTime MyMethod()
        {
            return _utcTimeNow();
        }
    }

1
ฉันชอบคำตอบนี้ จากสิ่งที่ฉันรวบรวมมานี่อาจจะเป็นการทำให้ตาคิ้วขึ้นในโลก C # ซึ่งมีการเน้นที่คลาส / วัตถุมากกว่า ฉันชอบสิ่งนี้เพราะมันง่ายและชัดเจนสำหรับฉันว่าเกิดอะไรขึ้น
pseudoramble

0

ฉันมีปัญหาเดียวกัน แต่ฉันคิดว่าเราไม่ควรใช้ชุด datetime ในชั้นเรียนเดียวกัน เพราะอาจนำไปสู่การใช้ผิดวัตถุประสงค์ในวันหนึ่ง ดังนั้นฉันได้ใช้ผู้ให้บริการเช่น

public class DateTimeProvider
{
    protected static DateTime? DateTimeNow;
    protected static DateTime? DateTimeUtcNow;

    public DateTime Now
    {
        get
        {
            return DateTimeNow ?? System.DateTime.Now;
        }
    }

    public DateTime UtcNow
    {
        get
        {
            return DateTimeUtcNow ?? System.DateTime.UtcNow;
        }
    }

    public static DateTimeProvider DateTime
    {
        get
        {
            return new DateTimeProvider();
        }
    }

    protected DateTimeProvider()
    {       
    }
}

สำหรับการทดสอบที่โครงการทดสอบทำผู้ช่วยซึ่งจะจัดการกับสิ่งต่าง ๆ

public class MockDateTimeProvider : DateTimeProvider
{
    public static void SetNow(DateTime now)
    {
        DateTimeNow = now;
    }

    public static void SetUtcNow(DateTime utc)
    {
        DateTimeUtcNow = utc;
    }

    public static void RestoreAsDefault()
    {
        DateTimeNow = null;
        DateTimeUtcNow = null;
    }
}

ในรหัส

var dateTimeNow = DateTimeProvider.DateTime.Now         //not DateTime.Now
var dateTimeUtcNow = DateTimeProvider.DateTime.UtcNow   //not DateTime.UtcNow

และในการทดสอบ

[Test]
public void Mocked_Now()
{
    DateTime now = DateTime.Now;
    MockDateTimeProvider.SetNow(now);    //set to mock
    Assert.AreEqual(now, DateTimeProvider.DateTime.Now);
    Assert.AreNotEqual(now, DateTimeProvider.DateTime.UtcNow);
}

[Test]
public void Mocked_UtcNow()
{
    DateTime utcNow = DateTime.UtcNow;
    MockDateTimeProvider.SetUtcNow(utcNow);   //set to mock
    Assert.AreEqual(utcNow, DateTimeProvider.DateTime.UtcNow);
    Assert.AreNotEqual(utcNow, DateTimeProvider.DateTime.Now);
}

แต่จำเป็นต้องจดจำสิ่งหนึ่งบางครั้ง DateTime ที่แท้จริงและ DateTime ของผู้ให้บริการจะไม่ทำงานเหมือนกัน

[Test]
public void Now()
{
    Assert.AreEqual(DateTime.Now.Kind, DateTimeProvider.DateTime.Now.Kind);
    Assert.LessOrEqual(DateTime.Now, DateTimeProvider.DateTime.Now);
    Assert.LessOrEqual(DateTimeProvider.DateTime.Now - DateTime.Now, TimeSpan.FromMilliseconds(1));
}

ผมถือว่าการแสดงความเคารพจะสูงสุดTimeSpan.FromMilliseconds (0.00002) แต่ส่วนใหญ่มันน้อยกว่า

ค้นหาตัวอย่างได้ที่MockSamples


0

นี่คือ anwer ของคำถามนี้ ฉันรวมรูปแบบ 'บริบทโดยรอบ' กับ IDisposable ดังนั้นคุณสามารถใช้ DateTimeProvider.Current ในรหัสโปรแกรมปกติของคุณและในการทดสอบคุณจะแทนที่ขอบเขตด้วยคำสั่งที่ใช้

using System;
using System.Collections.Immutable;


namespace ambientcontext {

public abstract class DateTimeProvider : IDisposable
{
    private static ImmutableStack<DateTimeProvider> stack = ImmutableStack<DateTimeProvider>.Empty.Push(new DefaultDateTimeProvider());

    protected DateTimeProvider()
    {
        if (this.GetType() != typeof(DefaultDateTimeProvider))
            stack = stack.Push(this);
    }

    public static DateTimeProvider Current => stack.Peek();
    public abstract DateTime Today { get; }
    public abstract DateTime Now {get; }

    public void Dispose()
    {
        if (this.GetType() != typeof(DefaultDateTimeProvider))
            stack = stack.Pop();
    }

    // Not visible Default Implementation 
    private class DefaultDateTimeProvider : DateTimeProvider {
        public override DateTime Today => DateTime.Today; 
        public override DateTime Now => DateTime.Now; 
    }
}
}

นี่คือวิธีการใช้ DateTimeProvider ข้างต้นภายในการทดสอบหน่วย

using System;
using Xunit;

namespace ambientcontext
{
    public class TestDateTimeProvider
    {
        [Fact]
        public void TestDateTime()
        {
            var actual = DateTimeProvider.Current.Today;
            var expected = DateTime.Today;

            Assert.Equal<DateTime>(expected, actual);

            using (new MyDateTimeProvider(new DateTime(2012,12,21)))
            {
                Assert.Equal(2012, DateTimeProvider.Current.Today.Year);

                using (new MyDateTimeProvider(new DateTime(1984,4,4)))
                {
                    Assert.Equal(1984, DateTimeProvider.Current.Today.Year);    
                }

                Assert.Equal(2012, DateTimeProvider.Current.Today.Year);
            }

            // Fall-Back to Default DateTimeProvider 
            Assert.Equal<int>(expected.Year,  DateTimeProvider.Current.Today.Year);
        }

        private class MyDateTimeProvider : DateTimeProvider 
        {
            private readonly DateTime dateTime; 

            public MyDateTimeProvider(DateTime dateTime):base()
            {
                this.dateTime = dateTime; 
            }

            public override DateTime Today => this.dateTime.Date;

            public override DateTime Now => this.dateTime;
        }
    }
}

0

การใช้งานITimeProviderเราถูกบังคับให้นำไปใช้ในโครงการทั่วไปที่ใช้ร่วมกันแบบพิเศษซึ่งจะต้องอ้างอิงจากส่วนที่เหลือของโครงการอื่น ๆ แต่ตอนนี้มีความซับซ้อนการควบคุมของการอ้างอิง

เราค้นหาITimeProviderใน. NET Framework เราได้ค้นหาสำหรับแพคเกจ NuGet และพบหนึ่งDateTimeOffsetที่ไม่สามารถทำงานร่วมกับ

ดังนั้นเราจึงหาวิธีแก้ปัญหาของเราเองซึ่งขึ้นอยู่กับประเภทของห้องสมุดมาตรฐานเท่านั้น เรากำลังใช้อินสแตนซ์ของFunc<DateTimeOffset>เรากำลังใช้เป็นตัวอย่างของ

วิธีใช้

public class ThingThatNeedsTimeProvider
{
    private readonly Func<DateTimeOffset> now;
    private int nextId;

    public ThingThatNeedsTimeProvider(Func<DateTimeOffset> now)
    {
        this.now = now;
        this.nextId = 1;
    }

    public (int Id, DateTimeOffset CreatedAt) MakeIllustratingTuple()
    {
        return (nextId++, now());
    }
}

วิธีการลงทะเบียน

Autofac

builder.RegisterInstance<Func<DateTimeOffset>>(() => DateTimeOffset.Now);

( สำหรับบรรณาธิการในอนาคต: ต่อท้ายกรณีของคุณที่นี่ )

วิธีการทดสอบหน่วย

public void MakeIllustratingTuple_WhenCalled_FillsCreatedAt()
{
    DateTimeOffset expected = CreateRandomDateTimeOffset();
    DateTimeOffset StubNow() => expected;
    var thing = new ThingThatNeedsTimeProvider(StubNow);

    var (_, actual) = thing.MakeIllustratingTuple();

    Assert.AreEqual(expected, actual);
}

0

อาจเป็นมืออาชีพน้อยลง แต่การแก้ปัญหาที่ง่ายกว่าอาจทำให้พารามิเตอร์ DateTime ที่เมธอด consumer ตัวอย่างเช่นแทนที่จะสร้างเมธอดเช่น SampleMethod ทำให้ SampleMethod1 พร้อมพารามิเตอร์การทดสอบ SampleMethod1 ง่ายขึ้น

public void SampleMethod()
    {
        DateTime anotherDateTime = DateTime.Today.AddDays(-10);
        if ((DateTime.Now-anotherDateTime).TotalDays>10)
        {

        }
    }
    public void SampleMethod1(DateTime dateTimeNow)
    {
        DateTime anotherDateTime = DateTime.Today.AddDays(-10);
        if ((dateTimeNow - anotherDateTime).TotalDays > 10)
        {

        }

    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.