Опирается на правила: TS-12TS-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-12TestObjectGenerator с fluent builders
OffsetDateTime.now() без .withNano(0)TS-14.withNano(0) или .truncatedTo(MILLIS)
Generator как Spring beanTS-12plain Java класс
Generator без разумных дефолтовTS-13UUID/now/DEFAULT_STATUS
Один generator на все таблицыTS-12per POJO
withX(null) для clearing defaultTS-13отдельный метод withoutX()
generate() возвращает singleton — все тесты mutate один объектTS-12каждый generate() создаёт новый
Generator зависит от Spring contextTS-12независимый класс, тестируемый отдельно

Куда дальше