Как вы тестируете классы, которые используют таймеры внутри? - PullRequest
10 голосов
/ 19 мая 2009

Нравится вам это или нет, иногда вам приходится писать тесты для классов, которые используют таймеры для внутреннего использования.

Скажем, например, класс, который принимает отчеты о доступности системы и выдает событие, если система слишком долго не работает

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

У меня есть пара трюков, которые я использую (опубликую их как ответ), но мне интересно, что делают другие люди, поскольку я не полностью удовлетворен ни одним из моих подходов.

Ответы [ 7 ]

8 голосов
/ 19 мая 2009

Я извлекаю таймер из объекта, который реагирует на сигнал тревоги. Например, в Java вы можете передать ему ScheduledExecutorService. В модульных тестах я передаю реализацию, которой могу управлять детерминистически, например, DeterministicScheduler jMock .

4 голосов
/ 02 июля 2009

Если вы ищете ответы на эту проблему, вас может заинтересовать этот блог: 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);
}
}

Конечно, это требует, чтобы вы могли передавать таймер в тестируемый класс. Если это невозможно, то дизайн класса имеет недостатки, когда речь заходит о тестируемости, поскольку он не поддерживает внедрение зависимостей. Вы должны изменить его дизайн, если можете. В противном случае вам может не повезти, и вы не сможете протестировать что-либо об этом классе, включая внутренний таймер.

Для более подробного объяснения посетите блог.

3 голосов
/ 20 мая 2009

Это то, что я использую. Я нашел это в книге: Test Driven - Практический TDD и TDD Acceptance для разработчиков Java Лассе Коскела.

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 миллисекунд.

В рабочем коде нет тестового кода, просто получается обычное системное время.

Обязательно вызовите сброс после тестирования, чтобы вернуться к использованию метода системного времени, а не фальсифицированного времени.

1 голос
/ 19 мая 2009

Звучит так, будто надо издеваться над таймером, но, увы ... после быстрого Google этот другой ТАК вопрос с некоторыми ответами стал хитом поиска. Но потом я понял, что вопрос о классах, использующих таймеры внутри, дох. В любом случае, когда вы программируете игру / движок - вы иногда передаете таймеры в качестве опорных параметров конструкторам - что, я думаю, сделало бы их насмешку снова? Но опять же, я кодер Noob ^^

0 голосов
/ 25 мая 2009

Я понимаю, что это вопрос 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");
}
0 голосов
/ 19 мая 2009

Я реорганизую их так, что временное значение является параметром метода, а затем создаю другой метод, который ничего не делает, кроме передачи правильного параметра. Таким образом, все действительное поведение будет изолированным и легко проверяемым во всех случаях с самыми острыми краями, оставляя только очень тривиальную вставку параметров без проверки.

В качестве чрезвычайно тривиального примера, если бы я начал с этого:

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;
}
0 голосов
/ 19 мая 2009

Способы, которыми я обычно занимаюсь, это либо

  1. Установите таймер на каждые 100 миллисекунд и предположите, что, вероятно, к тому времени мой поток будет переключен. Это неловко и дает несколько неопределенные результаты.
  2. Свяжите прошедшее событие таймера с открытым или защищенным внутренним событием Tick (). Затем из теста установите интервал таймера на что-то очень большое и запустите метод Tick () вручную из теста. Это дает вам детерминированные тесты, но есть некоторые вещи, которые вы просто не можете протестировать с помощью этого подхода.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...