Apa cara yang baik untuk menimpa DateTime.Now selama pengujian?

116

Saya punya beberapa kode (C #) yang bergantung pada tanggal hari ini untuk menghitung dengan benar berbagai hal di masa depan. Jika saya menggunakan tanggal hari ini dalam pengujian, saya harus mengulangi perhitungan dalam pengujian, yang rasanya tidak benar. Apa cara terbaik untuk menyetel tanggal ke nilai yang diketahui dalam pengujian sehingga saya dapat menguji bahwa hasilnya adalah nilai yang diketahui?

Craig.Nicol
sumber

Jawaban:

157

Preferensi saya adalah memiliki kelas yang menggunakan waktu sebenarnya bergantung pada antarmuka, seperti

interface IClock
{
    DateTime Now { get; } 
}

Dengan implementasi yang konkrit

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

Kemudian jika mau, Anda dapat menyediakan jenis jam lain yang Anda inginkan untuk pengujian, seperti

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

Mungkin ada beberapa overhead dalam menyediakan jam ke kelas yang bergantung padanya, tetapi itu dapat ditangani oleh sejumlah solusi injeksi ketergantungan (menggunakan wadah Pembalikan Kontrol, injeksi konstruktor / penyetel lama biasa, atau bahkan Pola Gerbang Statis ).

Mekanisme lain untuk mengirimkan objek atau metode yang menyediakan waktu yang diinginkan juga berfungsi, tetapi menurut saya kuncinya adalah menghindari menyetel ulang jam sistem, karena itu hanya akan menimbulkan rasa sakit di level lain.

Selain itu, menggunakan DateTime.Nowdan memasukkannya ke dalam kalkulasi Anda tidak hanya terasa tidak tepat - ini merampas kemampuan Anda untuk menguji waktu tertentu, misalnya jika Anda menemukan bug yang hanya terjadi di dekat batas tengah malam, atau pada hari Selasa. Menggunakan waktu saat ini tidak memungkinkan Anda untuk menguji skenario tersebut. Atau setidaknya tidak kapan pun Anda mau.

Blair Conrad
sumber
2
Kami benar-benar memformalkannya di salah satu ekstensi xUnit.net. Kami memiliki kelas Jam yang Anda gunakan sebagai statis daripada DateTime, dan Anda dapat "membekukan" dan "mencairkan" jam, termasuk ke tanggal tertentu. Lihat is.gd/3xds dan is.gd/3xdu
Brad Wilson
2
Perlu juga dicatat bahwa ketika Anda ingin mengganti metode jam sistem - ini akan terjadi, misalnya, saat menggunakan jam global di perusahaan dengan cabang di zona waktu yang tersebar luas - metode ini memberi Anda kebebasan tingkat bisnis yang berharga untuk mengubah arti dari "Sekarang".
Mike Burton
1
Cara ini bekerja sangat baik untuk saya, bersama dengan menggunakan Kerangka Injeksi Ketergantungan untuk sampai ke contoh IClock.
Wilka
8
Jawaban yang bagus. Hanya ingin menambahkan bahwa di hampir semua kasus UtcNowharus digunakan dan kemudian disesuaikan secara tepat sesuai dengan perhatian kode, misalnya logika bisnis, UI, dll. Manipulasi DateTime melintasi zona waktu adalah ladang ranjau tetapi langkah pertama yang terbaik ke depan adalah selalu memulai dengan Waktu UTC.
Adam Ralph
@BradWilson Tautan tersebut sekarang rusak. Juga tidak bisa menarik mereka ke WayBack.
55

Ayende Rahien menggunakan metode statis yang cukup sederhana ...

public static class SystemTime
{
    public static Func<DateTime> Now = () => DateTime.Now;
}
Anthony Mastrean
sumber
1
Tampaknya berbahaya untuk menjadikan titik rintisan / tiruan sebagai variabel global publik (variabel statis kelas). Tidakkah lebih baik untuk menskalakannya hanya ke sistem yang diuji - misalnya, menjadikannya anggota statis pribadi dari kelas yang diuji?
Aaron
1
Ini masalah gaya. Ini adalah hal terkecil yang dapat Anda lakukan untuk mendapatkan waktu sistem unit-test-changeable.
Anthony Mastrean
3
IMHO, lebih disukai menggunakan antarmuka daripada single statis global. Pertimbangkan skenario berikut: Runner pengujian efisien dan menjalankan pengujian sebanyak yang dia bisa secara paralel, satu pengujian berubah ke waktu tertentu ke X, yang lain ke Y. Kami sekarang mengalami konflik dan kedua pengujian ini akan mengalihkan kegagalan. Jika kita menggunakan antarmuka, setiap pengujian mengolok-olok antarmuka sesuai dengan kebutuhannya, setiap pengujian sekarang diisolasi dari pengujian lainnya. HTH.
ShloEmi
17

Saya pikir membuat kelas jam terpisah untuk sesuatu yang sederhana seperti mendapatkan tanggal saat ini agak berlebihan.

Anda dapat melewatkan tanggal hari ini sebagai parameter sehingga Anda dapat memasukkan tanggal yang berbeda dalam pengujian. Ini memiliki keuntungan tambahan yaitu membuat kode Anda lebih fleksibel.

Mendelt
sumber
Saya memberi +1 pada jawaban Anda dan Blair, meskipun keduanya berlawanan. Saya pikir kedua pendekatan itu valid. Pendekatan Anda, saya mungkin akan menggunakan proyek yang tidak menggunakan sesuatu seperti Unity.
RichardOD
1
Ya, Anda dapat menambahkan parameter untuk "sekarang". Namun, dalam beberapa kasus hal ini mengharuskan Anda untuk mengekspos parameter yang biasanya tidak ingin Anda tampilkan. Misalkan, misalnya, Anda memiliki metode yang menghitung hari antara tanggal dan sekarang, maka Anda tidak ingin menampilkan "sekarang" sebagai parameter karena ini memungkinkan manipulasi hasil Anda. Jika itu adalah kode Anda sendiri, lanjutkan menambahkan "sekarang" sebagai parameter, tetapi jika Anda bekerja dalam tim, Anda tidak pernah tahu untuk apa pengembang lain akan menggunakan kode Anda, jadi jika "sekarang" adalah bagian penting dari kode Anda, Anda perlu melindunginya dari manipulasi atau penyalahgunaan.
Stitch10925
17

Menggunakan Microsoft Fakes untuk membuat shim adalah cara yang sangat mudah untuk melakukan ini. Misalkan saya memiliki kelas berikut:

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

}

Dalam Visual Studio 2012 Anda dapat menambahkan rakitan Fakes ke proyek pengujian Anda dengan mengklik kanan rakitan yang ingin Anda buat Fakes / Shims dan memilih "Add Fakes Assembly"

Menambahkan Perakitan Palsu

Terakhir, kelas pengujian akan terlihat seperti ini:

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.dll
sumber
1
Inilah yang saya cari. Terima kasih! BTW, bekerja sama di VS 2013.
Douglas Ludlow
Atau hari ini, edisi VS 2015 Enterprise. Yang memalukan, untuk praktik terbaik seperti itu.
RJB
12

Kunci sukses unit testing adalah decoupling . Anda harus memisahkan kode yang menarik dari dependensi eksternalnya, sehingga dapat diuji secara terpisah. (Untungnya, Test-Driven Development menghasilkan kode terpisah.)

Dalam hal ini, eksternal Anda adalah DateTime saat ini.

Saran saya di sini adalah mengekstrak logika yang berhubungan dengan DateTime ke metode atau kelas baru atau apa pun yang masuk akal dalam kasus Anda, dan meneruskan DateTime masuk Sekarang, pengujian unit Anda dapat meneruskan DateTime sewenang-wenang, untuk menghasilkan hasil yang dapat diprediksi.

Jay Bazuzi
sumber
10

Yang lain menggunakan Microsoft Moles ( kerangka Isolasi untuk .NET ).

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

Moles memungkinkan untuk mengganti metode .NET apa pun dengan delegasi. Moles mendukung metode statis atau non-virtual. Tahi lalat bergantung pada profiler dari Pex.

João Angelo
sumber
Ini indah, tetapi membutuhkan Visual Studio 2010! :-(
Pandincus
Ia bekerja dengan baik di VS 2008 juga. Ini bekerja paling baik dengan MSTest. Anda dapat menggunakan NUnit, tetapi menurut saya Anda harus menjalankan pengujian dengan runner pengujian khusus.
Torbjørn
Saya akan menghindari penggunaan Moles (alias Microsoft Fakes) bila memungkinkan. Idealnya, ini hanya boleh digunakan untuk kode lama yang belum dapat diuji melalui injeksi ketergantungan.
brianpeiris
1
@ Brianpeiris, apa kerugian menggunakan Microsoft Fakes?
Ray Cheng
Anda tidak bisa selalu melakukan konten pihak ke-3, jadi Pemalsuan dapat diterima untuk pengujian unit jika kode Anda memanggil API pihak ketiga yang tidak ingin Anda buat untuk pengujian unit. Saya setuju untuk menghindari penggunaan Fakes / Moles untuk kode Anda sendiri, tetapi dapat diterima untuk tujuan lain.
ChrisCW
5

Saya sarankan menggunakan pola 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);
    } 
}

Secara rinci dijelaskan di sini: http://www.lesnikowski.com/blog/index.php/testing-datetime-now/

Pawel Lesnikowski
sumber
2

Anda dapat memasukkan kelas (lebih baik: metode / delegasi ) yang Anda gunakan untuk DateTime.Nowdi kelas yang diuji. Telah DateTime.Nowmenjadi nilai default dan hanya mengaturnya dalam pengujian ke metode dummy yang mengembalikan nilai konstan.

EDIT: Apa yang dikatakan Blair Conrad (dia memiliki beberapa kode untuk dilihat). Kecuali, saya cenderung lebih suka delegasi untuk ini, karena mereka tidak mengacaukan hierarki kelas Anda dengan hal-hal seperti IClock...

Daren Thomas
sumber
1

Saya sering menghadapi situasi ini, sehingga saya membuat nuget sederhana yang mengekspos properti Now melalui antarmuka.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Penerapannya tentu saja sangat mudah

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

Jadi setelah menambahkan nuget ke proyek saya, saya dapat menggunakannya dalam pengujian unit

masukkan deskripsi gambar di sini

Anda dapat menginstal modul langsung dari GUI Nuget Package Manager atau dengan menggunakan perintah:

Install-Package -Id DateTimePT -ProjectName Project

Dan kode untuk Nuget ada di sini .

Contoh penggunaan dengan Autofac dapat ditemukan di sini .

Pawel Wujczyk
sumber
-11

Pernahkah Anda mempertimbangkan untuk menggunakan kompilasi bersyarat untuk mengontrol apa yang terjadi selama debug / penerapan?

misalnya

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

Jika gagal, Anda ingin mengekspos properti agar dapat memanipulasinya, ini semua adalah bagian dari tantangan menulis kode yang dapat diuji , yang saat ini sedang saya geluti sendiri: D

Edit

Sebagian besar dari diri saya lebih menyukai pendekatan Blair . Ini memungkinkan Anda untuk "menyambungkan" bagian kode untuk membantu pengujian. Itu semua mengikuti prinsip desain merangkum apa yang bervariasi kode uji tidak berbeda dengan kode produksi, hanya saja tidak ada yang pernah melihatnya secara eksternal.

Membuat dan antarmuka mungkin tampak seperti banyak pekerjaan untuk contoh ini meskipun (itulah sebabnya saya memilih kompilasi bersyarat).

Rob Cooper
sumber
wow Anda terpukul keras pada jawaban ini. inilah yang telah saya lakukan, meskipun di satu tempat, saya menyebut metode yang menggantikan DateTime's Nowdan Todaylain
Dave Cousineau