На днях писал тесты для модуля, который взаимодействует с базой данных. Привязывать модуль тестов к настоящей базе данных не хотелось — это создаёт дополнительные требования к окружению, где будет выполняться тестирование. Создавать сразу экземпляры типа sql.Rows
, с нужными табличками также не хотелось — для меня этот тип "чёрный ящик", и хотелось бы, чтобы так оно и оставалось. Поиском по теме ничего интересного также не нашлось.
Захотел поделиться с сообществом своей находкой: в основной поставке Go, есть почти готовый инструмент для таких нужд: называется FakeDb.
Что умеет данный инструмент?
По сути это простейшая, но полноценная как объект DB, база данных, умеющая создавать таблицы, наполнять их и делать из них простейшие выборки. Все созданные таблицы временные и существуют, пока существует экземпляр FakeDb. Кроме того, доступна эмуляция задержки при вызове команды и эмуляция исключения.
Команды FakeDb
Команды состоят из фраз, разделённых вертикальной чертой — |
. Фраза — это или ключевое слово, или выражение вида ключ=значение
. Где ключ — это имя столбца.
WIPE
Команда уничтожает таблицы и возвращает пустой результат. Кроме того это проверка, что нет взаимоблокировки.
Пример вызова:
WIPE
CREATE
Команда создаёт таблицу с указанными столбцами указанного типа. Формат вызова:
CREATE|Имя_таблицы|Столбец1=Тип,...,СтолбецN=Тип
Поддерживаемые типы:
- bool
- nullbool — булево или NULL
- int32
- string
- nullstring — строка или NULL
- int64
- nullint64 — int64 или NULL
- float64
- nullfloat64 — float64 или NULL
- datetime
- any — пустой интерфейс, т. е. любой тип
Пример вызова:
CREATE|people|name=string,age=int32,photo=any,dead=bool,bdate=datetime
INSERT
Команда добавляет строки в созданную таблицу.
Формат вызова:
INSERT|Имя_таблицы|Столбец1=Значение,...,СтолбецN
Здесь и далее значение можно задать тремя способами:
- Явно указать значение, например
123
. - Указать
?
, а значение передать параметром. - Указать
?имя_параметра
и значение передать именованным параметром.
Пример вызова:
INSERT|people|name=Alice,age=?,photo=?photo
SELECT
Команда позволяет выбрать или все строки таблицы, или с некоторым отбором.
Формат вызова:
SELECT|Имя_таблицы|Столбец1,...,СтолбецN|
SELECT|Имя_таблицы|Столбец1,...,СтолбецN|ОтборПоСтолбцу1=?,...,ОтборПоСтолбцуM
Пример вызова:
SELECT|categories|category_id,category_parent_id,category_name|
SELECT|categories|category_id,category_parent_id,category_name|category_parent_id=?parent
В результате возвращается полновесный экземпляр sql.Rows.
WAIT
Имитирует задержку при выполнении команды. Необходимо добавить перед любой указанной выше командой.
Формат вызова:
WAIT|Задержка|Команда
Задержка указывается как количество и постфикс, обозначающий единицу измерения: s — секунды, n — наносекунды, u — микросекунды, h — часы, и так далее (используется функция time.ParseDuration()
).
Пример вызова:
WAIT|1s|SELECT|categories|category_id,category_parent_id,category_name|
PANIC
Генерирует исключение при вызове команды.
Формат вызова:
PANIC|Имя_метода|Команда
Значение Имя_метода
будет помещено в поле stmt.panic
(тип fakeStmt
).
Пример вызова:
PANIC|method_name|SELECT|categories|category_id,category_parent_id,category_name|
Использование
FakeDb создавался для тестирования пакета "sql" основной поставки в виде скрипта тестирования. В виде отдельного пакета я его не нашёл. Поэтому самостоятельно выделил в пакет и разместил здесь: gihub.
Для выделения в пакет потребовались незначительные правки.
Пример использования
package packname
import (
"fmt"
"testing"
"io/ioutil"
"database/sql"
"github.com/a1div0/fakedb"
)
func TestFoo(t *testing.T) {
fc := &fakedb.FakeConnector{ }
db := sql.OpenDB(fc)
if db.Driver() != fakedb.Fdriver {
t.Error("OpenDB should return the driver of the Connector")
return
}
if _, err := db.Exec("WIPE"); err != nil {
t.Error("exec wipe: %v", err)
}
defer db.Close()
db.Exec("CREATE|users|user_email=string,user_id=int64")
db.Exec("INSERT|users|user_email=?,user_id=?", "test@email.com", 345)
rows, err := db.Query(
"SELECT|users|user_id|user_email=?email",
sql.Named("email", user_email),
)
if err != nil {
t.Error(err)
}
result, err := Foo(rows) // тестируемая функция
if err != nil {
t.Error(err)
}
if result != 123 {
t.Error("Торжественно сообщаем, что функция Foo() не работает ;)")
}
}