2009-05-18 32 views
10

不管你喜欢与否,偶尔你都必须为内部使用定时器的类编写测试。你如何对内部使用定时器的类进行单元测试?

说,例如一类,它的系统可用性报告,并引发一个事件如果系统已经下过久

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

我有我使用(将张贴这些作为回答的几个技巧)但我不知道其他人做了什么,因为我对我的任何一种方法都不满意。

回答

8

我从对警报作出反应的对象提取计时器。例如,在Java中,您可以将它传递给ScheduledExecutorService。在单元测试中,我传递了一个可以确定性控制的实现,例如jMock's DeterministicScheduler

+0

是的,某种双重调度(我认为?)将是理想的方法,太糟糕的.NET使得这样做很麻烦。我有时会推出自己的定时器界面,但总是觉得我正在引入复杂性。 – 2009-05-18 21:52:57

0

,我通常处理这种情况的办法是要么

  1. 设置计时器滴答以往100毫秒并计算其可能我的线程将一直然后切换到。这很尴尬,并产生了一些不确定的结果。
  2. 将计时器已过去的事件连接到公共或受保护的内部Tick()事件。然后从测试中将计时器的间隔设置为非常大的值,并从测试中手动触发Tick()方法。这给你确定性的测试,但有些东西你无法用这种方法测试。
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; 
} 
+1

我不知道我明白了,这是如何涉及计时器? – 2009-05-18 21:47:09

1

听起来像是一个应该嘲笑计时器但唉...在一些快速Google this other SO question有一些答案是最热门的搜索命中。但后来我发现这个问题的概念是关于内部使用定时器的类,doh。无论如何,当进行游戏/引擎编程时 - 你有时会把定时器作为参考参数传递给构造函数 - 这会让我再次嘲笑它们。但是,再次,我是代码noob ^^

+0

不,你是对的,理想的做法是传递一个计时器对象,唯一的问题是这打破了一些.NET框架的约定,并且对于某些事情感觉很糟糕(现在我必须配置IoC容器另一个对象,例如) – 2009-05-18 22:00:49

3

这就是我正在使用的。我在书中找到它:测试驱动 - 用于Java开发人员的实用TDD和验收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; 
      } 
     }); 
    } 

在单元测试中,我更换了TIMESOURCE只有这为1000多家毫秒。这将作为开始时间。当调用setEndTime()时,我输入1020毫秒的结束时间。这给了我一个20毫秒的时间差。

生产代码中没有测试代码,只是得到正常的系统时间。

确保在测试后调用重置以恢复使用系统时间方法而不是假时间。

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

当然,这需要你能够定时传递到类测试。如果这是不可能的,那么就可测试性而言,该类的设计是有缺陷的,因为它不支持依赖注入。 如果可以的话,你应该改变它的设计。否则,你可能会失败,不能测试任何涉及其内部计时器的课程。

如需更详细的解释,请访问博客。

相关问题