Cách tốt để ghi đè DateTime.Now trong quá trình thử nghiệm là gì?


116

Tôi có một số mã (C #) dựa vào ngày hôm nay để tính toán chính xác mọi thứ trong tương lai. Nếu tôi sử dụng ngày hôm nay trong bài kiểm tra, tôi phải lặp lại phép tính trong bài kiểm tra, điều này cảm thấy không ổn. Cách tốt nhất để đặt ngày thành một giá trị đã biết trong bài kiểm tra để tôi có thể kiểm tra xem kết quả có phải là giá trị đã biết không?

Câu trả lời:


157

Sở thích của tôi là có các lớp sử dụng thời gian thực sự dựa trên một giao diện, chẳng hạn như

interface IClock
{
    DateTime Now { get; } 
}

Với một triển khai cụ thể

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

Sau đó, nếu muốn, bạn có thể cung cấp bất kỳ loại đồng hồ nào khác mà bạn muốn để thử nghiệm, chẳng hạn như

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

Có thể có một số chi phí trong việc cung cấp đồng hồ cho lớp dựa vào nó, nhưng điều đó có thể được xử lý bằng bất kỳ giải pháp chèn phụ thuộc nào (sử dụng Inversion of Control container, phương thức khởi tạo / bộ định tuyến cũ đơn giản hoặc thậm chí là một Mẫu cổng tĩnh ).

Các cơ chế khác của việc cung cấp một đối tượng hoặc phương thức cung cấp thời gian mong muốn cũng hoạt động, nhưng tôi nghĩ điều quan trọng là tránh đặt lại đồng hồ hệ thống, vì điều đó sẽ chỉ giới thiệu nỗi đau ở các cấp độ khác.

Ngoài ra, việc sử dụng DateTime.Nowvà đưa nó vào các tính toán của bạn không chỉ là không ổn - nó còn cướp đi của bạn khả năng kiểm tra các thời điểm cụ thể, chẳng hạn như nếu bạn phát hiện ra một lỗi chỉ xảy ra gần ranh giới nửa đêm hoặc vào các ngày Thứ Ba. Sử dụng thời gian hiện tại sẽ không cho phép bạn kiểm tra các tình huống đó. Hoặc ít nhất là không phải bất cứ khi nào bạn muốn.


2
Chúng tôi thực sự đã chính thức hóa điều này trong một trong những phần mở rộng xUnit.net. Chúng tôi có một lớp Đồng hồ mà bạn sử dụng dưới dạng tĩnh thay vì DateTime và bạn có thể "đóng băng" và "tan băng" đồng hồ, bao gồm cả những ngày cụ thể. Xem is.gd/3xdsis.gd/3xdu
Brad Wilson

2
Cũng cần lưu ý rằng khi bạn muốn thay thế phương pháp đồng hồ hệ thống của mình - điều này sẽ xảy ra, ví dụ: khi sử dụng đồng hồ toàn cầu trong một doanh nghiệp có chi nhánh ở các múi giờ phân tán rộng rãi - phương pháp này mang lại cho bạn quyền tự do có giá trị ở cấp doanh nghiệp để thay đổi nghĩa của "Now".
Mike Burton

1
Cách này thực sự hiệu quả đối với tôi, cùng với việc sử dụng Khung tiêm phụ thuộc để truy cập phiên bản IClock.
Wilka

8
Câu trả lời chính xác. Chỉ muốn thêm rằng trong hầu hết các trường hợp UtcNownên được sử dụng và sau đó được điều chỉnh thích hợp theo mối quan tâm của mã, ví dụ: logic nghiệp vụ, giao diện người dùng, v.v. Thao tác DateTime trên các múi giờ là một bãi mìn nhưng bước đầu tiên tốt nhất là luôn bắt đầu với Giờ UTC.
Adam Ralph

@BradWilson Những liên kết đó hiện đã bị hỏng. Cũng không thể kéo chúng lên WayBack.

55

Ayende Rahien sử dụng một phương thức tĩnh khá đơn giản ...

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

1
Có vẻ nguy hiểm khi biến điểm sơ khai / mô phỏng thành một biến toàn cục công khai (biến tĩnh lớp). Sẽ tốt hơn nếu chỉ phạm vi hệ thống đang được thử nghiệm - ví dụ: đặt nó trở thành thành viên tĩnh riêng của lớp đang được thử nghiệm?
Aaron

1
Đó là một vấn đề của phong cách. Đây là điều ít nhất bạn có thể làm để có được thời gian hệ thống có thể thay đổi đơn vị kiểm tra.
Anthony Mastrean

3
IMHO, nó được ưu tiên sử dụng giao diện thay vì các đơn lẻ tĩnh toàn cầu. Hãy xem xét tình huống sau: Người chạy thử nghiệm hiệu quả và chạy song song nhiều thử nghiệm nhất có thể, một thử nghiệm chuyển sang thời gian nhất định thành X, thử nghiệm kia thành Y. Bây giờ chúng ta có xung đột và hai thử nghiệm này sẽ chuyển sang lỗi. Nếu chúng ta sử dụng các giao diện, mỗi bài kiểm tra sẽ mô phỏng giao diện theo nhu cầu của mình, mỗi bài kiểm tra giờ đây được cách ly với các bài kiểm tra khác. HTH.
ShloEmi

17

Tôi nghĩ rằng việc tạo một lớp đồng hồ riêng cho một thứ đơn giản như lấy ngày hiện tại là hơi quá mức cần thiết.

Bạn có thể chuyển ngày hôm nay làm tham số để có thể nhập một ngày khác trong bài kiểm tra. Điều này có thêm lợi ích là làm cho mã của bạn linh hoạt hơn.


Tôi đã +1 câu trả lời của cả bạn và Blair, mặc dù cả hai đều phản đối nhau. Tôi nghĩ rằng cả hai cách tiếp cận đều hợp lệ. Cách tiếp cận của bạn Tôi có thể sẽ sử dụng một dự án không sử dụng thứ gì đó như Unity.
RichardOD

1
Có, có thể thêm một tham số cho "bây giờ". Tuy nhiên, trong một số trường hợp, điều này yêu cầu bạn hiển thị một tham số mà bạn thường không muốn hiển thị. Ví dụ: giả sử bạn có một phương pháp tính toán số ngày giữa một ngày và bây giờ, thì bạn không muốn hiển thị "bây giờ" làm tham số vì điều này cho phép thao tác với kết quả của bạn. Nếu đó là mã của riêng bạn, hãy tiếp tục thêm "bây giờ" làm tham số, nhưng nếu bạn đang làm việc trong một nhóm, bạn không bao giờ biết các nhà phát triển khác sẽ sử dụng mã của bạn để làm gì, vì vậy nếu "bây giờ" là một phần quan trọng trong mã của bạn, bạn cần bảo vệ nó khỏi bị thao túng hoặc lạm dụng.
Stitch10925

17

Sử dụng Microsoft Fakes để tạo miếng đệm là một cách thực sự dễ dàng để thực hiện việc này. Giả sử tôi có lớp sau:

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

}

Trong Visual Studio 2012, bạn có thể thêm một lắp ráp Giả mạo vào dự án thử nghiệm của mình bằng cách nhấp chuột phải vào hợp ngữ bạn muốn tạo Giả mạo / Shims và chọn "Add Fakes Assembly"

Thêm hội giả

Cuối cùng, đây là lớp thử nghiệm sẽ trông như thế nào:

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
Đây chính xác là những gì tôi đang tìm kiếm. Cảm ơn! BTW, hoạt động tương tự trong VS 2013.
Douglas Ludlow

Hoặc những ngày này, phiên bản VS 2015 Enterprise. Đó là một điều đáng tiếc, cho một thực hành tốt nhất như vậy.
RJB

12

Chìa khóa để thử nghiệm đơn vị thành công là tách . Bạn phải tách mã thú vị của mình khỏi các phần phụ thuộc bên ngoài của nó, để nó có thể được kiểm tra một cách riêng biệt. (May mắn thay, Phát triển theo hướng thử nghiệm tạo ra mã tách rời.)

Trong trường hợp này, bên ngoài của bạn là DateTime hiện tại.

Lời khuyên của tôi ở đây là trích xuất logic giao dịch với DateTime sang một phương thức hoặc lớp mới hoặc bất kỳ điều gì có ý nghĩa trong trường hợp của bạn và chuyển DateTime vào. Bây giờ, bài kiểm tra đơn vị của bạn có thể vượt qua DateTime tùy ý, để tạo ra kết quả có thể dự đoán được.


10

Một cái khác sử dụng Microsoft Moles ( Isolation framework cho .NET ).

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

Các nốt ruồi cho phép thay thế bất kỳ phương thức .NET nào bằng một đại biểu. Nốt ruồi hỗ trợ các phương pháp tĩnh hoặc không ảo. Nốt ruồi phụ thuộc vào hồ sơ từ Pex.


Điều này thật đẹp, nhưng nó yêu cầu Visual Studio 2010! :-(
Pandincus

Nó cũng hoạt động tốt trong VS 2008. Nó hoạt động tốt nhất với MSTest. Bạn có thể sử dụng NUnit, nhưng sau đó tôi nghĩ bạn phải chạy thử nghiệm của mình với một trình chạy thử nghiệm đặc biệt.
Torbjørn

Tôi sẽ tránh sử dụng Moles (hay còn gọi là Microsoft Fakes) khi có thể. Tốt nhất, nó chỉ nên được sử dụng cho mã kế thừa chưa thể kiểm tra được thông qua chèn phụ thuộc.
brianpeiris

1
@brianpeiris, nhược điểm của việc sử dụng Microsoft Fakes là gì?
Ray Cheng

Bạn không phải lúc nào cũng DI nội dung của bên thứ ba, vì vậy Giả mạo hoàn toàn có thể chấp nhận được để kiểm tra đơn vị nếu mã của bạn gọi vào API của bên thứ ba mà bạn không muốn khởi tạo để kiểm tra đơn vị. Tôi đồng ý tránh sử dụng Giả mạo / Nốt ruồi cho mã của riêng bạn, nhưng nó hoàn toàn có thể chấp nhận được cho các mục đích khác.
ChrisCW



2

Bạn có thể đưa vào lớp (tốt hơn: phương thức / ủy nhiệm ) mà bạn sử dụng cho DateTime.Nowlớp đang được kiểm tra. CóDateTime.Now một giá trị mặc định và chỉ đặt nó trong thử nghiệm thành một phương thức giả trả về một giá trị không đổi.

CHỈNH SỬA: Blair Conrad đã nói gì (anh ấy có một số mã để xem xét). Ngoại trừ, tôi có xu hướng thích đại biểu cho điều này hơn, vì chúng không làm lộn xộn thứ bậc lớp của bạn với những thứ như IClock...


1

Tôi đã đối mặt với tình huống này thường xuyên, đến mức tôi đã tạo một nuget đơn giản để hiển thị thuộc tính Hiện hành thông qua giao diện.

public interface IDateTimeTools
{
    DateTime Now { get; }
}

Việc triển khai tất nhiên là rất đơn giản

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

Vì vậy, sau khi thêm nuget vào dự án của mình, tôi có thể sử dụng nó trong các bài kiểm tra đơn vị

nhập mô tả hình ảnh ở đây

Bạn có thể cài đặt mô-đun ngay từ Trình quản lý gói GUI Nuget hoặc bằng cách sử dụng lệnh:

Install-Package -Id DateTimePT -ProjectName Project

Và mã cho Nuget ở đây .

Có thể tìm thấy ví dụ về cách sử dụng Autofac tại đây .


-11

Bạn đã xem xét sử dụng biên dịch có điều kiện để kiểm soát những gì xảy ra trong quá trình gỡ lỗi / triển khai chưa?

ví dụ

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

Không thành công, bạn muốn để lộ tài sản để bạn có thể thao túng nó, tất cả đây là một phần của thử thách viết mã có thể kiểm tra , đó là điều mà tôi hiện đang tự đấu tranh: D

Biên tập

Phần lớn tôi thích cách tiếp cận của Blair hơn . Điều này cho phép bạn "cắm nóng" các phần của mã để hỗ trợ thử nghiệm. Tất cả tuân theo nguyên tắc thiết kế gói gọn những gì khác nhau mã kiểm tra không khác gì mã sản xuất, chỉ là không ai nhìn thấy nó bên ngoài.

Tuy nhiên, việc tạo và giao diện có vẻ như rất nhiều công việc đối với ví dụ này (đó là lý do tại sao tôi chọn biên dịch có điều kiện).


wow bạn đã bị đánh mạnh vào câu trả lời này. đây là những gì tôi đã làm, mặc dù ở một nơi duy nhất, nơi tôi gọi là phương pháp mà thay thế DateTime's NowTodayvv
Dave Cousineau
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.