Опирается на правила:
TS-12…TS-14из Test Strategy Style Guide → раздел 4. TestObjectGenerator.
Важно знать
- На каждую POJO-таблицу — отдельный generator с
with*методами иgenerate().- Разумные дефолты — UUID, текущее время, default-статус.
- В тесте перезаписываем только то, что важно для сценария.
withNano(0)обязательно при сравненииOffsetDateTime(PG = ms, Java = ns).- Generator — не Spring bean, plain Java класс.
- Без generator-а каждый тест дублирует 10-15 строк инициализации POJO.
TestObjectGenerator — builder pattern для тестовых данных. Без него каждый тест содержит длинные new OrdersPojo(); pojo.setId(...); pojo.setStatus(...); pojo.setCustomerId(...); ... — на 50 тестов получаем огромный объём шума, важные различия теряются среди boilerplate.
Структура generator-а
TS-12: builder с fluent методами.
public class OrderTestObjectGenerator {
private UUID id = UUID.randomUUID();
private OrderStatus status = OrderStatus.DRAFT;
private UUID customerId = UUID.randomUUID();
private BigDecimal totalAmount = new BigDecimal("100.00");
private OffsetDateTime createdAt = OffsetDateTime.now(ZoneOffset.UTC).withNano(0);
private OffsetDateTime updatedAt = OffsetDateTime.now(ZoneOffset.UTC).withNano(0);
public OrderTestObjectGenerator withId(UUID id) {
this.id = id;
return this;
}
public OrderTestObjectGenerator withStatus(OrderStatus status) {
this.status = status;
return this;
}
public OrderTestObjectGenerator withCustomerId(UUID customerId) {
this.customerId = customerId;
return this;
}
public OrderTestObjectGenerator withTotalAmount(BigDecimal totalAmount) {
this.totalAmount = totalAmount;
return this;
}
public OrderTestObjectGenerator withCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
return this;
}
public OrdersPojo generate() {
var pojo = new OrdersPojo();
pojo.setId(id);
pojo.setStatus(status);
pojo.setCustomerId(customerId);
pojo.setTotalAmount(totalAmount);
pojo.setCreatedAt(createdAt);
pojo.setUpdatedAt(updatedAt);
return pojo;
}
}
Что важно:
- Plain Java, не Spring bean. Создаётся через
new OrderTestObjectGenerator(). - Mutable fields с дефолтами — fluent setter возвращает
this. generate()возвращает свежий POJO (можно создать несколько с разными настройками).with*методы для каждого поля, которое имеет смысл варьировать.
Разумные дефолты
TS-13: чтобы в тесте не задавать каждое поле.
private UUID id = UUID.randomUUID();
private OrderStatus status = OrderStatus.DRAFT;
private UUID customerId = UUID.randomUUID();
В тесте — только то, что важно:
@Test
void confirmOrder_whenDraft_returns200() {
var orderId = UUID.fromString("11111111-...");
given(uuidGenerator.generate()).willReturn(orderId);
var draft = new OrderTestObjectGenerator()
.withId(orderId)
.withStatus(OrderStatus.DRAFT)
.generate();
databasePreparer.createOrder(draft).prepare();
// ...
}
customerId, totalAmount, createdAt — не указаны, берутся дефолты. Тест читается: «есть order в статусе DRAFT с известным id».
Сравнение с дефолтами:
// КАТАСТРОФА — каждый field руками
var pojo = new OrdersPojo();
pojo.setId(orderId);
pojo.setStatus(OrderStatus.DRAFT);
pojo.setCustomerId(UUID.randomUUID());
pojo.setTotalAmount(new BigDecimal("100.00"));
pojo.setCreatedAt(OffsetDateTime.now().withNano(0));
pojo.setUpdatedAt(OffsetDateTime.now().withNano(0));
// ... 15 строк
databasePreparer.createOrder(pojo).prepare();
15 строк → 4 строки. Различающаяся часть видна сразу.
withNano(0)
TS-14: критично для сравнения времени.
private OffsetDateTime createdAt = OffsetDateTime.now(ZoneOffset.UTC).withNano(0);
Почему withNano(0):
PostgreSQL timestamp хранит миллисекунды (precision 6). Java OffsetDateTime.now() имеет наносекунды (precision 9).
Java: 2026-05-26T10:00:00.123456789Z
PostgreSQL: 2026-05-26T10:00:00.123456+00
After read: 2026-05-26T10:00:00.123456000Z ← наносекунды потеряны!
Если assertion expected.equals(actual):
var expected = OffsetDateTime.now(); // ns precision
databasePreparer.createOrder(...).prepare(); // saved as ms
var actual = orderRepository.findById(...).getCreatedAt(); // restored as ms
assertThat(actual).isEqualTo(expected); // FAIL: 123456789 != 123456000
С withNano(0):
Java: 2026-05-26T10:00:00.000Z (ms precision OK)
PostgreSQL: 2026-05-26T10:00:00.000+00 (ms)
After read: 2026-05-26T10:00:00.000Z
Equality работает. Без withNano(0) — flaky tests.
В тесте, когда явно задаём time:
var now = OffsetDateTime.parse("2026-05-26T10:00:00Z"); // без ns
given(dateTimeService.getCurrentDateTimeInUTC()).willReturn(now);
parse без ns — OK.
OffsetDateTime.now() — добавь .withNano(0) или .truncatedTo(ChronoUnit.MILLIS).
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
new OrdersPojo(); pojo.setX(); ... повсеместно | TS-12 | TestObjectGenerator с fluent builders |
OffsetDateTime.now() без .withNano(0) | TS-14 | .withNano(0) или .truncatedTo(MILLIS) |
| Generator как Spring bean | TS-12 | plain Java класс |
| Generator без разумных дефолтов | TS-13 | UUID/now/DEFAULT_STATUS |
| Один generator на все таблицы | TS-12 | per POJO |
withX(null) для clearing default | TS-13 | отдельный метод withoutX() |
generate() возвращает singleton — все тесты mutate один объект | TS-12 | каждый generate() создаёт новый |
| Generator зависит от Spring context | TS-12 | независимый класс, тестируемый отдельно |
Куда дальше
- Test Strategy → раздел 4. TestObjectGenerator — нормативные формулировки.
- DatabasePreparer — куда передаём
generate()-результат. - BaseIntegrationTest —
@MockitoBean DateTimeService. - Один тест — полный пример использования.
- JOOQ → repository — generated POJOs.
- PG schema —
timestampprecision.