Dalam bahasa dinamis, seperti python dan javascript, dimungkinkan untuk mengganti metode dan kelas dalam modul secara langsung selama operasi. Ini sangat nyaman untuk pengujian - Anda cukup meletakkan "tambalan" yang akan mengecualikan logika yang berat atau tidak perlu dalam konteks tes ini.
Tapi apa yang harus dilakukan di C ++? Pergi Jawa? Dalam bahasa-bahasa ini, kode tidak dapat dimodifikasi untuk pengujian dengan cepat, dan membuat tambalan membutuhkan alat terpisah.
Dalam kasus seperti itu, Anda harus secara khusus menulis kode sehingga diuji. Ini bukan hanya keinginan besar untuk melihat cakupan 100% dalam proyek Anda. Ini adalah langkah menuju penulisan kode yang didukung dan berkualitas.
Pada artikel ini saya akan mencoba untuk berbicara tentang ide-ide utama di balik penulisan kode yang dapat diuji dan menunjukkan bagaimana mereka dapat digunakan dengan contoh program go sederhana.
Program tidak rumit
Kami akan menulis program sederhana untuk membuat permintaan ke VK API. Ini adalah program yang cukup sederhana yang menghasilkan permintaan, membuatnya, membaca respons, menerjemahkan jawaban dari JSON ke dalam struktur dan menampilkan hasilnya kepada pengguna.
package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" ) const token = "token here" func main() {
Sebagai profesional di bidang kami, kami memutuskan bahwa perlu untuk menulis tes untuk aplikasi kami. Buat file uji ...
package main import ( "testing" ) func Test_Main(t *testing.T) { main() }
Itu tidak terlihat sangat menarik. Pemeriksaan ini adalah peluncuran sederhana aplikasi yang tidak dapat kami pengaruhi. Kami tidak dapat mengecualikan pekerjaan dengan jaringan, memeriksa operabilitas untuk berbagai kesalahan, dan bahkan mengganti token untuk verifikasi akan gagal. Mari kita coba mencari cara untuk meningkatkan program ini.
Pola Injeksi Ketergantungan
Pertama, Anda perlu menerapkan pola "injeksi ketergantungan" .
type VKClient struct { Token string } func (client VKClient) ShowUserInfo() { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, )
Dengan menambahkan struktur, kami membuat dependensi (kunci akses) untuk aplikasi, yang dapat ditransfer dari sumber yang berbeda, yang menghindari nilai "kabel" dan menyederhanakan pengujian.
package example import ( "testing" ) const workingToken = "workingToken" func Test_ShowUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
Sekarang hanya seseorang yang bisa membuat kesalahan, dan kemudian hanya jika dia tahu apa kesimpulannya. Untuk mengatasi masalah ini, perlu untuk tidak menampilkan informasi secara langsung ke aliran output, tetapi untuk menambahkan metode terpisah untuk mendapatkan informasi dan hasilnya. Dua bagian independen ini akan lebih mudah untuk diverifikasi dan dirawat.
Mari kita membuat metode GetUserInfo()
, yang akan mengembalikan struktur dengan informasi pengguna dan kesalahan (jika itu terjadi). Karena metode ini tidak menghasilkan apa-apa, kesalahan yang terjadi akan ditransmisikan lebih lanjut tanpa keluaran, sehingga kode yang membutuhkan data akan mengetahui situasinya.
type UserInfo struct { ID int `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` } func (client VKClient) GetUserInfo() (UserInfo, error) { var requestURL = fmt.Sprintf( "https://api.vk.com/method/%s?&access_token=%s&v=5.95", "users.get", client.Token, ) resp, err := http.PostForm(requestURL, nil) if err != nil { return UserInfo{}, err }
Ubah ShowUserInfo()
sehingga menggunakan GetUserInfo()
dan menangani kesalahan.
func (client VKClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Println(err) return } fmt.Printf( "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
Sekarang dalam tes Anda dapat memverifikasi bahwa jawaban yang benar diterima dari server, dan jika token salah, kesalahan dikembalikan.
func Test_GetUserInfo_Successful(t *testing.T) { client := VKClient{workingToken} userInfo, err := client.GetUserInfo() if err != nil { t.Fatal(err) } if userInfo.ID == 0 { t.Fatal("ID is empty") } if userInfo.FirstName == "" { t.Fatal("FirstName is empty") } if userInfo.LastName == "" { t.Fatal("LastName is empty") } } func Test_ShowUserInfo_EmptyToken(t *testing.T) { client := VKClient{""} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but found <nil>") } if err.Error() != "No values in response array" { t.Fatalf(`Expected "No values in response array", but found "%s"`, err) } }
Selain memperbarui tes yang ada, Anda perlu menambahkan tes baru untuk metode ShowUserInfo()
.
func Test_ShowUserInfo(t *testing.T) { client := VKClient{workingToken} client.ShowUserInfo() } func Test_ShowUserInfo_WithError(t *testing.T) { client := VKClient{""} client.ShowUserInfo() }
Alternatif khusus
Tes untuk ShowUserInfo()
menyerupai apa yang kami coba hindari pada awalnya. Dalam hal ini, satu-satunya titik metode ini adalah untuk menampilkan informasi ke aliran keluaran standar. Di satu sisi, Anda dapat mencoba mendefinisikan ulang os.Stdout dan memeriksa output, sepertinya solusi yang terlalu berlebihan ketika Anda dapat bertindak lebih elegan.
Alih-alih menggunakan fmt.Printf
, Anda dapat menggunakan fmt.Fprintf
, yang memungkinkan Anda untuk output ke io.Writer
. os.Stdout
mengimplementasikan antarmuka ini, yang memungkinkan kita untuk mengganti fmt.Printf(text)
dengan fmt.Fprintf(os.Stdout, text)
. Setelah itu, kita bisa meletakkan os.Stdout
di bidang terpisah, yang dapat diatur ke nilai yang diinginkan (untuk tes - string, untuk pekerjaan - aliran output standar).
Karena kemampuan untuk mengubah Writer untuk output akan jarang digunakan, terutama untuk tes, masuk akal untuk menetapkan nilai default. Pada VKClient
, untuk ini kita akan melakukan ini - membuat tipe VKClient
diekspor dan membuat fungsi konstruktor untuknya.
type vkClient struct { Token string OutputWriter io.Writer } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, } }
Dalam fungsi ShowUserInfo()
, kami mengganti Panggilan Print
dengan Fprintf
.
func (client vkClient) ShowUserInfo() { userInfo, err := client.GetUserInfo() if err != nil { fmt.Fprintf(client.OutputWriter, err.Error()) return } fmt.Fprintf( client.OutputWriter, "Your id: %d\nYour full name: %s %s\n", userInfo.ID, userInfo.FirstName, userInfo.LastName, ) }
Sekarang Anda perlu memperbarui tes sehingga mereka membuat klien menggunakan konstruktor dan menginstal Writer lain jika diperlukan.
func Test_ShowUserInfo(t *testing.T) { client := CreateVKClient(workingToken) buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) matched, err := regexp.Match( `Your id: \d+\nYour full name: [^\n]+\n`, result, ) if err != nil { t.Fatal(err) } if !matched { t.Fatalf(`Expected match but failed with "%s"`, result) } } func Test_ShowUserInfo_WithError(t *testing.T) { client := CreateVKClient("") buffer := bytes.NewBufferString("") client.OutputWriter = buffer client.ShowUserInfo() result, _ := ioutil.ReadAll(buffer) if string(result) != "No values in response array" { t.Fatal("Wrong error") } }
Untuk setiap pengujian tempat kami mengeluarkan sesuatu, kami membuat buffer yang akan memainkan peran aliran keluaran standar. Setelah fungsi dieksekusi, diperiksa bahwa hasilnya sesuai dengan harapan kami - dengan bantuan ekspresi reguler atau perbandingan sederhana.
Mengapa saya menggunakan ekspresi reguler? Agar tes dapat bekerja dengan token apa pun yang valid yang akan saya berikan ke program, terlepas dari nama pengguna dan ID pengguna.
Pola Injeksi Ketergantungan - 2
Saat ini, program ini memiliki cakupan 86,4%. Kenapa tidak 100%? Kami tidak dapat memprovokasi kesalahan dari http.PostForm()
, ioutil.ReadAll()
dan json.Unmarshal()
, yang berarti bahwa kami tidak dapat memeriksa setiap " return UserInfo, err
".
Untuk memberi Anda kontrol lebih besar terhadap situasi ini, Anda harus membuat antarmuka di mana http.Client
akan cocok, implementasinya akan di vkClient, dan digunakan untuk operasi jaringan. Bagi kami, dalam antarmuka, hanya satu metode yang PostForm
- PostForm
.
type Networker interface { PostForm(string, url.Values) (*http.Response, error) } type vkClient struct { Token string OutputWriter io.Writer Networker Networker } func CreateVKClient(token string) vkClient { return vkClient{ token, os.Stdout, &http.Client{}, } }
Langkah seperti itu menghilangkan kebutuhan untuk melakukan operasi jaringan secara umum. Sekarang kita cukup mengembalikan data yang diharapkan dari VKontakte menggunakan Networker
palsu. Tentu saja, jangan singkirkan tes yang akan memeriksa permintaan ke server, tetapi tidak perlu membuat permintaan di setiap tes.
Kami akan membuat implementasi untuk Networker
dan Reader
palsu, sehingga kami dapat menguji kesalahan dalam setiap kasus - berdasarkan permintaan, saat membaca isi dan selama deserialisasi. Jika kita menginginkan kesalahan saat memanggil PostForm, maka kita cukup mengembalikannya dalam metode ini. Jika kita menginginkan kesalahan
saat membaca badan tanggapan - perlu mengembalikan Reader
palsu, yang akan menimbulkan kesalahan. Dan jika kita membutuhkan kesalahan untuk memanifestasikan dirinya selama deserialisasi, maka kita mengembalikan jawabannya dengan string kosong di tubuh. Jika kami tidak ingin kesalahan, kami hanya mengembalikan isi dengan isi yang ditentukan.
type fakeReader struct{} func (fakeReader) Read(p []byte) (n int, err error) { return 0, errors.New("Error on read") } type fakeNetworker struct { ErrorOnPostForm bool ErrorOnBodyRead bool ErrorOnUnmarchal bool RawBody string } func (fn *fakeNetworker) PostForm(string, url.Values) (*http.Response, error) { if fn.ErrorOnPostForm { return nil, fmt.Errorf("Error on PostForm") } if fn.ErrorOnBodyRead { return &http.Response{Body: ioutil.NopCloser(fakeReader{})}, nil } if fn.ErrorOnUnmarchal { fakeBody := ioutil.NopCloser(bytes.NewBufferString("")) return &http.Response{Body: fakeBody}, nil } fakeBody := ioutil.NopCloser(bytes.NewBufferString(fn.RawBody)) return &http.Response{Body: fakeBody}, nil }
Untuk setiap situasi masalah, kami menambahkan tes. Mereka akan membuat Networker
palsu dengan pengaturan yang diperlukan, yang menurutnya ia akan melakukan kesalahan pada titik tertentu. Setelah itu, kami memanggil fungsi untuk diperiksa dan memastikan bahwa terjadi kesalahan, dan kami mengharapkan kesalahan ini.
func Test_GetUserInfo_ErrorOnPostForm(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnPostForm: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on PostForm" { t.Fatalf(`Expected "Error on PostForm" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnBodyRead(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnBodyRead: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } if err.Error() != "Error on read" { t.Fatalf(`Expected "Error on read" but got "%s"`, err.Error()) } } func Test_GetUserInfo_ErrorOnUnmarchal(t *testing.T) { client := CreateVKClient(workingToken) client.Networker = &fakeNetworker{ErrorOnUnmarchal: true} _, err := client.GetUserInfo() if err == nil { t.Fatal("Expected error but none found") } const expectedError = "unexpected end of JSON input" if err.Error() != expectedError { t.Fatalf(`Expected "%s" but got "%s"`, expectedError, err.Error()) } }
Menggunakan bidang RawBody
, RawBody
dapat menyingkirkan permintaan jaringan (cukup kembalikan yang kami harapkan akan diterima dari VKontakte). Ini mungkin perlu untuk menghindari melampaui batas permintaan selama pengujian atau untuk mempercepat pengujian.
Ringkasan
Setelah semua operasi pada proyek, kami menerima paket dengan panjang 91 baris (+170 garis tes), yang mendukung output ke io.Writer
, memungkinkan Anda untuk menggunakan metode alternatif bekerja dengan jaringan (menggunakan adaptor ke antarmuka kami), di mana ada metode seperti untuk menghasilkan data, dan untuk mendapatkannya. Proyek ini memiliki cakupan 100%. Tes sepenuhnya memeriksa setiap baris dan respons aplikasi untuk setiap kemungkinan kesalahan.
Setiap langkah di jalan menuju cakupan 100% meningkatkan modularitas, rawatan dan keandalan aplikasi, sehingga tidak ada yang salah dengan fakta bahwa tes menentukan struktur paket.
Testabilitas kode apa pun adalah kualitas yang tidak muncul dari udara. Testabilitas muncul ketika pengembang cukup menggunakan pola dalam situasi yang sesuai dan menulis kode kustom dan modular. Tugas utama adalah menunjukkan proses berpikir saat melakukan program refactoring. Pemikiran serupa dapat diperluas ke aplikasi dan perpustakaan apa saja, serta bahasa lain.