Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/.net/20.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
.net 如何对内部使用计时器的类进行单元测试?_.net_Unit Testing_Testing_Timer - Fatal编程技术网

.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) {
        //..
    }
}

我有一些技巧可以使用(将这些作为答案发布),但我想知道其他人会怎么做,因为我对我的两种方法都不完全满意。

我通常采用的处理方法是

  • 将计时器设置为每100毫秒滴答一次,并计算到我的线程可能已经切换到100毫秒。这很尴尬,会产生一些不确定的结果
  • 将计时器的已用事件关联到公共或受保护的内部Tick()事件。然后在测试中,将计时器的间隔设置为非常大的值,并从测试中手动触发Tick()方法。这将为您提供确定性测试,但有些东西您无法使用这种方法进行测试

  • 我重构这些函数,使时间值成为该方法的参数,然后创建另一个方法,该方法只传递正确的参数。这样,所有的实际行为都是隔离的,并且可以在所有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()的经过时间。