.net 如何对内部使用计时器的类进行单元测试?
不管你喜欢与否,有时候你不得不为内部使用定时器的类编写测试 例如,假设一个类获取系统可用性报告,并在系统停机时间过长时引发事件.net 如何对内部使用计时器的类进行单元测试?,.net,unit-testing,testing,timer,.net,Unit Testing,Testing,Timer,不管你喜欢与否,有时候你不得不为内部使用定时器的类编写测试 例如,假设一个类获取系统可用性报告,并在系统停机时间过长时引发事件 public class SystemAvailabilityMonitor { public event Action SystemBecameUnavailable = delegate { }; public event Action SystemBecameAvailable = delegate { }; public void Sys
public class SystemAvailabilityMonitor {
public event Action SystemBecameUnavailable = delegate { };
public event Action SystemBecameAvailable = delegate { };
public void SystemUnavailable() {
//..
}
public void SystemAvailable() {
//..
}
public SystemAvailabilityMonitor(TimeSpan bufferBeforeRaisingEvent) {
//..
}
}
我有一些技巧可以使用(将这些作为答案发布),但我想知道其他人会怎么做,因为我对我的两种方法都不完全满意。我通常采用的处理方法是
我重构这些函数,使时间值成为该方法的参数,然后创建另一个方法,该方法只传递正确的参数。这样,所有的实际行为都是隔离的,并且可以在所有wierd边缘情况下轻松地进行测试,只留下非常琐碎的参数插入未经测试 作为一个非常简单的例子,如果我从以下内容开始:
public long timeElapsedSinceJan012000()
{
Date now = new Date();
Date jan2000 = new Date(2000, 1, 1); // I know...deprecated...bear with me
long difference = now - jan2000;
return difference;
}
我将对此进行重构,并对第二种方法进行单元测试:
public long timeElapsedSinceJan012000()
{
return calcDifference(new Date());
}
public long calcDifference(Date d) {
Date jan2000 = new Date(2000, 1, 1);
long difference = d - jan2000;
return difference;
}
听起来你应该嘲笑计时器,但唉。。。在快速搜索了一些答案后,谷歌成为搜索热门。但后来我发现问题的概念是关于在内部使用计时器的类,doh。无论如何,在进行游戏/引擎编程时——有时您会将计时器作为参考参数传递给构造函数——我想这会使再次模拟它们成为可能吗?但是,我是编码器noob^^我从对警报做出反应的对象中提取计时器。例如,在Java中,您可以向它传递ScheduledExecutorService。在单元测试中,我向它传递一个我可以确定控制的实现,例如。这就是我正在使用的。我在Lasse Koskela的书中找到了它:测试驱动-Java开发人员的实用TDD和验收TDD
public interface TimeSource {
long millis();
}
public class SystemTime {
private static TimeSource source = null;
private static final TimeSource DEFAULTSRC =
new TimeSource() {
public long millis() {
return System.currentTimeMillis();
}
};
private static TimeSource getTimeSource() {
TimeSource answer;
if (source == null) {
answer = DEFAULTSRC;
} else {
answer = source;
}
return answer;
}
public static void setTimeSource(final TimeSource timeSource) {
SystemTime.source = timeSource;
}
public static void reset() {
setTimeSource(null);
}
public static long asMillis() {
return getTimeSource().millis();
}
public static Date asDate() {
return new Date(asMillis());
}
}
请注意,默认时间源DEFAULTSRC是System.currentTimeMillis()。它在单元测试中被替换;但是,正常行为是标准系统时间
这是使用它的地方:
public class SimHengstler {
private long lastTime = 0;
public SimHengstler() {
lastTime = SystemTime.asMillis(); //System.currentTimeMillis();
}
}
下面是单元测试:
import com.company.timing.SystemTime;
import com.company.timing.TimeSource;
public class SimHengstlerTest {
@After
public void tearDown() {
SystemTime.reset();
}
@Test
public final void testComputeAccel() {
// Setup
setStartTime();
SimHengstler instance = new SimHengstler();
setEndTime(1020L);
}
private void setStartTime() {
final long fakeStartTime = 1000L;
SystemTime.setTimeSource(new TimeSource() {
public long millis() {
return fakeStartTime;
}
});
}
private void setEndTime(final long t) {
final long fakeEndTime = t; // 20 millisecond time difference
SystemTime.setTimeSource(new TimeSource() {
public long millis() {
return fakeEndTime;
}
});
}
在单元测试中,我用一个设置为1000毫秒的数字替换了时间源。这将作为开始时间。调用setEndTime()时,我输入1020毫秒作为结束时间。这给了我一个受控的20毫秒时差
生产代码中没有测试代码,只是获得正常的Systemtime
请确保在测试后调用reset,以重新使用系统时间方法,而不是伪造的时间。我意识到这是一个Java问题,但展示一下它在Perl世界中是如何完成的可能很有意思。您可以简单地覆盖测试中的核心时间函数。:)这可能看起来很可怕,但这意味着您不必在生产代码中注入大量额外的间接操作来测试它。这是一个例子。在考试中冻结时间会使一些事情变得容易得多。就像那些敏感的非原子时间比较测试,你在时间X运行某个东西,然后检查它的X+1。下面的代码中有一个示例 更传统的是,我最近有一个PHP类从外部数据库中提取数据。我希望它最多每X秒发生一次。为了测试它,我将上次更新时间和更新时间间隔作为对象的属性。我最初将它们设置为常量,因此这一测试更改也改进了代码。然后,测试可以像这样处理这些值:
function testUpdateDelay() {
$thing = new Thing;
$this->assertTrue($thing->update, "update() runs the first time");
$this->assertFalse($thing->update, "update() won't run immediately after");
// Simulate being just before the update delay runs out
$just_before = time() - $thing->update_delay + 2;
$thing->update_ran_at = $just_before;
$this->assertFalse($thing->update, "update() won't run just before the update delay runs out");
$this->assertEqual($thing->update_ran_at, $just_before, "update_ran_at unchanged");
// Simulate being just after
$just_after = time() - $thing->update_delay - 2;
$thing->update_ran_at = $just_after;
$this->assertTrue($thing->update, "update() will run just after the update delay runs out");
// assertAboutEqual() checks two numbers are within N of each other.
// where N here is 1. This is to avoid a clock tick between the update() and the
// check
$this->assertAboutEqual($thing->update_ran_at, time(), 1, "update_ran_at updated");
}
如果您正在寻找此问题的答案,您可能会对以下博客感兴趣: 在本文中,我解释了一种重写System.Timers.Timer类的常规行为的方法,以使其在Start()时启动 以下是简短的版本:
class FireOnStartTimer : System.Timers.Timer
{
public new event System.Timers.ElapsedEventHandler Elapsed;
public new void Start()
{
this.Elapsed.Invoke(this, new EventArgs() as System.Timers.ElapsedEventArgs);
}
}
当然,这需要您能够将计时器传递到被测试的类中。如果这是不可能的,那么类的设计在可测试性方面是有缺陷的,因为它不支持依赖注入。
如果可以的话,你应该改变它的设计。否则,您可能会运气不佳,无法测试有关该类的任何涉及其内部计时器的内容
要获得更全面的解释,请访问博客。我不确定我是否了解,这是如何涉及计时器的?是的,某种双重调度(我认为?)将是理想的方法,但糟糕的是.NET会让这样做很麻烦。我有时会滚动我自己的计时器接口,但总觉得我在引入复杂性。对我来说,听起来更像是依赖注入而不是双重分派。次要的术语含糊其辞,但我认为称之为双重分派或访问者是正确的。这是肯定的,但你也拿了一个对象,并告诉它将其更新策略应用于“this”。很公平,我没有这样想过。。。谢谢你的启示!:)不,你是对的,理想的方法是传入一个timer对象,唯一的问题是这打破了一些.NET framework惯例,在某些事情上感觉非常沉重(现在我必须用另一个对象配置我的IoC容器),这是对其他喜欢这个想法的人的提醒,如果您使用AutoReset=false模拟计时器,并从已用委托调用Start()(重新启动计时器),那么您将有一个朝向StackOverflowException的螺旋。我通过在模拟计时器中创建一个Fire()方法来解决这个问题,该方法调用独立于Start()的经过时间。