Модульное тестирование: DateTime.Now - PullRequest
137 голосов
/ 11 марта 2010

У меня есть некоторые модульные тесты, которые ожидают, что «текущее время» будет отличаться от DateTime.Now, и я, очевидно, не хочу изменять время компьютера.

Какова лучшая стратегия для достижения этой цели?

Ответы [ 20 ]

2 голосов
/ 21 декабря 2015

Я удивлен, что никто не предложил один из самых очевидных путей:

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

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

Тогда вы можете просто переопределить этот метод в вашем двойном тесте.

Мне также нравится вводить класс TimeProvider в некоторых случаях, но для других этого более чем достаточно. Я, вероятно, предпочел бы версию TimeProvider, если вам нужно использовать ее в нескольких классах.

РЕДАКТИРОВАТЬ: Для всех, кто заинтересован, это называется добавлением «шва» в ваш класс, точки, в которой вы можете подключиться к его поведению, чтобы изменить его (для целей тестирования или иным образом) без необходимости изменять код в класс.

2 голосов
/ 11 марта 2010

Макет объектов.

фиктивный DateTime, который возвращает теперь подходящий для вашего теста

1 голос
/ 28 октября 2016

Хорошая практика, когда 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

0 голосов
/ 30 августа 2018

Мы использовали статический объект SystemTime, но столкнулись с проблемами при выполнении параллельных модульных тестов. Я попытался использовать решение Хенка Ван Бойджена, но у меня возникли проблемы в порожденных асинхронных потоках, и в итоге я использовал 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

0 голосов
/ 20 марта 2018

Один из простых способов сделать это - ввести 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);
}
0 голосов
/ 08 октября 2017

Возможно, менее профессиональное, но более простое решение может заключаться в создании параметра DateTime в потребительском методе. Например, вместо метода make, такого как 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)
        {

        }

    }
0 голосов
/ 29 июня 2017

Используя 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 голосов
/ 19 апреля 2017

Вот мой ответ на этот вопрос. Я комбинирую шаблон «Ambient Context» с IDisposable. Таким образом, вы можете использовать DateTimeProvider.Current в коде вашей обычной программы, а в тесте вы переопределяете область с помощью оператора using.

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 голосов
/ 04 мая 2016

Альтернативный вариант, который не упоминается, - ввести текущее время в зависимый метод:

public class DateTimeNowDependencyClass
{
    ...

    public void ImplicitTimeDependencyMethod(Obj arg0)
    {
        this.TimeDependencyMethod(DateTime.Now, arg0);
    }

    internal void TimeDependencyMethod(DateTime now, Obj arg1)
    {
        ...
    }

    ...
}

Внутренний вариант открыт для модульного тестирования, параллельный или нет.

Это основано на том принципе, что ImplicitTimeDependencyMethod "слишком просто сломать" (см .: http://junit.sourceforge.net/doc/faq/faq.htm#best_3), поэтому его не нужно включать в покрытие модульных тестов. Хотя это должно быть затронуто в интеграционных тестах в любом случае .

В зависимости от цели класса, в любом случае может быть желательно, чтобы оба этих метода были общедоступными.

0 голосов
/ 02 июля 2015

У меня та же проблема, но я думал, что мы не должны использовать установленные даты и времени в одном классе. потому что это может привести к неправильному использованию в один прекрасный день. поэтому я использовал провайдера, как

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

...