Testing Lebih Mudah Dengan io.Reader
• 3 menit
Sebelumnya Granary memiliki fungsi yang kurang lebih terlihat seperti ini:
package content
import (
"log"
"os"
)
func ReadContent(path string) string {
content, err := os.ReadFile(path)
if err != nil {
log.Fatal(err)
}
// lakukan sesuatu dengan content
return string(content)
}
(Kode di atas adalah versi yang sudah disederhanakan, untuk melihat versi aslinya, kalian bisa akses di sini)
Simpel, tetapi fungsi di atas memiliki sebuah keterbatasan: fungsi tersebut sulit untuk dilakukan testing.
Mengapa?
Fungsi ReadContent
menerima sebuah parameter, yakni path
, yang berupa string.
Kemudian fungsi ini akan membaca isi konten dari file yang memiliki path
tersebut.
Permasalahannya adalah, ketika kita ingin melakukan testing untuk fungsi ini, kita harus membuat sebuah file dummy terlebih dulu. Setelahnya baru path dari file dummy tersebut akan diberikan ketika memanggil fungsi ReadContent
.
Hal ini tentunya bukan solusi terbaik, mengingat setiap kali kita ingin menjalankan test, kita perlu membuat sebuah file dan (mungkin) juga akan langsung menghapusnya setelah test selesai.
Bagaimana jika kita ingin membuat banyak test case untuk fungsi yang sama? Tentunya kita perlu menambah jumlah file yang perlu dibuat juga.
Solusi: gunakan io.Reader
Mari kira refactor kode di atas menjadi:
package content
import (
"log"
"io"
)
func ReadContent(in io.Reader) string {
content, err := io.ReadAll(in)
if err != nil {
log.Fatal(err)
}
// ...
}
Sekarang kita mengubah fungsi ReadContent
untuk menerima parameter berupa interface io.Reader
, bukan lagi path dari sebuah file.
Kelebihan dari teknik ini adalah kita bisa memberikan argumen dengan tipe data apapun selama ia mengimplementasikan fungsi Read
milik io.Reader
.
Sehingga saat ini fungsi ReadContent
tidak lagi bergantung dengan ada atau tidaknya file tertentu.
Sebagai contoh, kini kita dapat melakukan test seperti ini:
package content
import (
"strings"
"testing"
)
func TestReadContent(t *testing.T) {
// Pengganti file dummy
in := strings.NewReader("ini adalah konten")
content, err := ReadContent(in)
if err != nil {
t.Fatal(err)
}
if content != "ini adalah konten" {
t.Fatal("konten tidak sama")
}
}
Sekarang kita tidak perlu lagi untuk membuat sebuah file dummy sebelum bisa menjalankan test dari fungsi ReadContent
👌
(Kalian juga bisa melihat contoh hasil dari refactor ini di repo yang sama)
Bagaimana cara untuk menggunakannya dengan file?
Simpel, sekarang kita hanya perlu membuka file di luar dari fungsi tersebut.
Sebagai contoh:
package content
import (
"log"
"os"
)
func Run() {
path := "my-file.txt"
f, err := os.OpenFile(path)
if err != nil {
log.Fatal(err)
}
defer f.Close()
fmt.Println(ReadContent(f))
}
Bagaimana dengan operasi “writing”?
Kita bisa menggunakan teknik yang sama, namun, kali ini kita menggunakan interface lain, yakni io.Writer
.
Kalian juga bisa melihat contoh dari penggunaan tipe ini di repo Granary.
Penutup
Kita telah melakukan refactoring fungsi ReadContent
dengan mengubah parameter yang diterima menjadi io.Reader
.
Kini, fungsi tersebut dapat dengan lebih mudah untuk dilakukan testing.
Kita tidak lagi perlu membuat file dummy terlebih dulu. Kita bisa menggunakan tipe strings.Reader
seperti contoh di atas, atau mungkin dengan tipe lain seperti bytes.Buffer
.
Sekian dan terima kasih.