Golang menguji di luar jangkauan kita



Tidak ada yang suka menulis tes. Tentu saja aku bercanda, semua orang suka menulisnya! Seperti yang akan disampaikan oleh pemimpin tim dan SDM, jawaban yang tepat dalam wawancara adalah bahwa saya benar-benar menyukai dan menulis ujian. Tapi tiba-tiba Anda suka menulis tes dalam bahasa lain. Bagaimana Anda mulai menulis kode go yang tercakup dalam tes?

Bagian 1. Menguji pawang


Di luar kotak ada dukungan untuk server http di "net / http", sehingga Anda dapat mengangkatnya tanpa usaha. Peluang yang terbuka memungkinkan kita untuk merasa sangat kuat, dan karena itu kode kita akan mengembalikan pengguna ke-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 } 

Kode ini menerima parameter id pengguna sebagai input, kemudian mengemulasi keberadaan pengguna dalam database, dan kembali. Sekarang kita perlu mengujinya ...

Ada hal yang luar biasa โ€œnet / http / httptestโ€, memungkinkan Anda untuk mensimulasikan panggilan ke handler'a kami dan kemudian membandingkan jawabannya.

 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) } 

Bagian 2. Sayang, kami memiliki API eksternal di sini


Dan mengapa kita perlu mengambil napas, jika kita baru saja melakukan pemanasan? Di dalam layanan kami, cepat atau lambat, api eksternal akan muncul. Ini adalah binatang aneh yang sering bersembunyi yang dapat berperilaku seperti yang diinginkan. Untuk tes, kami ingin kolega yang lebih akomodatif. Dan httptest kami yang baru ditemukan juga akan membantu kami di sini. Sebagai contoh, kode panggilan adalah api eksternal dengan transfer data lebih lanjut.

 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) } 

Untuk mengalahkan ini, kita dapat membuat tiruan API eksternal, opsi paling sederhana adalah:

  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 akan berisi string format `http: //127.0.0.1: 49799`, yang akan menjadi api tiruan yang menyebut implementasi kami

Bagian 3. Mari kita bekerja dengan pangkalan


Ada cara sederhana: untuk menaikkan buruh pelabuhan dengan pangkalan, roll migrasi, perlengkapan dan menjalankan layanan terbaik kami. Tetapi mari kita coba menulis tes dengan ketergantungan minimum dengan layanan eksternal.

Implementasi bekerja dengan base in go memungkinkan Anda untuk mengganti driver itu sendiri, dan, melewati 100 halaman kode dan refleksi, saya sarankan Anda mengambil perpustakaan github.com/DATA-DOG/go-sqlmock
Anda dapat menangani sql.Db di dok. Mari kita ambil contoh yang sedikit lebih menarik, di mana akan ada orm for - 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() } 

Saya harap contoh ini setidaknya membuat Anda berpikir bagaimana cara mengujinya. Dalam "mock.ExpectExec" Anda bisa mengganti ekspresi reguler yang mencakup case yang Anda butuhkan. Satu-satunya hal yang perlu diingat adalah bahwa urutan penetapan harapan harus sesuai dengan urutan dan jumlah panggilan.

 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) } } 

Saya menemukan banyak contoh untuk menguji pangkalan di sini .

Bagian 4. Bekerja dengan sistem file


Kami mencoba tangan kami di daerah yang berbeda dan berdamai bahwa semuanya baik untuk basah. Semuanya tidak begitu jelas di sini. Saya menyarankan dua pendekatan, basah atau gunakan sistem file.

Opsi 1 - kita semua basah di github.com/spf13/afero

Pro :
  • Anda tidak perlu mengulang apa pun jika Anda sudah menggunakan perpustakaan ini. (tapi kemudian kamu bosan membaca itu)
  • Bekerja dengan sistem file virtual, yang akan sangat mempercepat tes Anda.


Cons :
  • Diperlukan modifikasi kode yang ada.
  • Chmod tidak berfungsi pada sistem file virtual. Tapi itu bisa menjadi fitur sejak itu dokumentasi menyatakan "Hindari masalah keamanan dan izin".

Dari beberapa poin ini, saya langsung melakukan 2 tes. Dalam versi dengan sistem file, saya membuat file yang tidak dapat dibaca dan memeriksa cara kerja sistem.

 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 } 

Menggunakan afero.Fs membutuhkan modifikasi minimal, tetapi pada dasarnya tidak ada perubahan dalam kode

 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 } 

Tapi kesenangan kita tidak akan lengkap kecuali kita mengetahui seberapa cepat afero daripada asli.
Menit benchmark:

 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 

Jadi, pustaka adalah urutan besarnya di atas standar, tetapi menggunakan sistem file virtual atau yang asli adalah atas kebijakan Anda.

Saya merekomendasikan:

haisum.imtqy.com/2017/09/11/golang-ioutil-readall
matthias-endler.de/2018/go-io-testing

Kata penutup


Jujur saya sangat suka cakupan 100%, tetapi kode non-perpustakaan tidak membutuhkannya. Dan bahkan itu tidak menjamin perlindungan terhadap kesalahan. Fokus pada persyaratan bisnis, bukan kemampuan fungsi untuk mengembalikan 10 kesalahan yang berbeda.

Bagi mereka yang suka menyodok kode dan menjalankan tes, repositori .

Source: https://habr.com/ru/post/id425025/


All Articles