Expecto

Framework pengujian yang dapat dikomposisi dan peduli performa eksekusi tes.

Daftar isi

Expecto adalah framework pengujian untuk F# yang menyusun tes sebagai nilai biasa, yaitu fungsi kecil yang digabung menjadi daftar tes dengan operator komposisi. Topik ini membahas cara membuat proyek pengujian Expecto, menyusun tes dengan testList dan testCase, menyaring tes dari baris perintah, serta membaca keluaran kegagalan tes. Seluruh perintah dijalankan dengan dotnet CLI, sehingga alur yang dibahas berjalan sama di editor mana pun.

Komposisi tes dengan testList dan testCase#

Proyek pengujian Expecto berupa proyek konsol biasa yang mereferensikan paket Expecto. Perintah berikut membuat proyek bernama UjiExpecto dan menambahkan paketnya. Kedua perintah mencetak ringkasan progres yang memuat durasi dan jalur berkas, sehingga keluarannya tidak ditempelkan di sini.

bash
# membuat proyek konsol F# bernama UjiExpecto
dotnet new console -lang F# -o UjiExpecto
# menambahkan paket Expecto ke proyek
dotnet add UjiExpecto package Expecto

Tes Expecto disusun dari dua elemen dasar. testCase menerima nama tes dan fungsi tanpa argumen yang berisi asersi, sedangkan testList menggabungkan sejumlah tes menjadi satu kelompok bernama. Karena keduanya adalah nilai biasa, kelompok dapat disarang menjadi subkelompok tanpa sintaks tambahan. Berkas Program.fs berikut berisi tiga tes dalam dua subkelompok.

fsharp Program.fs
// tidak-dijalankan: berkas proyek ini dieksekusi melalui blok bash berikutnya
module Program

open Expecto

let tests =
    testList "statistik" [
        testList "rata-rata" [
            testCase "rata-rata tiga nilai" <| fun () ->
                Expect.equal ((80 + 60 + 70) / 3) 70 "rata-rata tiga nilai ujian"
            testCase "rata-rata simetris" <| fun () ->
                Expect.equal ((-10 + 10) / 2) 0 "rata-rata nilai yang saling meniadakan"
        ]
        testList "suhu" [
            testCase "celsius ke fahrenheit" <| fun () ->
                Expect.equal (25.0 * 9.0 / 5.0 + 32.0) 77.0 "konversi 25 derajat celsius"
        ]
    ]

[<EntryPoint>]
let main argv = runTestsWithCLIArgs [] argv tests

Fungsi Expect.equal menerima nilai aktual, nilai yang diharapkan, dan pesan kegagalan, lalu menghentikan tes dengan pesan bila keduanya berbeda. Operator <| menerapkan fungsi testCase pada argumen terakhirnya dan membuat baris tes terbaca seperti kalimat. Titik masuk main memanggil runTestsWithCLIArgs agar argumen baris perintah seperti --filter dikenali.

bash
# menyalin berkas tes ke proyek, menggantikan Program.fs bawaan
cp Program.fs UjiExpecto/Program.fs
# menjalankan tes dengan warna dimatikan, lalu menyaring baris ringkasannya
dotnet run --project UjiExpecto -- --colours 0 | grep -oE "[0-9]+ passed, [0-9]+ ignored, [0-9]+ failed, [0-9]+ errored"
text Keluaran
3 passed, 0 ignored, 0 failed, 0 errored

Baris ringkasan memuat empat angka, yaitu jumlah tes yang lolos, diabaikan, gagal, dan error. Expecto mencetak keluaran lengkap dengan waktu berjalan dan durasi eksekusi yang berubah-ubah antar eksekusi, sehingga blok keluaran di sini sengaja disaring ke baris ringkasannya saja. Opsi --colours 0 menonaktifkan kode warna terminal agar keluaran yang disaring bersih dari karakter kendali.

Menyaring tes dari baris perintah#

Argumen --filter menjalankan sebagian pohon tes berdasarkan hierarki namanya, dengan pemisah titik antar tingkat. Nilai filter menyebut jalur subkelompok yang ingin dijalankan, dan kelompok lain dilewati seluruhnya. Filter berguna ketika kelompok tes sudah banyak dan eksekusi penuh dirasa lambat.

bash
# hanya menjalankan kelompok suhu dari pohon tes
dotnet run --project UjiExpecto -- --colours 0 --filter statistik.suhu | grep -oE "[0-9]+ passed, [0-9]+ ignored, [0-9]+ failed, [0-9]+ errored"
text Keluaran
1 passed, 0 ignored, 0 failed, 0 errored

Hanya satu tes pada subkelompok suhu yang dieksekusi. Ringkasannya memuat satu tes yang lolos. Susunan nama filter mengikuti sarang testList, sehingga pengelompokan yang rapi sejak awal memudahkan penyaringan nanti.

Menandai tes yang belum siap#

Tes yang ditulis lebih dahulu daripada implementasinya membutuhkan penanda khusus, yaitu status yang mencatat keberadaan tes tanpa mengeksekusinya. Expecto menyediakan ptestCase untuk kebutuhan itu: tesnya masuk daftar, dicatat pada ringkasan sebagai tes yang diabaikan (ignored), dan badan fungsinya tidak dijalankan. Versi testList untuk kelompok, yaitu ptestList, juga tersedia dengan perilaku yang sama pada seluruh isinya. Berkas berikut menambahkan satu tes tertunda pada kelompok suhu.

fsharp ProgramPending.fs
// tidak-dijalankan: berkas proyek ini dieksekusi melalui blok bash berikutnya
module Program

open Expecto

let tests =
    testList "statistik" [
        testList "rata-rata" [
            testCase "rata-rata tiga nilai" <| fun () ->
                Expect.equal ((80 + 60 + 70) / 3) 70 "rata-rata tiga nilai ujian"
            testCase "rata-rata simetris" <| fun () ->
                Expect.equal ((-10 + 10) / 2) 0 "rata-rata nilai yang saling meniadakan"
        ]
        testList "suhu" [
            testCase "celsius ke fahrenheit" <| fun () ->
                Expect.equal (25.0 * 9.0 / 5.0 + 32.0) 77.0 "konversi 25 derajat celsius"
            ptestCase "kelvin ke fahrenheit" <| fun () ->
                Expect.equal 0.0 0.0 "konversi kelvin"
        ]
    ]

[<EntryPoint>]
let main argv = runTestsWithCLIArgs [] argv tests
bash
# menimpa Program.fs dengan versi yang memuat tes tertunda, lalu menjalankannya
cp ProgramPending.fs UjiExpecto/Program.fs
dotnet run --project UjiExpecto -- --colours 0 | grep -oE "[0-9]+ passed, [0-9]+ ignored, [0-9]+ failed, [0-9]+ errored"
text Keluaran
3 passed, 1 ignored, 0 failed, 0 errored

Ringkasan kini memuat satu tes yang diabaikan, yaitu kelvin ke fahrenheit, tanpa mengubah jumlah tes yang lolos. Konvensi kerjanya sederhana: algoritma konversi yang belum ada ditandai ptestCase, dan penandanya dilepas menjadi testCase begitu implementasinya selesai. Lawan dari ptestCase adalah ftestCase yang memusatkan eksekusi hanya pada tes terpilih, berguna ketika satu tes gagal dan sisa pohon tes ingin dibiarkan tidak dieksekusi untuk sementara.

Membaca keluaran kegagalan#

Tes yang gagal tidak menghentikan sisa eksekusi, melainkan dicatat dan dilaporkan pada ringkasan. Berkas berikut berisi satu tes yang sengaja dibuat gagal karena Expect.equal menerima 2 sedangkan nilai yang diharapkan 3.

fsharp ProgramGagal.fs
// tidak-dijalankan: berkas proyek ini dieksekusi melalui blok bash berikutnya
module Program

open Expecto

let tests =
    testList "statistik" [
        testCase "jumlah dua nilai" <| fun () ->
            Expect.equal (1 + 1) 3 "penjumlahan dua nilai"
    ]

[<EntryPoint>]
let main argv = runTestsWithCLIArgs [] argv tests
bash
# menimpa Program.fs dengan versi yang gagal, lalu menjalankannya
cp ProgramGagal.fs UjiExpecto/Program.fs
dotnet run --project UjiExpecto -- --colours 0 | grep -oE "[0-9]+ passed, [0-9]+ ignored, [0-9]+ failed, [0-9]+ errored"
text Keluaran
0 passed, 0 ignored, 1 failed, 0 errored

Ringkasan mencatat satu tes gagal dan tidak ada yang lolos. Kode keluar proses juga berubah menjadi bukan nol, sehingga alur continuous integration dapat menghentikan langkah berikutnya tanpa membaca keluaran sama sekali.

Format lengkap kegagalan di terminal memuat baris demi baris yang lebih rinci. Tangkapan berikut telah dipangkas dari waktu berjalan, durasi eksekusi, dan jalur berkas yang berubah antar eksekusi, sebagaimana dinyatakan pada prosa di atas.

text Format keluaran kegagalan di terminal
[INF] EXPECTO? Running tests... <Expecto>
[ERR] statistik.jumlah dua nilai failed.
penjumlahan dua nilai.
expected: 3
  actual: 2
[INF] EXPECTO! 1 tests run for statistik.jumlah dua nilai – 0 passed, 0 ignored, 1 failed, 0 errored. <Expecto>

Dua baris expected dan actual berasal dari pesan Expect.equal, sedangkan pesan teks “penjumlahan dua nilai” adalah argumen ketiga fungsi tersebut. Pesan asersi yang spesifik mempersingkat diagnosis karena pembaca langsung melihat maksud tes yang dilanggar. Di bawah pesan itu terdapat jejak pemanggilan yang menyebut lokasi asersi, misalnya baris at Program.tests@7 yang merujuk pada baris definisi tes dalam Program.fs. Jejak semacam itu berguna saat pohon tes sudah besar, karena lokasi kegagalan ditemukan tanpa membuka berkas satu per satu, dan pola yang sama berlaku ketika kegagalan terjadi di dalam fungsi pembantu yang dipanggil dari beberapa tes sekaligus.

Ringkasan#

  • Tes Expecto berupa nilai biasa: testCase untuk satu tes, testList untuk kelompok yang dapat disarang, dan runTestsWithCLIArgs sebagai pintu masuknya.
  • Expect.equal membandingkan nilai aktual dengan nilai yang diharapkan dan melaporkan kegagalan beserta pesannya tanpa menghentikan tes lain.
  • --filter mengeksekusi sebagian pohon tes menurut hierarki namanya, dan kode keluar proses berubah bila ada kegagalan sehingga cocok untuk alur continuous integration.

Referensi#