【问题标题】:How do you unit test classes that use timers internally?您如何对内部使用计时器的类进行单元测试?
【发布时间】:2009-07-02 21:03:42
【问题描述】:

不管你喜不喜欢,有时你必须为在内部使用定时器的类编写测试。

例如,一个类会报告系统可用性并在系统停机时间过长时引发事件

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

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

【问题讨论】:

    标签: .net unit-testing testing timer


    【解决方案1】:

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

    【讨论】:

    • 是的,某种双重调度(我认为?)将是理想的方法,太糟糕了 .NET 让这样做很麻烦。我有时会推出自己的计时器界面,但总是觉得我在引入复杂性。
    • 听起来更像是依赖注入,而不是双重分派给我。
    • 轻微的术语狡辩,但我认为将其称为双重派遣或访客是正确的。它的 DI 是肯定的,但您还获取了一个对象并告诉它将其更新策略应用于“this”。
    • 还不错,没想到是这样的……感谢开导! :)
    【解决方案2】:

    如果您正在寻找这个问题的答案,您可能会对这个博客感兴趣: 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);
    }
    }
    

    当然,这要求您能够将计时器传递给被测类。如果这是不可能的,那么类的设计在可测试性方面存在缺陷,因为它不支持依赖注入。 如果可以的话,你应该改变它的设计。否则,您可能会运气不好,并且无法测试该类涉及其内部计时器的任何内容。

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

    【讨论】:

    • 只是提醒任何喜欢这个想法的人,如果你用 AutoReset = false 模拟一个计时器并从你的 Elapsed 委托调用 Start() (重新启动计时器),那么你'会有一个螺旋向 StackOverflowException。我通过在我的模拟计时器中创建一个 Fire() 方法来解决这个问题,以独立于 Start() 调用 Elapsed。
    【解决方案3】:

    这就是我正在使用的。我在这本书中找到了它:Test Driven - Practical TDD and Acceptance TDD for Java Developers,作者 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 以恢复使用系统时间方法而不是伪造的时间。

    【讨论】:

      【解决方案4】:

      听起来应该嘲笑计时器,但唉......在快速谷歌this other SO question 和一些答案之后是热门搜索。但后来我发现问题是关于在内部使用计时器的类的概念,doh。无论如何,在进行游戏/引擎编程时 - 您有时将计时器作为参考参数传递给构造函数 - 我猜这会使模拟它们再次成为可能?但话又说回来,我是编码新手^^

      【讨论】:

      • 不,你是对的,理想的方法是传入一个计时器对象,唯一的问题是这打破了一些 .NET 框架约定,并且在某些事情上感觉很重(现在我例如,必须使用另一个对象配置我的 IoC 容器)
      【解决方案5】:

      我通常处理这种情况的方法是

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

      【讨论】:

        【解决方案6】:

        我重构这些,使时间值成为该方法的参数,然后创建另一个除了传递正确参数之外什么都不做的方法。这样一来,所有实际行为都是隔离的,并且可以在所有奇怪的边缘情况下轻松测试,只留下非常微不足道的参数插入未经测试。

        作为一个极其微不足道的例子,如果我从这个开始:

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

        【讨论】:

        • 我不确定我明白了,这如何涉及计时器?
        【解决方案7】:

        我知道这是一个 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");
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多