วิธีที่ดีในการเขียนทับ 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); } }
}

อาจมีค่าใช้จ่ายบางอย่างในการจัดหานาฬิกาให้กับคลาสที่อาศัยอยู่ แต่สามารถจัดการได้โดยใช้โซลูชันการฉีดแบบพึ่งพาจำนวนเท่าใดก็ได้ (โดยใช้คอนเทนเนอร์ Inversion of Control, คอนสตรัคเตอร์ / เซ็ตเตอร์แบบเก่าธรรมดาหรือแม้กระทั่งรูปแบบเกตเวย์แบบคงที่ )

กลไกอื่น ๆ ในการส่งมอบวัตถุหรือวิธีการที่ให้เวลาที่ต้องการก็ใช้ได้เช่นกัน แต่ฉันคิดว่าสิ่งสำคัญคือการหลีกเลี่ยงการรีเซ็ตนาฬิการะบบเนื่องจากจะทำให้เกิดความเจ็บปวดในระดับอื่น ๆ

นอกจากนี้การใช้DateTime.Nowและการรวมไว้ในการคำนวณของคุณไม่เพียง แต่รู้สึกไม่ถูกต้องเท่านั้น แต่ยังช่วยให้คุณสามารถทดสอบช่วงเวลาที่เฉพาะเจาะจงได้เช่นหากคุณพบข้อบกพร่องที่เกิดขึ้นใกล้เขตแดนเที่ยงคืนเท่านั้นหรือในวันอังคาร การใช้เวลาปัจจุบันจะไม่อนุญาตให้คุณทดสอบสถานการณ์เหล่านั้น หรืออย่างน้อยก็ไม่เมื่อใดก็ตามที่คุณต้องการ


2
จริงๆแล้วเราทำให้สิ่งนี้เป็นทางการในหนึ่งในส่วนขยาย xUnit.net เรามีคลาสนาฬิกาที่คุณใช้เป็นแบบคงที่แทนที่จะเป็น DateTime และคุณสามารถ "หยุด" และ "ละลาย" นาฬิกาได้รวมถึงวันที่ที่ระบุ ดูis.gd/3xdsและis.gd/3xdu
Brad Wilson

2
นอกจากนี้ยังเป็นที่น่าสังเกตว่าเมื่อคุณต้องการแทนที่วิธีการนาฬิการะบบของคุณ - สิ่งนี้จะเกิดขึ้นเช่นเมื่อใช้นาฬิกาทั่วโลกในองค์กรที่มีสาขาในเขตเวลาที่กระจัดกระจายอยู่ทั่วไปวิธีนี้จะช่วยให้คุณมีอิสระในระดับธุรกิจในการเปลี่ยนแปลง ความหมายของ "Now"
Mike Burton

1
วิธีนี้ใช้งานได้ดีสำหรับฉันพร้อมกับการใช้ Dependency Injection Framework เพื่อไปยังอินสแตนซ์ IClock
Wilka

8
คำตอบที่ดี แค่อยากจะเพิ่มว่าในเกือบทุกกรณีUtcNowควรใช้แล้วปรับให้เหมาะสมตามข้อกังวลของรหัสเช่นตรรกะทางธุรกิจ UI ฯลฯ การจัดการวันที่และเวลาข้ามเขตเวลาเป็นเขตที่วางทุ่นระเบิด แต่ขั้นแรกที่ดีที่สุดคือการเริ่มต้นด้วยเสมอ เวลา UTC
Adam Ralph

@BradWilson ลิงค์เหล่านั้นเสียแล้ว ไม่สามารถดึงพวกเขาขึ้นมาบน WayBack ได้เช่นกัน

55

Ayende Rahien ใช้วิธีการคงที่ค่อนข้างง่าย ...

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

1
ดูเหมือนว่าจะอันตรายที่จะทำให้ต้นขั้ว / จุดจำลองเป็นตัวแปรส่วนกลางสาธารณะ (ตัวแปรคงที่ของคลาส) จะดีกว่าไหมหากกำหนดขอบเขตให้เป็นระบบที่อยู่ระหว่างการทดสอบเช่นทำให้เป็นสมาชิกแบบคงที่ส่วนตัวของชั้นเรียนที่อยู่ระหว่างการทดสอบ
แอรอน

1
มันเป็นเรื่องของสไตล์ นี่เป็นสิ่งที่น้อยที่สุดที่คุณสามารถทำได้เพื่อให้ได้เวลาของระบบที่เปลี่ยนแปลงได้ในการทดสอบหน่วย
Anthony Mastrean

3
IMHO ขอแนะนำให้ใช้อินเทอร์เฟซแทนที่จะใช้เสื้อกล้ามแบบคงที่ทั่วโลก พิจารณาสถานการณ์ต่อไปนี้: นักวิ่งทดสอบมีประสิทธิภาพและทำการทดสอบให้มากที่สุดเท่าที่จะทำได้โดยขนานกันการทดสอบหนึ่งเปลี่ยนไปตามเวลาที่กำหนดเป็น X อีกรายการเป็น Y ขณะนี้เรามีความขัดแย้งกันและการทดสอบทั้งสองนี้จะสลับความล้มเหลว หากเราใช้อินเทอร์เฟซการทดสอบแต่ละครั้งจะเลียนแบบอินเทอร์เฟซตามความต้องการของเขาการทดสอบแต่ละครั้งจะแยกออกจากการทดสอบอื่น ๆ HTH
ShloEmi

17

ฉันคิดว่าการสร้างคลาสนาฬิกาแยกต่างหากสำหรับสิ่งง่ายๆเช่นการหาวันที่ปัจจุบันเป็นเรื่องที่เกินความจำเป็นไปหน่อย

คุณสามารถส่งวันที่ของวันนี้เป็นพารามิเตอร์เพื่อให้คุณป้อนวันที่อื่นในการทดสอบได้ สิ่งนี้มีประโยชน์เพิ่มเติมในการทำให้โค้ดของคุณมีความยืดหยุ่นมากขึ้น


ฉัน +1 ทั้งของคุณและคำตอบของแบลร์แม้ว่าทั้งคู่จะคัดค้าน ฉันคิดว่าทั้งสองวิธีใช้ได้ แนวทางของคุณฉันอาจจะใช้โครงการที่ไม่ใช้อะไรอย่าง Unity
RichardOD

1
ได้คุณสามารถเพิ่มพารามิเตอร์สำหรับ "now" ได้ อย่างไรก็ตามในบางกรณีคุณต้องเปิดเผยพารามิเตอร์ที่โดยปกติคุณไม่ต้องการเปิดเผย ตัวอย่างเช่นสมมติว่าคุณมีวิธีการคำนวณวันระหว่างวันที่ถึงตอนนี้คุณไม่ต้องการให้ "now" เป็นพารามิเตอร์เพราะจะทำให้สามารถปรับเปลี่ยนผลลัพธ์ของคุณได้ หากเป็นโค้ดของคุณเองให้เพิ่ม "now" เป็นพารามิเตอร์ แต่ถ้าคุณทำงานในทีมคุณจะไม่มีทางรู้ว่านักพัฒนาคนอื่นจะใช้โค้ดของคุณเพื่ออะไรดังนั้นหาก "ตอนนี้" เป็นส่วนสำคัญของโค้ดของคุณคุณต้อง ป้องกันไม่ให้มีการจัดการหรือใช้ในทางที่ผิด
Stitch10925

17

การใช้ Microsoft Fakes เพื่อสร้าง shim เป็นวิธีที่ง่ายมากในการทำเช่นนี้ สมมติว่าฉันมีชั้นเรียนต่อไปนี้:

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

}

ใน Visual Studio 2012 คุณสามารถเพิ่ม Fakes Assembly ในโครงการทดสอบของคุณได้โดยคลิกขวาที่ชุดประกอบที่คุณต้องการสร้าง Fakes / Shims และเลือก "Add Fakes Assembly"

การเพิ่ม Fakes Assembly

สุดท้ายนี่คือลักษณะของคลาสทดสอบ:

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 edition ซึ่งน่าเสียดายสำหรับแนวทางปฏิบัติที่ดีที่สุดเช่นนี้
RJB

12

กุญแจสำคัญในการทดสอบหน่วยที่ประสบความสำเร็จคือการแยกส่วน คุณต้องแยกโค้ดที่น่าสนใจของคุณออกจากการอ้างอิงภายนอกจึงจะสามารถทดสอบแยกกันได้ (โชคดีที่ Test-Driven Development สร้างรหัสแยก)

ในกรณีนี้ภายนอกของคุณคือ DateTime ปัจจุบัน

คำแนะนำของฉันในที่นี้คือการแยกตรรกะที่เกี่ยวข้องกับ DateTime ไปเป็นวิธีการหรือคลาสใหม่หรืออะไรก็ตามที่เหมาะสมในกรณีของคุณและส่ง DateTime เข้ามาตอนนี้การทดสอบหน่วยของคุณสามารถส่ง DateTime ได้ตามอำเภอใจเพื่อให้ได้ผลลัพธ์ที่คาดเดาได้


10

อีกอันหนึ่งใช้ Microsoft Moles ( กรอบการแยกสำหรับ. NET )

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

โมลอนุญาตให้แทนที่เมธอด. NET ใด ๆ ด้วยตัวแทน โมลรองรับเมธอดแบบคงที่หรือไม่เสมือน ไฝอาศัย profiler จาก Pex


สวย แต่ต้อง Visual Studio 2010! :-(
Pandincus

ใช้งานได้ดีใน VS 2008 เช่นกัน ทำงานได้ดีที่สุดกับ MSTest แม้ว่า คุณสามารถใช้ NUnit ได้ แต่ฉันคิดว่าคุณต้องทำการทดสอบกับนักวิ่งทดสอบพิเศษ
Torbjørn

ฉันจะหลีกเลี่ยงการใช้ Moles (aka Microsoft Fakes) เมื่อเป็นไปได้ ตามหลักการแล้วควรใช้สำหรับรหัสเดิมที่ยังไม่สามารถทดสอบได้ผ่านการฉีดขึ้นต่อกัน
brianpeiris

1
@brianpeiris ข้อเสียของการใช้ Microsoft Fakes คืออะไร?
Ray Cheng

คุณไม่สามารถ DI เนื้อหาของบุคคลที่สามได้เสมอไปดังนั้น Fakes จึงเป็นที่ยอมรับอย่างสมบูรณ์ในการทดสอบหน่วยหากโค้ดของคุณเรียกไปยัง API ของบุคคลที่สามที่คุณไม่ต้องการสร้างอินสแตนซ์สำหรับการทดสอบหน่วย ฉันตกลงที่จะหลีกเลี่ยงการใช้ของปลอม / โมลสำหรับรหัสของคุณเอง แต่เป็นที่ยอมรับอย่างสมบูรณ์สำหรับวัตถุประสงค์อื่น ๆ
ChrisCW



2

คุณสามารถฉีดคลาส (ดีกว่า: method / delegate ) ที่คุณใช้DateTime.Nowในคลาสที่กำลังทดสอบ มีDateTime.Nowเป็นค่าเริ่มต้นและการตั้งค่าเพียง แต่ในการทดสอบกับวิธีหลอกตาว่าผลตอบแทนที่คุ้มค่าอย่างต่อเนื่อง

แก้ไข:สิ่งที่แบลร์คอนราดพูด (เขามีรหัสที่จะดู) ยกเว้นฉันมักจะชอบผู้รับมอบสิทธิ์สำหรับสิ่งนี้เนื่องจากพวกเขาไม่ทำให้ลำดับชั้นของคุณยุ่งเหยิงด้วยสิ่งต่างๆเช่นIClock...


1

ฉันเผชิญกับสถานการณ์นี้บ่อยครั้งที่ฉันสร้างนักเก็ตง่ายๆซึ่งแสดงคุณสมบัติNowผ่านอินเทอร์เฟซ

public interface IDateTimeTools
{
    DateTime Now { get; }
}

แน่นอนว่าการนำไปใช้นั้นตรงไปตรงมามาก

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

ดังนั้นหลังจากเพิ่ม nuget ในโครงการของฉันฉันสามารถใช้มันในการทดสอบหน่วย

ใส่คำอธิบายภาพที่นี่

คุณสามารถติดตั้งโมดูลได้จาก GUI Nuget Package Manager หรือโดยใช้คำสั่ง:

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 Cousineau
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.