如何对内部使用计时器的类进行单元测试?

时间:2009-05-18 21:37:38

标签: .net unit-testing testing timer

不管你喜欢与否,偶尔你必须为内部使用计时器的类编写测试。

比如说一个获取系统可用性报告的类,如果系统停机时间过长会引发事件

public class SystemAvailabilityMonitor {
    public event Action SystemBecameUnavailable = delegate { };
    public event Action SystemBecameAvailable = delegate { };
    public void SystemUnavailable() {
        //..
    }
    public void SystemAvailable() {
        //..
    }
    public SystemAvailabilityMonitor(TimeSpan bufferBeforeRaisingEvent) {
        //..
    }
}

我使用了一些技巧(将这些作为答案发布)但我想知道其他人做了什么,因为我对我的任何一种方法都不满意。

7 个答案:

答案 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)

我通常处理的方式是

  1. 将计时器设置为打勾100毫秒,然后确定我的线程可能已切换到那时。这很尴尬,并产生一些不确定的结果。
  2. 将计时器的已用事件连接到公共或受保护的内部Tick()事件。然后从测试中将计时器的间隔设置为非常大的值,并从测试中手动触发Tick()方法。这为您提供了确定性测试,但有些事情您无法使用此方法进行测试。

答案 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");
}