Тестирование
Настоящий PostgreSQL, одноразовые базы и никакого мокинга SQL.
Позиция#
Не мокайте базу. Мок SQL-драйвера проверяет, что вы умеете писать моки; а то, что ломается в проде — ограничения, типы, семантика NULL, изоляция транзакций, — ровно то, о чём у мока нет мнения.
Всё здесь сделано так, чтобы запускаться против настоящего PostgreSQL и чтобы это было дёшево.
База на тестовый бинарник#
ormtest не создаёт базы. Он даёт то, что делает настоящую базу удобной:
применение миграций, проверку, что схема — это та, которую описывает ваш
конфиг, и запуск теста внутри транзакции, которая всегда откатывается.
import "github.com/AlexAli29/orm/ormtest"
func TestMain(m *testing.M) {
conn, _ := pgx.Connect(ctx, os.Getenv("TEST_DSN"))
ormtest.Migrate(t, conn, "migrations") // применить закоммиченные артефакты
os.Exit(m.Run())
}ormtest.CheckSchema(ctx, "orm.yaml") — та проверка, которую стоит поставить в
CI: она падает, когда база не соответствует схеме из деклараций, а иначе это
всплывает как странно сломавшийся посторонний тест.
RequireSchemaClean — та же проверка в виде жёсткого требования.
Контейнеры#
import ormpg "github.com/AlexAli29/orm/ormtest/postgres"
func TestMain(m *testing.M) {
ormpg.Run(m, ormpg.WithImage("postgres:17"))
}Отдельный модуль, потому что Testcontainers — тяжёлая зависимость, и проект, у которого PostgreSQL уже есть, платить за неё не должен.
Очистка таблиц между тестами#
Truncate живёт в ormtest, а не в API запросов, потому что очистка таблицы —
это операция фикстуры, а не то, что делает приложение:
import "github.com/AlexAli29/orm/ormtest"
ormtest.MustTruncate(t, pool, domain.Users, domain.Orders)
err := ormtest.TruncateWith(ctx, pool,
[]ormtest.TruncateOption{ormtest.RestartIdentity(), ormtest.Cascade()},
domain.Users,
)Она принимает сгенерированные ручки таблиц напрямую. RestartIdentity сбрасывает
последовательности identity — обычно этого фикстура и хочет; Cascade идёт по
внешним ключам и опустошит таблицы, которых вы не называли, поэтому включается
явно.
Очистка нескольких таблиц одним вызовом — не удобство: это единственный способ
опустошить таблицы, ссылающиеся друг на друга, без Cascade.
Транзакция на тест#
Быстро и изолированно, без удаления чего-либо:
func TestSomething(t *testing.T) {
ormtest.TxFunc(t, pool, func(ex orm.Executor) {
db := domain.New(ex)
// ... тест ... транзакция откатывается при выходе
})
}Проверка SQL без базы#
sql, args, err := q.SQL()
if !strings.Contains(sql, "LEFT JOIN") { /* ... */ }Полезно для формы запроса. Не замена запуску: SQL, который выглядит правильным и возвращает не те строки, — это тот самый режим отказа, против которого выстроен весь проект.
Что должно быть в CI#
orm makemigrations --check # декларация без миграции
orm check --generated # сгенерированный код разошёлся
go test -race ./...Тесты на всех мажорах#
Собственный набор совместимости проекта отказывается запускаться меньше чем на всех пяти поддерживаемых мажорах, и идею стоит позаимствовать: матрица, тихо отработавшая на том сервере, который случайно был поднят, доказывает меньше, чем заявляет.
strategy:
matrix:
postgres: ['14', '15', '16', '17', '18']Разобранные примеры#
Тест, который ничего после себя не оставляет#
func TestPlaceOrder(t *testing.T) {
ormtest.TxFunc(t, pool, func(ex orm.Executor) {
db := domain.New(ex)
customer, err := db.Customers.Insert(t.Context(), Customer{Email: "a@example.com"})
if err != nil {
t.Fatal(err)
}
order, err := db.Orders.Insert(t.Context(), Order{CustomerID: customer.ID})
if err != nil {
t.Fatal(err)
}
if order.ID == 0 {
t.Error("the insert returned no key")
}
})
}Всё откатывается при выходе из колбэка, поэтому тесты можно запускать в любом порядке и ни один не видит строк другого.
Сброс фикстур между наборами#
func resetFixtures(t *testing.T, pool *pgxpool.Pool) {
ormtest.MustTruncate(t, pool, domain.OrderLines, domain.Orders, domain.Customers)
}Перечисление таблиц одним вызовом и позволяет им ссылаться друг на друга без
Cascade.
Проверка, что схема — та, которую вы объявили#
func TestMain(m *testing.M) {
if err := ormtest.CheckSchema(context.Background(), "orm.yaml"); err != nil {
log.Fatalf("the test database is not the declared schema: %v", err)
}
os.Exit(m.Run())
}Это превращает «тест странно упал» в «база отстала на миграцию», а это гораздо более короткий разбор.
Проверка запроса без базы#
sql, args, err := db.Orders.Query().
Where(Orders.CustomerID.Eq(7)).
OrderBy(Orders.PlacedAt.Desc()).
SQL()
if !strings.Contains(sql, "ORDER BY") {
t.Error("the ordering was dropped")
}
if len(args) != 1 {
t.Errorf("args = %d, want the customer id as a parameter", len(args))
}Полезно для формы. Не замена запуску: SQL, который выглядит правильным и возвращает не те строки, — тот самый режим отказа, против которого выстроен весь проект.