Никто не любит писать тесты, конечно же я шучу, все обожают их писать. Как подскажут тимлиды и HR, на собеседованиях правильный ответ — я очень люблю и пишу тесты. Но вдруг вы любите писать тесты на другом языке. Как же начать писать покрытый тестами код на го?
Часть 1. Тестируем handler
В go из коробки есть поддержка http server в «net/http», так что поднять его можно без каких либо усилий. Открывшиеся возможности позволяют почувствовать себя крайне могущественным, и поэтому наш код будет возвращать 42’ого пользователя.
func userHandler(w http.ResponseWriter, r *http.Request) { var user User userId, err := strconv.Atoi(r.URL.Query().Get("id")) if err != nil { w.Write([]byte( "Error")) return } if userId == 42 { user = User{userId, "Jack", 2} } jsonData, _ := json.Marshal(user) w.Write(jsonData) } type User struct { Id int Name string Rating uint }
Данный код получает на вход параметр id пользователя, дальше эмулирует его наличие в базе, и возвращает. Дальше надо это тестировать…
Есть прекрасная вещь «net/http/httptest», она позволяет имитировать вызов нашего handler’a и затем сравнить ответ.
r := httptest.NewRequest("GET", "http://127.0.0.1:80/user?id=42", nil) w := httptest.NewRecorder() userHandler(w, r) user := User{} json.Unmarshal(w.Body.Bytes(), &user) if user.Id != 42 { t.Errorf("Invalid user id %d expected %d", user.Id, 42) }
Часть 2. Дорогая, у нас тут внешний API
И зачем нам переводить дыхание, если мы только размялись? Внутри наших сервисов рано или поздно появятся внешние api. Это странный часто прячущийся зверь, который может вести себя как угодно. Для тестов нам бы хотелось более сговорчивого коллегу. И наш недавно познанный httptest поможет нам и тут. Код вызова внешнего api с передачей данных далее.
func ApiCaller(user *User, url string) error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() return updateUser(user, resp.Body) }
Что бы победить это, мы можем сделать мок внешнего API, самый просто вариант выглядит так
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, `{ "result": "ok", "data": { "user_id": 1, "rating": 42 } }`) })) defer ts.Close() user := User{id: 1} err := ApiCaller(&user, ts.URL)
ts.URL будет содержать строку формата `http://127.0.0.1:49799`, которая и будет моком api вызывающего вашу реализацию
Часть 3. Давайте работать с базой.
Есть простой путь: поднять докер с базой, накатить миграции, фикстуры и запустить наш прекрасный сервис. Но попытаемся писать тесты, имея минимум зависимостей со внешними сервисами.
Работа с базой в го позволяет подменить сам драйвер, и минуя 100 страниц кода и размышлений, я предлагаю вам взять библиотеку github.com/DATA-DOG/go-sqlmock
Разобраться с sql.Db можно по доке. Возьмем чуть более интересный пример, в котором будет orm для — gorm.
func DbListener(db *gorm.DB) { user := User{} transaction := db.Begin() transaction.First(&user, 1) transaction.Model(&user).Update("counter", user.Counter+1) transaction.Commit() }
Надеюсь этот пример хотя бы заставил вас подумать, как же это тестировать. В «mock.ExpectExec» можно подставить регулярное выражение, покрывающее нужный вам кейс. Единственное надо помнить, что порядок выставление expect’ов должен совпадать с порядком и количеством вызовов.
func TestDbListener(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() result := []string{"id", "name", "counter"} mock.ExpectQuery("SELECT \\* FROM `Users`").WillReturnRows(sqlmock.NewRows(result).AddRow(1, "Jack", 2)) mock.ExpectExec("UPDATE `Users`").WithArgs(3, 1).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() gormDB, _ := gorm.Open("mysql", db) DbListener(gormDB.LogMode(true)) if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }
Много примеров по тестированию базы я нашел здесь.
Часть 4. Работа с файловой системой.
Мы попробовали свои силы в разных областях и смирились, что хорошо все мокать. Тут все не так однозначно. Я тут предлагаю два подхода, мокать или использовать файловую систему.
Вариант 1, все мокаем по github.com/spf13/afero
Плюсы:
Ничего не надо переделывать, если вы уже используете эту библиотеку. ( но тогда и читать это вам скучно)
Работа с виртуальной файловой системой, что значительно ускорит ваши тесты.
Минусы:
Требуется доработка существующего кода.
В виртуальной файловой системе не работает chmod, но возможно это фича т.к. плюсы “Avoid security issues and permissions”
github.com/spf13/afero/issues/150
Из этих немногочисленных пунктов я сразу сделал 2 теста. В варианте с файловой системой я создал нечитаемый файл и проверил, как отработает система.
func FileRead(path string) error { path = strings.TrimRight(path, "/") + "/" // независим от заверщающего слеша files, err := ioutil.ReadDir(path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := ioutil.ReadFile(deleteFileName) if err != nil { return err } err = os.Remove(deleteFileName) // после вывода удаляем файл } return nil }
Использование afero.Fs требует минимальных доработок, но принципиально в коде ничего не меняет
func FileReadAlt(path string, fs afero.Fs) error { path = strings.TrimRight(path, "/") + "/" // независим от заверщающего слеша files, err := afero.ReadDir(fs, path) if err != nil { return fmt.Errorf("cannot read from file, %v", err) } for _, f := range files { deleteFileName := path + f.Name() _, err := afero.ReadFile(fs, deleteFileName) if err != nil { return err } err = fs.Remove(deleteFileName) // после вывода удаляем файл } return nil }
Но наше веселье будет неполным, если мы не узнаем насколько быстрее afero, чем native:
Минутка бенчмарков
BenchmarkIoutil 5000 242504 ns/op 7548 B/op 27 allocs/op BenchmarkAferoOs 300000 4259 ns/op 2144 B/op 30 allocs/op BenchmarkAferoMem 300000 4169 ns/op 2144 B/op 30 allocs/op
Итак библиотека на порядок опережает стандартную, но вот использовать виртуальную файловую систему или реальную — уже на ваше усмотрение.
Рекомендую:
haisum.github.io/2017/09/11/golang-ioutil-readall
matthias-endler.de/2018/go-io-testing
Послесловие, мне честно очень нравится 100% покрытие, но небиблиотечный код не нуждается в нем. И даже оно не гарантирует защиту от ошибки. Ориентируйтесь на требования бизнеса, а не на возможность функции вернуть 10 различных ошибок.
Для любителей потыкать код и позапускать тесты, репозиторий в студию.
ссылка на оригинал статьи https://habr.com/post/425025/
Добавить комментарий