How do you unit test classes that use timers inter

2019-02-12 11:56发布

Like it or not, occasionally you have have to write tests for classes that make internal use of timers.

Say for example a class that takes reports of system availability and raises an event if the system has been down for too long

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

I have a couple tricks which I use (will post these as an answer) but I wonder what other people do since I'm not fully satisfied with either of my approaches.

7条回答
女痞
2楼-- · 2019-02-12 12:29

If you are looking for answers to this problem, you might be interested in this blog: http://thorstenlorenz.blogspot.com/2009/07/mocking-timer.html

In it I explain a way to override the usual behavior of a System.Timers.Timer class, to make it fire on Start().

Here is the short version:

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

Of course this requires you to be able to pass the timer into the class under test. If this is not possible, then the design of the class is flawed when it comes to testability since it doesn't support dependency injection. You should change its design if you can. Otherwise you could be out of luck and not be able to test anything about that class which involves its internal timer.

For a more thorough explanation visit the blog.

查看更多
3楼-- · 2019-02-12 12:36

Sounds like one should mock the timer but alas... after a quick Google this other SO question with some answers was the top search hit. But then I caught the notion of the question being about classes using timers internally, doh. Anyhow, when doing game/engine programming - you sometimes pass the timers as reference parameters to the constructors - which would make mocking them possible again I guess? But then again, I'm the coder noob ^^

查看更多
Anthone
4楼-- · 2019-02-12 12:36

The ways that I usually handle this are either

  1. Set the timer to tick ever 100 milliseconds and figure that its likely my thread will have been switched to by then. This is awkward and produces somewhat indeterministic results.
  2. Wire the timer's elapsed event to a public or protected internal Tick() event. Then from the test set the timer's interval to something very large and trigger the Tick() method manually from the test. This gives you deterministic tests but there's some things that you just can't test with this approach.
查看更多
Fickle 薄情
5楼-- · 2019-02-12 12:45

I refactor these such that the temporal value is a parameter to the method, and then create another method that does nothing but pass the correct parameter. That way all the actual behavior is isolated and easily testable on all the wierd edge cases leaving only the very trivial parameter insertion untested.

As an extremely trivial example, if I started with this:

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

I would refactor to this, and unit test the second method:

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;
}
查看更多
Ridiculous、
6楼-- · 2019-02-12 12:45

I realize this is a Java question, but it may be of interest to show how its done in the Perl world. You can simply override the core time functions in your tests. :) This may seem horrifying, but it means you don't have to inject a whole lot of extra indirection into your production code just to test it. Test::MockTime is one example. Freezing time in your test makes some things a lot easier. Like those touchy non-atomic time comparison tests where you run something at time X and by the time you check its X+1. There's an example in the code below.

A bit more conventionally, I recently had a PHP class to pull data from an external database. I wanted it to happen at most once every X seconds. To test it I put both the last update time and the update time interval as attributes of the object. I'd originally made them constants, so this change for testing also improved the code. Then the test could fiddle with those values like so:

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");
}
查看更多
Fickle 薄情
7楼-- · 2019-02-12 12:50

I extract the timer from the object that reacts to the alarm. In Java you can pass it a ScheduledExecutorService, for example. In unit tests I pass it an implementation that I can control deterministically, such as jMock's DeterministicScheduler.

查看更多
登录 后发表回答