Рик и Гертьян уже давно приглашают меня написать здесь на NetBeans DZone, и я виновен в том, что до сих пор не отреагировал на это.
В самом деле, я написал довольно плотную кодовую статью несколько месяцев назад (в то время для JavaLobby), но по некоторым причинам, о которых я вам сейчас не скажу, я не могу сейчас ее опубликовать.
Но теперь у меня есть кое-что, хотя это определенно более простая тема.
Недавно я написал интеграционный тест для компонента blueMarine, который импортирует метаданные из пакета фотографий и сохраняет их в базе данных. Затем тест импортирует базу данных в плоский файл после импорта и сравнивает его с ожидаемым дампом файла. Всякий раз, когда новая фотография добавляется в набор тестов или некоторые данные изменяются из-за исправления или другого подхода к его сохранению, я просто «принимаю» новый файл, копируя его поверх предыдущего ожидаемого файла.
Довольно просто, но с большой проблемой: временные метки. Фактически, один из столбцов содержит временную метку операции импорта, полученную с помощью
System.currentTimeMillis()
, и, конечно, возвращаемые значения различаются от запуска к запуску, что делает дамп несовместимым. Как это решить? Я собираюсь проиллюстрировать, что я сделал с некоторым кодом, специфичным для NetBeans RCP; в любом случае, идея может быть легко реализована, если вы используете другие технологии, такие как Spring.TimestampProvider
Ответ: издеваться над провайдером времени. Что ж, это подразумевает, что мне нужен поставщик времени, поскольку
System.currentTimeMillis()
, к сожалению, это статический метод, который, например, не может быть заменен полиморфизмом (это одна из причин, по которой статические методы считаются вредными — держитесь от них подальше!).Вот как
TimestampProvider
может выглядеть интерфейс:package it.tidalwave.metadata;
public interface TimestampProvider
{
public Date getTimestamp();
}
Поскольку я работаю с NetBeans RCP, я могу использовать механизм по умолчанию для регистрации служб: они должны быть объявлены в папках META-INF / services и получены с помощью Lookup. Итак, полный список для интерфейса:
package it.tidalwave.metadata;
public interface TimestampProvider
{
public Date getTimestamp();
public static final class Locator
{
private Locator()
{
}
public static TimestampProvider findTimestampProvider()
{
final TimestampProvider timestampProvider = Lookup.getDefault().lookup(TimestampProvider.class);
if (timestampProvider == null)
{
throw new RuntimeException("Cannot find TimestampProvider");
}
return timestampProvider;
}
}
}
Реализация может быть:
package it.tidalwave.metadata.impl;
import java.util.Date;
import it.tidalwave.metadata.TimestampProvider;
public class TimestampProviderImpl implements TimestampProvider
{
public Date getTimestamp()
{
return new Date();
}
}
А регистрация службы заключается только в создании файла META-INF / services / it.tidalwave.metadata.TimestampProvider, который содержит it.tidalwave.metadata.impl.TimestampProviderImpl.
На данный момент я просто должен был заменить все вхождения
System.currentTimeMillis()
сDate timestamp = TimestampProvider.Locator.findTimestampProvider().getTimestamp();
В ваших тестах вы можете, например, определить a,
MockTimestampProviderImpl
который предоставляет разные значения для времени. Чтобы зарегистрировать фиктивный сервис для тестов вместо стандартного сервиса, вам просто нужен новый файлMETA-INF/services/it.tidalwave.metadata.TimestampProvider
, на этот раз сохраненныйtest/unit/src
вместоsrc
: NetBeans будет использовать его только при запуске JUnit вместо того, чтобы помещать его в производственный дистрибутив. Содержимое файла:#-it.tidalwave.metadata.impl.TimestampProviderImpl
it.tidalwave.bluemarine.metadata.impl.MockTimestampProviderImpl
Хотя тире обычно является комментарием, комбинация тире + минус является расширением RCP NetBeans и означает, что вы хотите отменить регистрацию ранее зарегистрированной службы; затем замена предоставляется в строке ниже.
Образец и удерживайте
Хороший. Как издеваться над временем в тестах? Ну, во-первых, можно подумать о перезапуске времени с нуля или базовом времени по умолчанию в начале каждого запуска теста. В любом случае этого недостаточно, так как вы не можете гарантировать, что каждая операция будет выполняться в одно и то же время с точностью до миллисекунды! Черт, это не приложение реального времени, и время действительно может измениться по ряду причин,
в первую очередь из- за того факта, что вы также оптимизируете код, так что мы надеемся, что он будет выполняться быстрее и быстрее (не считая идеи запуска тестов на разные компьютеры и / или с разными загрузками процессора).
Единственное решение, которое я вижу, — это переключиться на «дискретную» модель на время. То есть каждый вызов getTimeStamp () может просто увеличивать время с постоянным значением. Например:
package it.tidalwave.bluemarine.metadata.impl;
import java.util.Date;
import it.tidalwave.metadata.TimestampProvider;
public class MockTimestampProviderImpl implements TimestampProvider
{
private Date previous;
public Date getTimestamp()
{
final Date now = (previous != null) ? new Date(previous.getTime() + 1000) : new Date(2008 - 1900, 0, 1);
date = previous;
return new Date(now.getTime());
}
}
Обратите внимание, что я клонирую
Date
перед возвращением, getTimestamp()
поскольку этот класс не является неизменным.
Это нормально? Еще нет. К сожалению, результаты зависят от того, сколько раз вы вызываете
getTimestamp()
из своего кода. Фактически, вы будете вынуждены хранить копию значения временной метки и распространять ее в других местах кода, нуждающихся в ней согласованным образом; но это привело бы к ненужным связям в коде. Подумав дважды, это не только проблема тестирования: это приводит к проблеме, пусть даже легкой, даже в производстве. Например, представьте себе набор записей, логически принадлежащих к одной и той же группе, которые могут быть независимо помечены временными метками с несколькими значениями во время вставки в базу данных:Было бы лучше, если бы временная метка для них была одинаковой:
Возможное решение состоит в том, чтобы ввести подход выборки и удержания: вы должны вызывать
sample()
метод в начале каждой логической группы операций, и возвращаемая временная метка будет одинаковой для любого последующегоgetTimeStamp()
вызова до следующегоsample()
. Можно даже подумать о звонкеsample()
в начале каждой обычной транзакции.
Многопоточность
Выполнено? Еще нет! Что делать, если у вас есть несколько потоков? Если в производственном коде вы используете технику выборки и удержания, привязанную к текущей транзакции, и у вас есть несколько параллельных транзакций из нескольких потоков, каждый вызов
sample()
изменит значения для всех потоков, как правило, в середине транзакции, которая не является хороший. Но даже если вы запускаете один тест и не заинтересованы в сохранении постоянной метки времени в какой-либо транзакции, у вас все равно могут быть проблемы: поскольку планирование потоков непредсказуемо, вы не можете гарантировать, что каждый поток будет проходить через одну и ту же последовательность изsample()
вызовов, при этом нет никакой гарантии , чтобы иметь постоянный результат теста.К счастью, эту проблему легко решить. Вместо сохранения текущего значения метки времени в простой ссылке вы можете использовать a
ThreadLocal
, который предлагает уникальное хранилище для каждого потока; другими словами, каждый поток в этом случае будет видеть свою собственную последовательность временных меток.Теперь, хотя временные метки для каждого потока, безусловно, являются лучшим решением в производственном коде, для тестов дела обстоят не так просто из-за необходимости идеальной воспроизводимости. Если каждый поток создает свой собственный набор данных, последовательности для потоков являются правильным выбором. Если потоки объединяют свои результаты в один пакет данных (например, представьте себе шаблон Master / Worker, который использует очередь данных для обработки, разрабатывает и заполняет базу данных), последовательности для каждого потока все еще в порядке, поскольку независимо от планирования потоков, каждый элемент данных будет обработан в том же порядке и получит одну и ту же метку времени. Но в более сложном случае все может быть не так просто.
Это финальные интерфейсы и реализации с подходом ThreadLocal, взятые из реальной вещи. Ради гибкости я отделил getTimestamp (), которая возвращает обычное время, от getSampledTimestamp ().
public interface TimestampProvider
{
public Date getTimestamp();
public Date getSampledTimestamp();
public Date sample();
public static final class Locator
{
private Locator()
{
}
public static TimestampProvider findTimestampProvider()
{
final TimestampProvider timestampProvider = Lookup.getDefault().lookup(TimestampProvider.class);
if (timestampProvider == null)
{
throw new RuntimeException("Cannot find TimestampProvider");
}
return timestampProvider;
}
}
}
public class TimestampProviderImpl
{
private final ThreadLocal<Date> dateHolder = new ThreadLocal<Date>();
public Date getTimestamp()
{
return new Date();
}
public Date getSampledTimestamp()
{
if (dateHolder.get() == null)
{
sample();
}
return new Date(dateHolder.get().getTime());
}
public Date sample()
{
final Date now = new Date();
dateHolder.set(now);
return new Date(now.getTime());
}
}
public class MockTimestampProviderImpl implements TimestampProvider
{
private final Date INITIAL_TIMESTAMP = new Date(2008 - 1900, 0, 1);
private final ThreadLocal<Date> dateHolder = new ThreadLocal<Date>();
public Date getTimestamp()
{
return new Date();
}
public Date getSampledTimestamp()
{
if (dateHolder.get() == null)
{
sample();
}
return new Date(dateHolder.get().getTime());
}
public Date sample()
{
final Date previous = dateHolder.get();
final Date now = (previous != null) ? new Date(previous.getTime() + 1000) : INITIAL_TIMESTAMP;
dateHolder.set(now);
return new Date(now.getTime());
}
}