Який хороший спосіб перезаписати DateTime.Зараз під час тестування?


116

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

Відповіді:


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

Можливо, у наданні годинника класу, який на нього покладається, можуть бути накладні витрати, але це може вирішуватися будь-якою кількістю рішень для введення залежностей (використовуючи контейнер Інверсія управління, звичайний старий конструктор / інжектор-інжектор або навіть статичну схему шлюзу ).

Інші механізми доставки об'єкта чи методу, які забезпечують бажаний час, також працюють, але я думаю, що головне - уникнути перезавантаження системного годинника, оскільки це просто призведе до болю на інших рівнях.

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


2
Ми фактично формалізували це в одному з розширень xUnit.net. У нас є клас Clock, який ви використовуєте як статичний, а не DateTime, і ви можете "заморозити" та "відтанути" годинник, включаючи конкретні дати. Дивіться is.gd/3xds та is.gd/3xdu
Бред Уїлсон

2
Варто також зазначити, що коли ви хочете замінити свій системний годинниковий метод - це станеться, наприклад, при використанні глобального годинника на підприємстві з філіями в широко розсіяних часових поясах - цей метод дає вам цінну свободу бізнес-рівня змінювати значення "Зараз".
Майк Бертон

1
Цей спосіб справді добре працює для мене, разом із використанням рамок введення залежності, щоб дістатися до екземпляра IClock.
Вілька

8
Чудова відповідь. Просто хотів би додати, що майже у всіх випадках UtcNowслід використовувати та потім правильно підлаштовувати їх відповідно до коду, наприклад, ділової логіки, інтерфейсу користувача та ін. UTC.
Адам Ральф

@BradWilson Ці посилання тепер розірвані. Не вдалося також підняти їх на WayBack.

55

Ейенде Рахієн використовує статичний метод, який досить простий ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}

1
Здається небезпечним зробити пункт заглушки / макету публічною глобальною змінною (статична змінна класу). Чи не було б краще застосувати його лише до тестуваної системи - наприклад, зробити її приватним статичним членом тестуваного класу?
Аарон

1
Це питання стилю. Це найменше, що ви можете зробити, щоб отримати системний час на зміну одиниці тесту.
Ентоні Мастрен

3
IMHO, краще використовувати інтерфейс замість глобальних статичних синглів. Розглянемо наступний сценарій: Тестовий бігун ефективний і виконує стільки тестів, скільки він може паралельно; один тест змінюється на заданий час на X, а інший на Y. Зараз у нас конфлікт, і ці два тести змінять помилки. Якщо ми використовуємо інтерфейси, кожен тест знущається над інтерфейсом відповідно до його потреб, кожен тест тепер ізольований від іншого тесту. HTH.
ShloEmi

17

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

Ви можете передати сьогоднішню дату як параметр, щоб ви могли ввести іншу дату в тесті. Це має додаткову перевагу - зробити ваш код більш гнучким.


Я поставив оцінку +1 і вашим, і Блеровим відповідям, хоча вони обидва проти. Я думаю, що обидва підходи справедливі. Ваш підхід я, мабуть, використовував проект, який не використовує щось на кшталт Unity.
РічардОД

1
Так, можна додати параметр "зараз". Однак у деяких випадках для цього потрібно викрити параметр, який, як правило, не хотіли б викрити. Скажімо, наприклад, що у вас є метод, який обчислює дні між датою і тепер, то ви не хочете виставляти "зараз" як параметр, оскільки це дозволяє маніпулювати вашим результатом. Якщо це ваш власний код, вперед додайте параметр "зараз", але якщо ви працюєте в команді, ви ніколи не знаєте, для чого інші розробники будуть використовувати ваш код, тому, якщо "зараз" є важливою частиною коду, вам потрібно захистити його від маніпуляцій чи зловживання.
Stitch10925

17

Використання Microsoft Fakes для створення шейму - це дійсно простий спосіб зробити це. Припустимо, у мене був такий клас:

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

}

У Visual Studio 2012 ви можете додати збірку Fakes до свого тестового проекту, клацнувши правою кнопкою миші на збірці, для якої потрібно створити Fakes / Shims, та вибравши "Додати збірку факсів"

Додавання фактів збірки

Нарешті, ось як виглядатиме тестовий клас:

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

        }
    }
}
}

1
Це саме те, що я шукав. Дякую! BTW, працює так само у VS 2013.
Douglas Ludlow

Або в ці дні, видання VS 2015 Enterprise. Що шкода, за таку найкращу практику.
RJB

12

Запорукою успішного тестування блоку є роз'єднання . Ви повинні відокремити цікавий код від його зовнішніх залежностей, щоб його можна було перевірити окремо. (На щастя, тестова розробка розробляє розв'язаний код.)

У цьому випадку вашим зовнішнім є поточний DateTime.

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


10

Ще один, що використовує Microsoft Moles ( Isolation Framework for .NET ).

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

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


Це прекрасно, але для цього потрібна Visual Studio 2010! :-(
Пандінкус

Він чудово працює і в VS 2008. Однак він найкраще працює з MSTest. Ви можете використовувати NUnit, але тоді я думаю, що вам доведеться запускати свої тести зі спеціальним тестовим бігуном.
Torbjørn

Я б уникав використання Moles (він же Microsoft Fakes), коли це можливо. В ідеалі його слід використовувати лише для застарілого коду, який ще не перевіряється за допомогою введення залежності.
brianpeiris

1
@brianpeiris, які недоліки у використанні Microsoft Fakes?
Рей Чен

Ви не завжди можете вміст сторонніх учасників DI, тому Fakes цілком прийнятний для тестування одиниць, якщо ваш код викликає сторонній API, який ви не хочете створювати для тестування одиниці. Я погоджуюся уникати використання факсів / молей для власного коду, але цілком прийнятний для інших цілей.
ChrisCW

5

Я б запропонував використовувати шаблон, що не використовується:

[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

Я стикався з такою ситуацією так часто, що я створив простий нут, який розкриває властивість Now через інтерфейс.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Здійснення, звичайно, дуже просте

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

Тож після додавання нугету в мій проект я можу використовувати його в одиничних тестах

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

Ви можете встановити модуль прямо з GUI Nuget Package Manager або за допомогою команди:

Install-Package -Id DateTimePT -ProjectName Project

І код Nuget є тут .

Приклад використання з автофакелом можна знайти тут .


-11

Чи планували ви використовувати умовну компіляцію для контролю того, що відбувається під час налагодження / розгортання?

напр

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

Якщо цього не потрібно, ви хочете викрити властивість, щоб ви могли маніпулювати нею, це все є складною проблемою написання тестового коду, який я зараз сам борю: D

Редагувати

Значна частина мене віддала перевагу підходу Блера . Це дозволяє "гарячої штепсельної" частини коду, щоб допомогти у тестуванні. Все це дотримується принципу дизайну, який включає в себе те, що змінюється тестовий код, не відрізняється від виробничого коду, його просто ніхто ніколи не бачить.

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


вау, ви сильно вразили цю відповідь. це те, що я роблю, хоча в одному місці, де я називаю методи, які замінюють DateTime' Nowі Todayт. д.
Дейв Кузен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.