Статьи

Время издевательства с RCP NetBeans


Рик и Гертьян уже давно приглашают меня написать здесь на 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());
}
}