Какой хороший способ перезаписать DateTime.Now во время тестирования?

116

У меня есть код (C #), который полагается на сегодняшнюю дату, чтобы правильно рассчитывать вещи в будущем. Если я использую сегодняшнюю дату в тестировании, мне придется повторить расчет в тесте, что кажется неправильным. Как лучше всего установить дату на известное значение в тесте, чтобы я мог проверить, является ли результат известным значением?

Craig.Nicol
источник

Ответы:

157

Я предпочитаю, чтобы классы, использующие время, фактически полагались на интерфейс, например

interface IClock
{
    DateTime Now { get; } 
}

С конкретной реализацией

class SystemClock: IClock
{
     DateTime Now { get { return DateTime.Now; } }
}

Затем, если вы хотите, вы можете предоставить любые другие часы для тестирования, например

class StaticClock: IClock
{
     DateTime Now { get { return new DateTime(2008, 09, 3, 9, 6, 13); } }
}

Могут возникнуть некоторые накладные расходы при предоставлении часов классу, который на них полагается, но это может быть обработано любым количеством решений для внедрения зависимостей (с использованием контейнера Inversion of Control, простого старого конструктора / установщика или даже шаблона статического шлюза). ).

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

Кроме того, использование DateTime.Nowи включение его в свои вычисления не просто кажется неправильным - это лишает вас возможности тестировать определенное время, например, если вы обнаруживаете ошибку, которая возникает только около полуночи или по вторникам. Использование текущего времени не позволит вам протестировать эти сценарии. Или, по крайней мере, не когда захотите.

Блэр Конрад
источник
2
Фактически мы формализовали это в одном из расширений xUnit.net. У нас есть класс Clock, который вы используете как статический, а не DateTime, и вы можете «заморозить» и «разморозить» часы, в том числе до определенных дат. См. Is.gd/3xds и is.gd/3xdu
Брэд Уилсон
2
Также стоит отметить, что когда вы хотите заменить метод системных часов - это произойдет, например, при использовании глобальных часов на предприятии с филиалами в широко разбросанных часовых поясах - этот метод дает вам ценную свободу на бизнес-уровне для изменения значение «сейчас».
Майк Бертон,
1
Этот способ очень хорошо работает для меня, наряду с использованием инфраструктуры внедрения зависимостей для доступа к экземпляру IClock.
Wilka
8
Отличный ответ. Просто хотел добавить, что почти во всех случаях UtcNowследует использовать, а затем соответствующим образом настраивать в соответствии с проблемами кода, например, бизнес-логикой, пользовательским интерфейсом и т. Д. Манипуляции с DateTime в часовых поясах - это минное поле, но лучший первый шаг вперед - всегда начинать с Время UTC.
Адам Ральф
@BradWilson Эти ссылки теперь не работают. Тоже не смог поднять их на обратном пути.
55

Айенде Рахиен использует довольно простой статический метод ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Энтони Мастрян
источник
1
Кажется опасным делать заглушку / фиктивную точку общедоступной глобальной переменной (статической переменной класса). Разве не было бы лучше применить его только к тестируемой системе - например, сделать его частным статическим членом тестируемого класса?
Аарон
1
Это вопрос стиля. Это наименьшее, что вы можете сделать, чтобы получить системное время, изменяемое модульным тестом.
Энтони Мастрян
3
IMHO, предпочтительнее использовать интерфейс вместо глобальных статических синглтонов. Рассмотрим следующий сценарий: исполнитель тестов эффективен и выполняет столько тестов, сколько он может, параллельно, один тест изменяет заданное время на X, другой - на Y. Теперь у нас есть конфликт, и эти два теста будут переключать сбои. Если мы используем интерфейсы, каждый тест имитирует интерфейс в соответствии с его потребностями, каждый тест теперь изолирован от другого теста. НТН.
ShloEmi
17

Я думаю, что создание отдельного класса часов для чего-то простого, например, получения текущей даты, немного излишне.

Вы можете передать сегодняшнюю дату в качестве параметра, чтобы вы могли ввести другую дату в тесте. Это дает дополнительное преимущество, делая ваш код более гибким.

Mendelt
источник
Я поставил +1 как на ваш, так и на ответ Блэра, хотя они оба возражают. Я думаю, что оба подхода верны. Ваш подход Я бы, наверное, использовал проект, который не использует что-то вроде Unity.
RichardOD
1
Да, можно добавить параметр «сейчас». Однако в некоторых случаях это требует, чтобы вы предоставили параметр, который вы обычно не хотите раскрывать. Скажем, например, что у вас есть метод, который вычисляет количество дней между датой и настоящим моментом, тогда вы не хотите указывать «сейчас» в качестве параметра, потому что это позволяет манипулировать вашим результатом. Если это ваш собственный код, добавьте «сейчас» в качестве параметра, но если вы работаете в команде, вы никогда не знаете, для чего другие разработчики будут использовать ваш код, поэтому, если «сейчас» является важной частью вашего кода, вам необходимо защитить его от манипуляций или неправильного использования.
Stitch10925 06
17

Использование Microsoft Fakes для создания прокладки - действительно простой способ сделать это. Предположим, у меня был следующий класс:

public class MyClass
{
    public string WhatsTheTime()
    {
        return DateTime.Now.ToString();
    }

}

В Visual Studio 2012 вы можете добавить сборку Fakes в свой тестовый проект, щелкнув правой кнопкой мыши сборку, для которой хотите создать Fakes / Shims, и выбрав «Добавить сборку Fakes».

Добавление поддельной сборки

Наконец, вот как будет выглядеть тестовый класс:

using System;
using ConsoleApplication11;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DateTimeTest
{
[TestClass]
public class UnitTest1
{
    [TestMethod]
    public void TestWhatsTheTime()
    {

        using(ShimsContext.Create()){

            //Arrange
            System.Fakes.ShimDateTime.NowGet =
            () =>
            { return new DateTime(2010, 1, 1); };

            var myClass = new MyClass();

            //Act
            var timeString = myClass.WhatsTheTime();

            //Assert
            Assert.AreEqual("1/1/2010 12:00:00 AM",timeString);

        }
    }
}
}
mmilleruva
источник
1
Это было именно то, что я искал. Спасибо! Кстати, то же самое работает и в VS 2013.
Дуглас Ладлоу
Или в наши дни VS 2015 Enterprise edition. Какая жалость за такую ​​передовую практику.
RJB
12

Ключ к успешному модульному тестированию - разделение . Вы должны отделить интересующий вас код от его внешних зависимостей, чтобы его можно было протестировать изолированно. (К счастью, разработка через тестирование создает несвязанный код.)

В этом случае вашим внешним является текущее DateTime.

Мой совет здесь - извлечь логику, которая имеет дело с DateTime, в новый метод или класс или что-то еще, что имеет смысл в вашем случае, и передать DateTime внутрь. Теперь ваш модульный тест может передавать произвольный DateTime, чтобы получить предсказуемые результаты.

Джей Базузи
источник
10

Еще один, использующий Microsoft Moles ( каркас изоляции для .NET ).

MDateTime.NowGet = () => new DateTime(2000, 1, 1);

Moles позволяет заменить любой .NET-метод делегатом. Moles поддерживает статические и не виртуальные методы. Moles полагается на профилировщик от Pex.

Жоао Анджело
источник
Это красиво, но для этого требуется Visual Studio 2010! :-(
Pandincus
Он отлично работает и в VS 2008. Однако лучше всего он работает с MSTest. Вы можете использовать NUnit, но тогда, я думаю, вам нужно запускать тесты с помощью специального средства запуска тестов.
Торбьёрн
Я бы по возможности избегал использования Moles (он же Microsoft Fakes). В идеале его следует использовать только для устаревшего кода, который еще не тестируется с помощью внедрения зависимостей.
brianpeiris
1
@brianpeiris, каковы недостатки использования Microsoft Fakes?
Рэй Ченг
Вы не всегда можете DI стороннего контента, поэтому Fakes вполне приемлемы для модульного тестирования, если ваш код вызывает сторонний API, который вы не хотите создавать для модульного теста. Я согласен избегать использования Fakes / Moles для вашего собственного кода, но это вполне приемлемо для других целей.
ChrisCW,
5

Я бы предложил использовать шаблон IDisposable:

[Test] 
public void CreateName_AddsCurrentTimeAtEnd() 
{
    using (Clock.NowIs(new DateTime(2010, 12, 31, 23, 59, 00)))
    {
        string name = new ReportNameService().CreateName(...);
        Assert.AreEqual("name 2010-12-31 23:59:00", name);
    } 
}

Подробно описано здесь: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Павел Лесниковский
источник
2

Вы можете ввести класс (лучше: метод / делегат ), который вы используете, DateTime.Nowв тестируемом классе. Должно DateTime.Nowбыть значением по умолчанию и устанавливать его только при тестировании на фиктивный метод, который возвращает постоянное значение.

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

Дарен Томас
источник
1

Я так часто сталкивался с этой ситуацией, что создал простой nuget, который предоставляет свойство Now через интерфейс.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Реализация, конечно, очень проста

public class DateTimeTools : IDateTimeTools
{
    public DateTime Now => DateTime.Now;
}

Поэтому после добавления nuget в свой проект я могу использовать его в модульных тестах

введите описание изображения здесь

Вы можете установить модуль прямо из диспетчера пакетов Nuget с графическим интерфейсом пользователя или с помощью команды:

Install-Package -Id DateTimePT -ProjectName Project

И код для Nuget здесь .

Пример использования с Autofac можно найти здесь .

Павел Вуйчик
источник
-11

Рассматривали ли вы использование условной компиляции для управления тем, что происходит во время отладки / развертывания?

например

DateTime date;
#if DEBUG
  date = new DateTime(2008, 09, 04);
#else
  date = DateTime.Now;
#endif

В противном случае вы хотите раскрыть свойство, чтобы вы могли манипулировать им, это все часть проблемы написания тестируемого кода, с чем я сейчас борюсь: D

редактировать

Большая часть меня предпочла бы подход Блэра . Это позволяет выполнять «горячую замену» частей кода для облегчения тестирования. Все это следует принципу дизайна: инкапсулируйте то, что меняется, тестовый код не отличается от производственного кода, просто никто никогда не видит его извне.

Хотя создание и интерфейс может показаться большим трудом для этого примера (вот почему я выбрал условную компиляцию).

Роб Купер
источник
вау, ты сильно ударил по этому ответу. это то , что я делал, хотя в одном месте, где я называю методы, заменяющие DateTime«S Nowи Todayт.д.
Dave Кузино