不管你喜欢与否,偶尔你必须为内部使用计时器的类编写测试。
比如说一个获取系统可用性报告的类,如果系统停机时间过长会引发事件
public class SystemAvailabilityMonitor {
public event Action SystemBecameUnavailable = delegate { };
public event Action SystemBecameAvailable = delegate { };
public void SystemUnavailable() {
//..
}
public void SystemAvailable() {
//..
}
public SystemAvailabilityMonitor(TimeSpan bufferBeforeRaisingEvent) {
//..
}
}
我使用了一些技巧(将这些作为答案发布)但我想知道其他人做了什么,因为我对我的任何一种方法都不满意。
答案 0 :(得分:8)
我从响应警报的对象中提取计时器。例如,在Java中,您可以将其传递给ScheduledExecutorService。在单元测试中,我传递了一个我可以确定性地控制的实现,例如jMock's DeterministicScheduler。
答案 1 :(得分:4)
如果您正在寻找此问题的答案,您可能会对此博客感兴趣: http://thorstenlorenz.blogspot.com/2009/07/mocking-timer.html
在其中我解释了一种覆盖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);
}
}
当然,这要求您能够将计时器传递到被测试的类中。如果这是不可能的,那么在可测试性方面,类的设计是有缺陷的,因为它不支持依赖注入。 如果可以,你应该改变它的设计。否则你可能会运气不好,无法测试涉及其内部计时器的那个类。
如需更全面的解释,请访问博客。
答案 2 :(得分:3)
这就是我正在使用的。我在书中找到了它:测试驱动 - 实用TDD和Java开发人员接受TDD 作者:Lasse Koskela。
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毫秒的数字替换了TimeSource。这将作为开始时间。当调用setEndTime()时,我输入1020毫秒的结束时间。这给了我20毫秒的控制时间。
生产代码中没有测试代码,只是获得正常的Systemtime。
确保在测试后调用reset以恢复使用系统时间方法而不是伪造时间。
答案 3 :(得分:1)
听起来有人应该嘲笑计时器,但唉......快速的谷歌this other SO question之后有一些答案是最热门的搜索命中率。但后来我发现问题的概念是内部使用计时器的类,doh。无论如何,在进行游戏/引擎编程时 - 你有时会将定时器作为参考参数传递给构造函数 - 这会让我们再次模拟它们吗?但话说回来,我是编码器noob ^^
答案 4 :(得分:0)
我通常处理的方式是
答案 5 :(得分:0)
我重构这些,使得时间值是方法的参数,然后创建另一个方法,除了传递正确的参数之外什么都不做。这样,所有实际行为都是孤立的,并且可以在所有奇怪的边缘情况下轻松测试,只留下非常重要的参数插入未经测试。
作为一个非常简单的例子,如果我从这开始:
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;
}
答案 6 :(得分:0)
我意识到这是一个Java问题,但可能有兴趣展示它在Perl世界中的表现。您可以简单地覆盖测试中的核心时间函数。 :)这可能看起来很可怕,但这意味着你不必为了测试它而在生产代码中注入大量的额外间接。 Test::MockTime就是一个例子。测试中的冻结时间使一些事情变得容易多了。就像那些敏感的非原子时间比较测试一样,你在时间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");
}