แทนที่ Java System.currentTimeMillis สำหรับการทดสอบโค้ดที่ไวต่อเวลา


129

มีวิธีการในรหัสหรือด้วยอาร์กิวเมนต์ JVM เพื่อแทนที่เวลาปัจจุบันตามที่นำเสนอผ่านSystem.currentTimeMillisนอกเหนือจากการเปลี่ยนนาฬิการะบบด้วยตนเองบนเครื่องโฮสต์หรือไม่

พื้นหลังเล็กน้อย:

เรามีระบบที่เรียกใช้งานบัญชีจำนวนมากที่หมุนเวียนตรรกะส่วนใหญ่ในวันที่ปัจจุบัน (เช่นวันที่ 1 ของเดือนวันที่ 1 ของปี ฯลฯ )

น่าเสียดายที่รหัสเดิมจำนวนมากเรียกใช้ฟังก์ชันเช่นnew Date()หรือCalendar.getInstance()ซึ่งในที่สุดทั้งสองเรียกSystem.currentTimeMillisว่า

สำหรับวัตถุประสงค์ในการทดสอบตอนนี้เราติดอยู่กับการอัปเดตนาฬิการะบบด้วยตนเองเพื่อปรับแต่งเวลาและวันที่ที่รหัสคิดว่ากำลังทำการทดสอบ

ดังนั้นคำถามของฉันคือ:

มีวิธีลบล้างสิ่งที่ส่งคืนSystem.currentTimeMillisหรือไม่? ตัวอย่างเช่นหากต้องการบอกให้ JVM เพิ่มหรือลบออฟเซ็ตบางส่วนโดยอัตโนมัติก่อนที่จะกลับจากวิธีการนั้น

ขอบคุณล่วงหน้า!


1
ฉันไม่รู้ว่ามันเกี่ยวข้องอีกต่อไปหรือไม่ แต่มีวิธีอื่นในการบรรลุเป้าหมายนี้ด้วย AspectJ ดูคำตอบของฉันได้ที่: stackoverflow.com/questions/18239859/…
NándorElőd Fekete

@ NándorElődFeketeวิธีแก้ปัญหาในลิงค์นั้นน่าสนใจ แต่ต้องมีการคอมไพล์โค้ดใหม่ ฉันสงสัยว่าผู้โพสต์ต้นฉบับมีความสามารถในการคอมไพล์ใหม่หรือไม่เนื่องจากเขาอ้างว่าเกี่ยวข้องกับรหัสเดิม
cleberz

1
@cleberz หนึ่งในคุณสมบัติที่ดีของ AspectJ คือทำงานบน bytecode โดยตรงดังนั้นจึงไม่จำเป็นต้องใช้ซอร์สโค้ดดั้งเดิมในการทำงาน
NándorElőd Fekete

@ NándorElődFeketeขอบคุณมากสำหรับคำใบ้ ฉันไม่ทราบถึงเครื่องมือวัดระดับ bytecode ของด้าน J ฉันใช้เวลาสักพัก แต่ฉันก็สามารถรู้ได้ว่าฉันต้องใช้ทั้งการทอแบบเวลาคอมไพล์ของ rt.jar รวมถึงการทอแบบเวลาโหลดของคลาสที่ไม่ใช่ jdk เพื่อให้เหมาะกับความต้องการของฉัน (แทนที่ระบบ. currentTimeMillis () และ System.nanoTime ())
cleberz

@cleberz ฉันมีคำตอบอื่นสำหรับการทอผ้าผ่านชั้นเรียน JREคุณสามารถตรวจสอบได้เช่นกัน
NándorElőd Fekete

คำตอบ:


115

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

เกือบจะเป็นไปโดยอัตโนมัติด้วยการค้นหาและแทนที่สำหรับเวอร์ชันซิงเกิลตัน:

  • แทนที่Calendar.getInstance()ด้วยClock.getInstance().getCalendarInstance().
  • แทนที่new Date()ด้วยClock.getInstance().newDate()
  • แทนที่System.currentTimeMillis()ด้วยClock.getInstance().currentTimeMillis()

(ฯลฯ ตามต้องการ)

เมื่อคุณทำตามขั้นตอนแรกแล้วคุณสามารถแทนที่ซิงเกิลตันด้วย DI ทีละนิดได้


50
การทำให้โค้ดของคุณยุ่งเหยิงโดยการสรุปหรือรวม API ที่เป็นไปได้ทั้งหมดเพื่อเพิ่มความสามารถในการทดสอบนั้น IMHO ไม่ใช่ความคิดที่ดีนัก แม้ว่าคุณจะสามารถทำการ refactoring เพียงครั้งเดียวด้วยการค้นหาและแทนที่ง่ายๆ แต่โค้ดก็ยากที่จะอ่านทำความเข้าใจและดูแลรักษา เฟรมเวิร์กจำลองจำนวนมากควรจะสามารถปรับเปลี่ยนลักษณะการทำงานของ System.currentTimeMillis ได้หากไม่เป็นเช่นนั้นการใช้ AOP หรือการใช้เครื่องมือที่สร้างขึ้นเองเป็นทางเลือกที่ดีกว่า
jarnbjo

21
@jarnbjo: แน่นอนว่าคุณยินดีรับฟังความคิดเห็นของคุณ - แต่นี่เป็นเทคนิค IMO ที่ได้รับการยอมรับค่อนข้างดีและฉันได้ใช้มันเพื่อให้ได้ผลดีอย่างแน่นอน ไม่เพียง แต่ปรับปรุงความสามารถในการทดสอบเท่านั้น แต่ยังทำให้การพึ่งพาเวลาของระบบมีความชัดเจนซึ่งจะมีประโยชน์เมื่อได้รับภาพรวมของรหัส
Jon Skeet

4
@ virgo47: ฉันคิดว่าเราจะต้องเห็นด้วยกับไม่เห็นด้วย ฉันไม่เห็นว่า "ระบุการอ้างอิงของคุณอย่างชัดเจน" เป็นการสร้างมลพิษให้กับโค้ด - ฉันเห็นว่ามันเป็นวิธีธรรมดาในการทำให้โค้ดชัดเจนขึ้น ฉันไม่เห็นว่าการซ่อนการอ้างอิงเหล่านั้นผ่านการโทรแบบคงที่โดยไม่จำเป็นว่า "ยอมรับได้อย่างสมบูรณ์"
Jon Skeet

26
อัปเดตแพคเกจ java.timeใหม่ที่สร้างขึ้นใน Java 8 มีjava.time.Clockคลาส "เพื่อให้สามารถเสียบนาฬิกาสำรองได้ตามต้องการ"
Basil Bourque

7
คำตอบนี้บ่งบอกว่า DI ดีทั้งหมด โดยส่วนตัวแล้วฉันยังไม่เห็นแอปพลิเคชันในโลกแห่งความเป็นจริงที่ใช้ประโยชน์ได้ดี แต่เรากลับเห็นความหลากหลายของอินเทอร์เฟซที่แยกจากกันอย่างไร้จุดหมาย "อ็อบเจ็กต์" ที่ไม่มีสถานะ "อ็อบเจ็กต์" ข้อมูลเท่านั้นและคลาสการทำงานร่วมกันที่ต่ำ IMO ความเรียบง่ายและการออกแบบเชิงวัตถุ (กับวัตถุจริงที่มีสภาพสมบูรณ์) เป็นทางเลือกที่ดีกว่า เกี่ยวกับstaticวิธีการตรวจสอบให้แน่ใจว่าพวกเขาไม่ใช่ OO แต่การฉีดการพึ่งพาแบบไร้สัญชาติซึ่งอินสแตนซ์ไม่ทำงานในสถานะอินสแตนซ์ใด ๆ นั้นไม่ดีกว่าจริงๆ มันเป็นเพียงวิธีการแปลก ๆ ในการอำพรางพฤติกรรม "คงที่" อย่างมีประสิทธิภาพอยู่ดี
Rogério

78

TL; DR

มีวิธีการในรหัสหรือด้วยอาร์กิวเมนต์ JVM เพื่อแทนที่เวลาปัจจุบันตามที่นำเสนอผ่าน System.currentTimeMillis นอกเหนือจากการเปลี่ยนนาฬิการะบบด้วยตนเองบนเครื่องโฮสต์หรือไม่

ใช่.

Instant.now( 
    Clock.fixed( 
        Instant.parse( "2016-01-23T12:34:56Z"), ZoneOffset.UTC
    )
)

Clock ใน java.time

เรามีโซลูชั่นใหม่ในการแก้ไขปัญหาของการเปลี่ยนนาฬิกา pluggable เพื่อความสะดวกในการทดสอบกับมารยาทค่าวันเวลา แพคเกจ java.timeในJava 8รวมถึงระดับนามธรรมjava.time.Clockมีวัตถุประสงค์อย่างชัดเจน:

เพื่อให้สามารถเสียบนาฬิกาสำรองได้ตามต้องการ

คุณสามารถเชื่อมต่อการใช้งานของคุณเองClockได้แม้ว่าคุณจะพบว่ามีอยู่แล้วเพื่อตอบสนองความต้องการของคุณ เพื่อความสะดวกของคุณ java.time มีวิธีการแบบคงที่เพื่อให้การใช้งานพิเศษ การใช้งานทางเลือกเหล่านี้อาจมีประโยชน์ในระหว่างการทดสอบ

จังหวะที่เปลี่ยนแปลง

tick…วิธีการต่างๆ ผลิตนาฬิกาที่เพิ่มช่วงเวลาปัจจุบันด้วยจังหวะที่แตกต่างกัน

ค่าเริ่มต้นจะClockรายงานเวลาที่อัปเดตบ่อยเป็นมิลลิวินาทีใน Java 8 และใน Java 9 โดยละเอียดเท่ากับนาโนวินาที (ขึ้นอยู่กับฮาร์ดแวร์ของคุณ) คุณสามารถขอให้รายงานช่วงเวลาปัจจุบันที่แท้จริงด้วยรายละเอียดที่แตกต่างกัน

  • tickSeconds - เพิ่มขึ้นทั้งวินาที
  • tickMinutes - เพิ่มขึ้นทั้งนาที
  • tick- เพิ่มขึ้นโดยDurationอาร์กิวเมนต์ที่ส่งผ่าน

นาฬิกาเท็จ

นาฬิกาบางตัวสามารถโกหกได้ซึ่งให้ผลลัพธ์ที่แตกต่างจากนาฬิกาฮาร์ดแวร์ของโฮสต์ OS

  • fixed - รายงานช่วงเวลาเดียวที่ไม่เปลี่ยนแปลง (ไม่เพิ่มขึ้น) เป็นช่วงเวลาปัจจุบัน
  • offset- รายงานช่วงเวลาปัจจุบัน แต่เปลี่ยนไปตามDurationอาร์กิวเมนต์ที่ส่งผ่าน

ตัวอย่างเช่นล็อกในช่วงเวลาแรกของคริสต์มาสที่เร็วที่สุดในปีนี้ ในคำอื่น ๆ เมื่อซานต้าและกวางเรนเดียของเขาให้หยุดแรกของพวกเขา โซนเวลาที่เร็วที่สุดในปัจจุบันน่าจะเป็นที่Pacific/Kiritimati+14:00

LocalDate ld = LocalDate.now( ZoneId.of( "America/Montreal" ) );
LocalDate xmasThisYear = MonthDay.of( Month.DECEMBER , 25 ).atYear( ld.getYear() );
ZoneId earliestXmasZone = ZoneId.of( "Pacific/Kiritimati" ) ;
ZonedDateTime zdtEarliestXmasThisYear = xmasThisYear.atStartOfDay( earliestXmasZone );
Instant instantEarliestXmasThisYear = zdtEarliestXmasThisYear.toInstant();
Clock clockEarliestXmasThisYear = Clock.fixed( instantEarliestXmasThisYear , earliestXmasZone );

ใช้นาฬิกาคงที่พิเศษเพื่อย้อนกลับไปยังช่วงเวลาเดิมเสมอ เราได้รับช่วงเวลาแรกของวันคริสต์มาสในKiritimatiโดย UTC จะแสดงเวลานาฬิกาแขวนที่สิบสี่ชั่วโมงก่อนหน้านี้ 10.00 น. ของวันก่อนหน้าของวันที่ 24 ธันวาคม

Instant instant = Instant.now( clockEarliestXmasThisYear );
ZonedDateTime zdt = ZonedDateTime.now( clockEarliestXmasThisYear );

instant.toString (): 2016-12-24T10: 00: 00Z

zdt.toString (): 2016-12-25T00: 00 + 14: 00 [Pacific / Kiritimati]

ดูรหัสที่อาศัยอยู่ใน IdeOne.com

เวลาจริงเขตเวลาที่แตกต่างกัน

คุณสามารถควบคุมเขตเวลาที่กำหนดโดยClockการนำไปใช้ สิ่งนี้อาจมีประโยชน์ในการทดสอบบางอย่าง แต่ฉันไม่แนะนำสิ่งนี้ในโค้ดการผลิตซึ่งคุณควรระบุอย่างชัดเจนเสมอว่าเป็นทางเลือกZoneIdหรือZoneOffsetอาร์กิวเมนต์

คุณสามารถระบุให้ UTC เป็นโซนเริ่มต้น

ZonedDateTime zdtClockSystemUTC = ZonedDateTime.now ( Clock.systemUTC () );

คุณสามารถระบุเขตเวลาใดก็ได้ ระบุชื่อโซนเวลาที่เหมาะสมในรูปแบบของcontinent/regionเช่นAmerica/Montreal, หรือAfrica/Casablanca Pacific/Aucklandอย่าใช้อักษรย่อ 3-4 ตัวเช่นESTหรือISTเนื่องจากไม่ใช่เขตเวลาจริงไม่เป็นมาตรฐานและไม่ซ้ำกัน (!)

ZonedDateTime zdtClockSystem = ZonedDateTime.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );

คุณสามารถระบุโซนเวลาเริ่มต้นปัจจุบันของ JVM ควรเป็นค่าเริ่มต้นสำหรับClockออบเจ็กต์หนึ่ง ๆ

ZonedDateTime zdtClockSystemDefaultZone = ZonedDateTime.now ( Clock.systemDefaultZone () );

เรียกใช้รหัสนี้เพื่อเปรียบเทียบ โปรดทราบว่าพวกเขารายงานในช่วงเวลาเดียวกันจุดเดียวกันบนไทม์ไลน์ พวกเขาต่างกันเพียง แต่ในเวลาผนังนาฬิกา ; กล่าวอีกนัยหนึ่งคือสามวิธีในการพูดสิ่งเดียวกันสามวิธีในการแสดงช่วงเวลาเดียวกัน

System.out.println ( "zdtClockSystemUTC.toString(): " + zdtClockSystemUTC );
System.out.println ( "zdtClockSystem.toString(): " + zdtClockSystem );
System.out.println ( "zdtClockSystemDefaultZone.toString(): " + zdtClockSystemDefaultZone );

America/Los_Angeles เป็นโซนเริ่มต้นปัจจุบันของ JVM บนคอมพิวเตอร์ที่เรียกใช้รหัสนี้

zdtClockSystemUTC.toString (): 2016-12-31T20: 52: 39.688Z

zdtClockSystem.toString (): 2016-12-31T15: 52: 39.750-05: 00 [อเมริกา / มอนทรีออล]

zdtClockSystemDefaultZone.toString (): 2016-12-31T12: 52: 39.762-08: 00 [อเมริกา / Los_Angeles]

Instantระดับอยู่เสมอในเวลา UTC โดยความหมาย ดังนั้นClockประเพณีที่เกี่ยวข้องกับโซนทั้งสามนี้จึงมีผลเหมือนกันทุกประการ

Instant instantClockSystemUTC = Instant.now ( Clock.systemUTC () );
Instant instantClockSystem = Instant.now ( Clock.system ( ZoneId.of ( "America/Montreal" ) ) );
Instant instantClockSystemDefaultZone = Instant.now ( Clock.systemDefaultZone () );

instantClockSystemUTC.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystem.toString (): 2016-12-31T20: 52: 39.763Z

instantClockSystemDefaultZone.toString (): 2016-12-31T20: 52: 39.763Z

นาฬิกาเริ่มต้น

การดำเนินงานที่ใช้โดยเริ่มต้นสำหรับการเป็นหนึ่งที่ส่งกลับโดยInstant.now Clock.systemUTC()นี่คือการนำไปใช้เมื่อคุณไม่ได้ระบุไฟล์Clock. ดูตัวเองในรุ่นก่อนวางจำหน่าย Java 9 Instant.nowซอร์สโค้ด

public static Instant now() {
    return Clock.systemUTC().instant();
}

เริ่มต้นClockสำหรับการOffsetDateTime.nowและเป็นZonedDateTime.now Clock.systemDefaultZone()ดูรหัสที่มา

public static ZonedDateTime now() {
    return now(Clock.systemDefaultZone());
}

ลักษณะการทำงานของการปรับใช้เริ่มต้นเปลี่ยนไประหว่าง Java 8 และ Java 9 ใน Java 8 ช่วงเวลาปัจจุบันจะถูกบันทึกด้วยความละเอียดเป็นมิลลิวินาทีเท่านั้นแม้ว่าคลาสจะสามารถจัดเก็บความละเอียดได้ในระดับนาโนวินาทีก็ตาม Java 9 นำเสนอการใช้งานใหม่ที่สามารถจับภาพช่วงเวลาปัจจุบันด้วยความละเอียดนาโนวินาที - แน่นอนขึ้นอยู่กับความสามารถของนาฬิกาฮาร์ดแวร์คอมพิวเตอร์ของคุณ


เกี่ยวกับjava.time

java.timeกรอบถูกสร้างขึ้นใน Java 8 และต่อมา ชั้นเรียนเหล่านี้แย่งลำบากเก่ามรดกเรียนวันที่เวลาเช่นjava.util.Date, และCalendarSimpleDateFormat

ต้องการเรียนรู้เพิ่มเติมโปรดดูที่ออราเคิลกวดวิชา และค้นหา Stack Overflow สำหรับตัวอย่างและคำอธิบายมากมาย สเปกJSR 310

โครงการJoda-Timeขณะนี้อยู่ในโหมดการบำรุงรักษาแนะนำการโยกย้ายไปยังคลาสjava.time

คุณสามารถแลกเปลี่ยนวัตถุjava.timeโดยตรงกับฐานข้อมูลของคุณ ใช้ไดรเวอร์ JDBC ที่สอดคล้องกับJDBC 4.2หรือใหม่กว่า ไม่จำเป็นต้องมีสตริงไม่จำเป็นต้องมีjava.sql.*คลาส Hibernate 5 & JPA 2.2 สนับสนุนjava.time

จะหาคลาส java.time ได้ที่ไหน?

  • Java SE 8 , Java SE 9 , Java SE 10 , Java SE 11และใหม่กว่า - เป็นส่วนหนึ่งของ Java API มาตรฐานพร้อมการใช้งานแบบรวม
    • Java 9นำเสนอคุณสมบัติและการแก้ไขเล็กน้อย
  • Java SE 6และ Java SE 7
    • ส่วนใหญ่ของjava.timeการทำงานจะกลับรังเพลิง Java 6 และ 7 ในThreeTen-ย้ายกลับ
  • Android
    • การใช้งานบันเดิล Android (26+) เวอร์ชันที่ใหม่กว่าของคลาสjava.time
    • สำหรับ Android รุ่นก่อนหน้า (<26) กระบวนการที่เรียกว่าAPI desugaringจะนำส่วนย่อยของฟังก์ชันjava.time ที่ไม่ได้ติดตั้งไว้ใน Android

นี่คือคำตอบที่ถูกต้อง
bharal

42

ในฐานะที่เป็นที่กล่าวโดยจอน Skeet :

"use Joda Time" เป็นคำตอบที่ดีที่สุดสำหรับคำถามที่เกี่ยวข้องกับ "ฉันจะบรรลุ X ด้วย java.util.Date/Calendar ได้อย่างไร"

ต่อไปนี้ (สมมติว่าคุณเพิ่งแทนที่ทั้งหมดnew Date()ด้วยnew DateTime().toDate())

//Change to specific time
DateTimeUtils.setCurrentMillisFixed(millis);
//or set the clock to be a difference from system time
DateTimeUtils.setCurrentMillisOffset(millis);
//Reset to system time
DateTimeUtils.setCurrentMillisSystem();

หากคุณต้องการนำเข้าไลบรารีที่มีอินเทอร์เฟซ (ดูความคิดเห็นของจอนด้านล่าง) คุณสามารถใช้นาฬิกาของ Prevaylerซึ่งจะให้การใช้งานเช่นเดียวกับอินเทอร์เฟซมาตรฐาน กระปุกเต็มมีเพียง 96kB ดังนั้นจึงไม่ควรทำลายธนาคาร ...


13
ไม่ฉันจะไม่แนะนำสิ่งนี้ - มันไม่ได้นำคุณไปสู่นาฬิกาที่ถอดเปลี่ยนได้อย่างแท้จริง มันช่วยให้คุณใช้สถิติตลอด แน่นอนว่าการใช้ Joda Time เป็นความคิดที่ดี แต่ฉันก็ยังต้องการใช้อินเทอร์เฟซนาฬิกาหรือที่คล้ายกัน
Jon Skeet

@ จอน: จริง - สำหรับการทดสอบก็โอเค แต่จะดีกว่าถ้ามีอินเทอร์เฟซ
Stephen

5
@ จอน: นี่เป็นวิธีที่ราบรื่นและสมบูรณ์แบบและง่าย IMHO หากคุณต้องการทดสอบรหัสขึ้นอยู่กับเวลาซึ่งใช้ JodaTime อยู่แล้ว การพยายามสร้างวงล้อใหม่โดยการแนะนำเลเยอร์ / เฟรมเวิร์กที่เป็นนามธรรมอื่นไม่ใช่หนทางที่จะไปหากทุกอย่างได้รับการแก้ไขสำหรับคุณด้วย JodaTime แล้ว
Stefan Haberl

2
@ JonSkeet: นี่ไม่ใช่สิ่งที่ไมค์ขอมา แต่แรก เขาต้องการเครื่องมือในการทดสอบรหัสขึ้นอยู่กับเวลาและไม่ใช่นาฬิกาที่เปลี่ยนได้เต็มรูปแบบในรหัสการผลิต ฉันชอบให้สถาปัตยกรรมของฉันซับซ้อนเท่าที่จำเป็น แต่เรียบง่ายที่สุด การแนะนำเลเยอร์นามธรรมเพิ่มเติม (เช่นอินเทอร์เฟซ) ที่นี่ไม่จำเป็น
Stefan Haberl

2
@StefanHaberl: มีหลายสิ่งที่ไม่ "จำเป็น" อย่างเคร่งครัด - แต่สิ่งที่ฉันแนะนำจริงๆคือการทำให้การพึ่งพาที่มีอยู่แล้วอย่างชัดเจนและสามารถทดสอบแยกได้ง่ายขึ้น แกล้งเวลาของระบบที่มีสถิตมีทุกปัญหาปกติของสถิต - เป็นรัฐทั่วโลกไม่มีเหตุผลที่ดี Mike กำลังขอวิธีเขียนโค้ดที่ทดสอบได้ซึ่งใช้วันที่และเวลาปัจจุบัน ฉันยังคงเชื่อว่าข้อเสนอแนะของฉันสะอาดกว่ามาก (และทำให้การอ้างอิงชัดเจนขึ้น) มากกว่าการฟัดแบบคงที่
Jon Skeet

15

ในขณะที่การใช้รูปแบบ DateFactory บางอย่างดูดี แต่ก็ไม่ครอบคลุมไลบรารีที่คุณไม่สามารถควบคุมได้ลองนึกภาพคำอธิบายประกอบการตรวจสอบความถูกต้อง @Past พร้อมการใช้งานโดยอาศัย System.currentTimeMillis (มีเช่นนั้น)

นั่นเป็นเหตุผลที่เราใช้ jmockit เพื่อจำลองเวลาของระบบโดยตรง:

import mockit.Mock;
import mockit.MockClass;
...
@MockClass(realClass = System.class)
public static class SystemMock {
    /**
     * Fake current time millis returns value modified by required offset.
     *
     * @return fake "current" millis
     */
    @Mock
    public static long currentTimeMillis() {
        return INIT_MILLIS + offset + millisSinceClassInit();
    }
}

Mockit.setUpMock(SystemMock.class);

เนื่องจากเป็นไปไม่ได้ที่จะไปถึงค่ามิลลิวินาทีที่ไม่ได้ลอกแบบเดิมเราจึงใช้ตัวจับเวลาแบบนาโนแทนซึ่งไม่เกี่ยวข้องกับนาฬิกาแขวน แต่เวลาสัมพัทธ์จะเพียงพอที่นี่:

// runs before the mock is applied
private static final long INIT_MILLIS = System.currentTimeMillis();
private static final long INIT_NANOS = System.nanoTime();

private static long millisSinceClassInit() {
    return (System.nanoTime() - INIT_NANOS) / 1000000;
}

มีเอกสารปัญหาซึ่งเมื่อใช้ HotSpot เวลาจะกลับมาเป็นปกติหลังจากมีการโทรหลายครั้งนี่คือรายงานปัญหา: http://code.google.com/p/jmockit/issues/detail?id=43

เพื่อเอาชนะสิ่งนี้เราต้องเปิดการเพิ่มประสิทธิภาพ HotSpot เฉพาะหนึ่งรายการ - รัน JVM ด้วยอาร์กิวเมนต์-XX:-Inlineนี้

แม้ว่าสิ่งนี้อาจไม่สมบูรณ์แบบสำหรับการผลิต แต่ก็ใช้ได้ดีสำหรับการทดสอบและมีความโปร่งใสอย่างยิ่งสำหรับการใช้งานโดยเฉพาะอย่างยิ่งเมื่อ DataFactory ไม่สมเหตุสมผลทางธุรกิจและได้รับการแนะนำเนื่องจากการทดสอบเท่านั้น จะเป็นการดีที่จะมีตัวเลือก JVM ในตัวเพื่อให้ทำงานในเวลาที่ต่างกัน แต่แย่มากที่เป็นไปไม่ได้หากไม่มีแฮ็กเช่นนี้

เรื่องราวทั้งหมดอยู่ในบล็อกโพสต์ของฉันที่นี่: http://virgo47.wordpress.com/2012/06/22/changing-system-time-in-java/

SystemTimeShifter คลาสที่มีประโยชน์ที่สมบูรณ์มีให้ในโพสต์ สามารถใช้คลาสในการทดสอบของคุณหรือสามารถใช้เป็นคลาสหลักอันดับหนึ่งก่อนคลาสหลักจริงของคุณได้อย่างง่ายดายเพื่อเรียกใช้แอปพลิเคชันของคุณ (หรือแม้แต่เซิร์ฟเวอร์แอพทั้งหมด) ในเวลาอื่น แน่นอนว่ามีจุดประสงค์เพื่อการทดสอบเป็นหลักไม่ใช่เพื่อสภาพแวดล้อมการผลิต

แก้ไขกรกฎาคม 2014: JMockit เปลี่ยนไปมากเมื่อเร็ว ๆ นี้และคุณต้องใช้ JMockit 1.0 เพื่อใช้สิ่งนี้อย่างถูกต้อง (IIRC) ไม่สามารถอัปเกรดเป็นเวอร์ชันใหม่ล่าสุดได้อย่างแน่นอนโดยที่อินเทอร์เฟซแตกต่างกันโดยสิ้นเชิง ฉันกำลังคิดที่จะใส่ของที่จำเป็น แต่เนื่องจากเราไม่ต้องการสิ่งนี้ในโครงการใหม่ของเราฉันจึงไม่ได้พัฒนาสิ่งนี้เลย


1
ความจริงที่ว่าคุณกำลังเปลี่ยนเวลาสำหรับทุกชั้นเรียนเป็นไปได้ทั้งสองทาง ... ตัวอย่างเช่นหากคุณล้อเลียน nanoTime แทน currentTimeMillis คุณจะต้องระวังอย่างมากที่จะไม่ทำลาย java.util.concurrent ตามหลักการทั่วไปหากไลบรารีของบุคคลที่สามใช้งานได้ยากในการทดสอบคุณไม่ควรเยาะเย้ยอินพุตของไลบรารี: คุณควรเยาะเย้ยไลบรารีนั้นเอง
Dan Berindei

1
เราใช้เทคนิคนี้สำหรับการทดสอบโดยที่เราไม่ได้ล้อเลียนสิ่งอื่นใด เป็นการทดสอบการบูรณาการและเราทดสอบทั้งสแต็กว่าทำในสิ่งที่ควรหรือไม่เมื่อมีกิจกรรมบางอย่างเกิดขึ้นเป็นครั้งแรกของปีเป็นต้นจุดที่ดีเกี่ยวกับ nanoTime โชคดีที่เราไม่จำเป็นต้องเยาะเย้ยสิ่งนั้นเนื่องจากไม่มีนาฬิกาแขวน ความหมายเลย ฉันชอบที่จะเห็น Java พร้อมคุณสมบัติการเปลี่ยนเวลาเพราะฉันต้องการมากกว่าหนึ่งครั้งแล้วและยุ่งกับเวลาของระบบเพราะนี่เป็นเรื่องที่โชคร้ายมาก
ราศีกันย์ 47

7

Powermock ใช้งานได้ดี แค่ใช้เพื่อล้อเลียนSystem.currentTimeMillis().


ฉันพยายามใช้ Powermock และถ้าฉันเข้าใจถูกต้องมันจะไม่ล้อเลียนด้านคาลลีจริงๆ แต่กลับพบผู้โทรทั้งหมดและใช้การเยาะเย้ยมากกว่า อาจใช้เวลานานมากเนื่องจากคุณต้องปล่อยให้ตรวจสอบทุกคลาสใน classpath ของคุณหากคุณต้องการให้แน่ใจว่าแอปพลิเคชันของคุณทำงานได้ตามเวลาที่กำหนด แก้ไขฉันถ้าฉันผิดเกี่ยวกับวิธีล้อเลียน Powermock
ราศีกันย์ 47

1
@ virgo47 ไม่ใช่คุณเข้าใจผิด PowerMock จะไม่ "ตรวจสอบทุกชั้นเรียนใน classpath ของคุณ"; จะตรวจสอบเฉพาะชั้นเรียนที่คุณบอกให้ตรวจสอบโดยเฉพาะเท่านั้น (ผ่าน@PrepareForTestคำอธิบายประกอบในชั้นเรียนทดสอบ)
Rogério

1
@ Rogério - นั่นคือสิ่งที่ฉันเข้าใจ - ฉันพูดว่า "คุณต้องปล่อยให้มัน ... " นั่นคือถ้าคุณคาดหวังว่าจะโทรไปSystem.currentTimeMillisที่ใดก็ได้บน classpath ของคุณ (lib ใดก็ได้) คุณต้องตรวจสอบทุกคลาส นั่นคือสิ่งที่ฉันหมายถึงไม่มีอะไรอื่น ประเด็นคือคุณล้อเลียนพฤติกรรมนั้นกับผู้โทร ("คุณบอกโดยเฉพาะ") วิธีนี้ใช้ได้สำหรับการทดสอบอย่างง่าย แต่ไม่ใช่สำหรับการทดสอบที่คุณไม่สามารถแน่ใจได้ว่าอะไรเรียกวิธีนั้นมาจากที่ใด (เช่นการทดสอบส่วนประกอบที่ซับซ้อนมากขึ้นกับไลบรารีที่เกี่ยวข้อง) นั่นไม่ได้หมายความว่า Powermock ผิด แต่หมายความว่าคุณไม่สามารถใช้กับการทดสอบประเภทนี้ได้
virgo47

1
@ virgo47 ใช่ฉันเข้าใจว่าคุณหมายถึงอะไร ฉันคิดว่าคุณหมายความว่า PowerMock จะต้องตรวจสอบทุกคลาสในคลาสพา ธ โดยอัตโนมัติซึ่งเห็นได้ชัดว่ามันไร้สาระ ในทางปฏิบัติสำหรับการทดสอบหน่วยเรามักจะรู้ว่าคลาสใดอ่านเวลาดังนั้นจึงไม่ใช่ประเด็นที่จะระบุ สำหรับการทดสอบการรวมแน่นอนข้อกำหนดใน PowerMock ที่จะต้องมีคลาสที่ใช้ทั้งหมดที่ระบุไว้อาจเป็นปัญหาได้
Rogério

6

ใช้ Aspect-Oriented Programming (AOP ตัวอย่างเช่น AspectJ) เพื่อสานคลาส System เพื่อส่งคืนค่าที่กำหนดไว้ล่วงหน้าซึ่งคุณสามารถตั้งค่าได้ภายในกรณีทดสอบของคุณ

หรือสานคลาสแอปพลิเคชันเพื่อเปลี่ยนเส้นทางการเรียกไปยังSystem.currentTimeMillis()หรือไปnew Date()ยังคลาสยูทิลิตี้อื่นของคุณเอง

java.lang.*อย่างไรก็ตามคลาสระบบการทอผ้า ( ) นั้นค่อนข้างยุ่งยากกว่าเล็กน้อยและคุณอาจต้องทำการทอแบบออฟไลน์สำหรับ rt.jar และใช้ JDK / rt.jar แยกต่างหากสำหรับการทดสอบของคุณ

เรียกว่าการทอแบบไบนารีและยังมีเครื่องมือพิเศษในการทอผ้าของคลาสระบบและหลีกเลี่ยงปัญหาบางอย่างด้วย (เช่นการบูตเครื่อง VM อาจไม่ทำงาน)


แน่นอนว่าใช้งานได้ แต่มันง่ายกว่าการใช้เครื่องมือจำลองมากกว่าเครื่องมือ AOP เครื่องมือประเภทหลังเป็นเพียงทั่วไปเกินไปสำหรับวัตถุประสงค์ที่เป็นปัญหา
Rogério

3

ไม่มีวิธีทำสิ่งนี้โดยตรงใน VM แต่คุณสามารถตั้งค่าเวลาของระบบบนเครื่องทดสอบโดยทางโปรแกรมได้ ระบบปฏิบัติการ (ทั้งหมด?) ส่วนใหญ่มีคำสั่งบรรทัดคำสั่งเพื่อทำสิ่งนี้


ตัวอย่างเช่นบน windows dateและtimeคำสั่ง บน Linux dateคำสั่ง
Jeremy Raymond

เหตุใดจึงไม่ควรทำเช่นนี้กับ AOP หรือการใช้เครื่องมือ (เคยทำมาแล้ว)
jarnbjo

3

วิธีการทำงานเพื่อแทนที่เวลาของระบบปัจจุบันสำหรับวัตถุประสงค์ในการทดสอบ JUnit ในเว็บแอปพลิเคชัน Java 8 ด้วย EasyMock โดยไม่ต้องใช้ Joda Time และไม่มี PowerMock

นี่คือสิ่งที่คุณต้องทำ:

สิ่งที่ต้องทำในคลาสทดสอบ

ขั้นตอนที่ 1

เพิ่มjava.time.Clockแอตทริบิวต์ใหม่ให้กับคลาสที่ทดสอบMyServiceและตรวจสอบให้แน่ใจว่าแอตทริบิวต์ใหม่จะได้รับการเตรียมใช้งานอย่างถูกต้องตามค่าเริ่มต้นด้วยบล็อกอินสแตนซ์หรือตัวสร้าง:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  private Clock clock;
  public Clock getClock() { return clock; }
  public void setClock(Clock newClock) { clock = newClock; }

  public void initDefaultClock() {
    setClock(
      Clock.system(
        Clock.systemDefaultZone().getZone() 
        // You can just as well use
        // java.util.TimeZone.getDefault().toZoneId() instead
      )
    );
  }
  { 
    initDefaultClock(); // initialisation in an instantiation block, but 
                        // it can be done in a constructor just as well
  }
  // (...)
}

ขั้นตอนที่ 2

ใส่แอตทริบิวต์ใหม่clockลงในเมธอดที่เรียกวันที่ - เวลาปัจจุบัน ตัวอย่างเช่นในกรณีของฉันฉันต้องทำการตรวจสอบว่าวันที่ที่เก็บไว้ใน dataase เกิดขึ้นก่อนหน้าLocalDateTime.now()นี้หรือไม่ซึ่งฉันได้เปลี่ยนตำแหน่งLocalDateTime.now(clock)ใหม่เช่น:

import java.time.Clock;
import java.time.LocalDateTime;

public class MyService {
  // (...)
  protected void doExecute() {
    LocalDateTime dateToBeCompared = someLogic.whichReturns().aDate().fromDB();
    while (dateToBeCompared.isBefore(LocalDateTime.now(clock))) {
      someOtherLogic();
    }
  }
  // (...) 
}

สิ่งที่ต้องทำในชั้นเรียนทดสอบ

ขั้นตอนที่ 3

ในคลาสทดสอบให้สร้างวัตถุนาฬิกาจำลองแล้วฉีดเข้าไปในอินสแตนซ์ของคลาสทดสอบก่อนที่คุณจะเรียกใช้วิธีการทดสอบdoExecute()จากนั้นรีเซ็ตกลับทันทีในภายหลังดังนี้:

import java.time.Clock;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import org.junit.Test;

public class MyServiceTest {
  // (...)
  private int year = 2017;
  private int month = 2;
  private int day = 3;

  @Test
  public void doExecuteTest() throws Exception {
    // (...) EasyMock stuff like mock(..), expect(..), replay(..) and whatnot

    MyService myService = new MyService();
    Clock mockClock =
      Clock.fixed(
        LocalDateTime.of(year, month, day, 0, 0).toInstant(OffsetDateTime.now().getOffset()),
        Clock.systemDefaultZone().getZone() // or java.util.TimeZone.getDefault().toZoneId()
      );
    myService.setClock(mockClock); // set it before calling the tested method

    myService.doExecute(); // calling tested method 

    myService.initDefaultClock(); // reset the clock to default right afterwards with our own previously created method

    // (...) remaining EasyMock stuff: verify(..) and assertEquals(..)
    }
  }

ตรวจสอบในโหมดการแก้ปัญหาและคุณจะเห็นวันที่ 3 กุมภาพันธ์ 2017 ที่ได้รับการฉีดอย่างถูกต้องลงในอินสแตนซ์และนำมาใช้ในการเรียนการสอนการเปรียบเทียบและจากนั้นได้รับอย่างถูกต้องรีเซ็ตวันที่ปัจจุบันด้วยmyServiceinitDefaultClock()


2

ในความคิดของฉันมีเพียงวิธีแก้ปัญหาที่ไม่รุกรานเท่านั้นที่สามารถทำงานได้ โดยเฉพาะอย่างยิ่งถ้าคุณมี libs ภายนอกและฐานรหัสดั้งเดิมขนาดใหญ่ไม่มีวิธีที่เชื่อถือได้ในการเยาะเย้ยเวลา

JMockit ... ใช้งานได้ตามจำนวนครั้งที่จำกัด เท่านั้น

PowerMock & Co ... จำเป็นต้องเยาะเย้ยไคลเอนต์ไปที่ System.currentTimeMillis () อีกครั้งตัวเลือกที่รุกราน

จากนี้ฉันเห็นเฉพาะวิธีjavaagentหรือaop ที่กล่าวถึงว่าโปร่งใสกับทั้งระบบ มีใครทำและชี้ไปที่วิธีแก้ปัญหาดังกล่าวได้บ้าง?

@jarnbjo: คุณช่วยแสดงรหัส javaagent ได้ไหม?


3
โซลูชัน jmockit ทำงานได้ไม่ จำกัด จำนวนครั้งด้วย -XX เล็กน้อย: - แฮ็คอินไลน์ (บน Sun's HotSpot): virgo47.wordpress.com/2012/06/22/changing-system-time-in-javaคลาสที่มีประโยชน์ที่สมบูรณ์มีให้ SystemTimeShifter ในโพสต์ สามารถใช้คลาสในการทดสอบของคุณหรือสามารถใช้เป็นคลาสหลักอันดับหนึ่งก่อนคลาสหลักจริงของคุณได้อย่างง่ายดายเพื่อเรียกใช้แอปพลิเคชันของคุณ (หรือแม้แต่เซิร์ฟเวอร์แอพทั้งหมด) ในเวลาอื่น แน่นอนว่ามีจุดประสงค์เพื่อการทดสอบเป็นหลักไม่ใช่เพื่อสภาพแวดล้อมการผลิต
ราศีกันย์ 47

2

หากคุณกำลังใช้ลินุกซ์, คุณสามารถใช้สาขาต้นแบบของ libfaketime หรือในช่วงเวลาของการทดสอบกระทำ4ce2835

เพียงตั้งค่าตัวแปรสภาพแวดล้อมด้วยเวลาที่คุณต้องการจำลองแอปพลิเคชัน java ของคุณและเรียกใช้โดยใช้ ld-preloading:

# bash
export FAKETIME="1985-10-26 01:21:00"
export DONT_FAKE_MONOTONIC=1
LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1 java -jar myapp.jar

ตัวแปรสภาพแวดล้อมที่สองเป็นสิ่งสำคัญยิ่งสำหรับแอ็พพลิเคชัน java ซึ่งมิฉะนั้นจะหยุดทำงาน ต้องใช้สาขาหลักของ libfaketime ในขณะที่เขียน

หากคุณต้องการเปลี่ยนเวลาของบริการที่มีการจัดการ systemd เพียงเพิ่มสิ่งต่อไปนี้ในการแทนที่ไฟล์หน่วยของคุณเช่นสำหรับ elasticsearch สิ่งนี้จะเป็น/etc/systemd/system/elasticsearch.service.d/override.conf:

[Service]
Environment="FAKETIME=2017-10-31 23:00:00"
Environment="DONT_FAKE_MONOTONIC=1"
Environment="LD_PRELOAD=/usr/local/lib/faketime/libfaketimeMT.so.1"

อย่าลืมโหลด systemd ใหม่โดยใช้ `systemctl daemon-reload


-1

หากคุณต้องการเยาะเย้ยเมธอดที่มีSystem.currentTimeMillis()อาร์กิวเมนต์คุณสามารถส่งanyLong()คลาส Matchers เป็นอาร์กิวเมนต์ได้

ป.ล. ฉันสามารถเรียกใช้กรณีทดสอบของฉันได้สำเร็จโดยใช้เคล็ดลับข้างต้นและเพียงเพื่อแบ่งปันรายละเอียดเพิ่มเติมเกี่ยวกับการทดสอบของฉันที่ฉันใช้กรอบงาน PowerMock และ Mockito

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