Testing Lebih Mudah Dengan io.Reader

3 menit

featured-image.avif

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.