ExUnit adalah kerangka pengujian bawaan Elixir yang menjalankan setiap tes di dalam process terpisah. Topik ini membahas anatomi berkas tes, perilaku eksekusi asinkron, doctest yang mengubah contoh dokumentasi menjadi tes, penyaringan tes dengan tag, serta berkas uji yang berdiri sendiri tanpa proyek Mix. Setelah topik ini diselesaikan, tes dapat ditulis, dijalankan, dan disaring sesuai kebutuhan.
Anatomi sebuah tes#
Sebuah tes ExUnit berada di dalam modul yang memanggil use ExUnit.Case, dan setiap tes ditulis dengan makro test/2 yang menerima deskripsi tes beserta blok asersinya. Fungsi assert/1 memeriksa kebenaran ekspresi dan menampilkan laporan yang rinci apabila pemeriksaan gagal. Skrip berikut mendefinisikan modul Kalkulator beserta tiga tes untuknya, lalu menjalankan tes tersebut secara programatik dan mencetak ringkasan statistiknya.
# menjalankan tes ExUnit secara programatik dan mencetak ringkasannya
ExUnit.start(autorun: false, seed: 0)
defmodule Kalkulator do
def tambah(a, b), do: a + b
def kali(a, b), do: a * b
end
defmodule KalkulatorTest do
use ExUnit.Case, async: true
test "penjumlahan dua bilangan" do
assert Kalkulator.tambah(2, 3) == 5
end
test "perkalian dua bilangan" do
assert Kalkulator.kali(2, 3) == 6
end
test "penjumlahan dengan nol" do
assert Kalkulator.tambah(5, 0) == 5
end
end
{hasil, _tangkapan} = ExUnit.CaptureIO.with_io(fn -> ExUnit.run() end)
IO.inspect(Map.delete(hasil, :seed), label: "ringkasan")ringkasan: %{total: 3, failures: 0, excluded: 0, skipped: 0}Baris ExUnit.start menyalakan server ExUnit yang mendaftarkan tes. Opsi autorun: false mencegah ExUnit menjalankan tes secara otomatis saat skrip berakhir, sehingga hanya pemanggilan ExUnit.run/0 yang memicu eksekusi. Opsi seed: 0 membuat urutan eksekusi tes tetap sama. Fungsi ExUnit.run/0 menjalankan seluruh tes yang terdaftar dan mengembalikan peta statistik berisi kunci total, failures, excluded, dan skipped. Pemanggilan ExUnit.CaptureIO.with_io menangkap keluaran konsol bawaan ExUnit. Keluaran itu memuat baris waktu dan tidak stabil untuk direkam, sehingga yang ditampilkan hanyalah ringkasan dari nilai kembalian ExUnit.run/0.
Deskripsi yang diberikan pada makro test/2 bukan hiasan, karena teks itulah yang tampil pada laporan apabila tes gagal. Asersi assert Kalkulator.tambah(2, 3) == 5 juga tidak berhenti pada pemberitahuan gagal, melainkan menampilkan nilai kedua sisi perbandingan, sehingga penyebab kegagalan sering langsung terbaca dari laporannya. Nama modul tes bebas, tetapi konvensinya berakhiran Test, sebagaimana KalkulatorTest pada contoh di atas.
Tes asinkron#
Opsi async: true pada use ExUnit.Case membuat modul tes dapat berjalan paralel dengan modul tes asinkron lainnya. ExUnit menyeimbangkan beban dengan batas max_cases, yaitu jumlah tes yang boleh berjalan serentak. Nilai batas itu dihitung dari sumber daya komputer yang tersedia. Setiap tes tetap berjalan dalam process sendiri, sehingga variabel lokal satu tes tidak pernah terlihat oleh tes lain.
Kemandirian itu membawa syarat yang jelas, yaitu tes tidak boleh bergantung pada urutan eksekusi dan tidak boleh menulis pada state bersama yang dapat berubah. State yang dibutuhkan setiap tes sebaiknya disiapkan ulang melalui callback setup, yang dijalankan sekali untuk setiap tes sebelum blok asersinya. Tes yang menyentuh sumber daya luar tetap dapat berjalan asinkron selama aksesnya terisolasi, misalnya tes basis data yang menggunakan sandbox yang disediakan Ecto untuk setiap tes.
Menjalankan tes dengan mix test#
Dalam proyek Mix, seluruh pengaturan di atas diurus oleh perintah mix test, yang mengompilasi aplikasi, menyiapkan lingkungan test, lalu mengeksekusi ExUnit. Transkrip berikut direkam dari proyek baru yang dibuat pada topik Proyek Mix.
# tidak-dijalankan
# transkrip asli perintah mix test pada proyek baru, direkam saat topik ditulis
$ mix test
Compiling 1 file (.ex)
Generated kalkulator app
Running ExUnit with seed: 483234, max_cases: 4
..
Finished in 0.02 seconds (0.00s async, 0.02s sync)
Result: 2 passed (1 doctest, 1 test)
Baris Running ExUnit with seed memuat angka acak yang berubah pada setiap eksekusi, karena ExUnit mengacak urutan tes berdasarkan seed tersebut. Baris Finished in memuat waktu yang juga selalu berbeda. Kedua baris itu wajar jika bervariasi dan tidak perlu dibandingkan antar eksekusi. Titik-titik pada keluaran menyatakan tes yang lulus, satu titik per tes, sedangkan baris Result merangkum jumlahnya beserta rincian (1 doctest, 1 test) karena proyek bawaan memuat satu doctest dan satu tes yang ditulis manual.
Seed tetap dapat dipilih secara manual melalui mix test --seed 0, yang menjalankan tes tanpa pengacakan urutan. Penyaringan tag pada bagian berikutnya juga dapat diberikan langsung sebagai opsi perintah ini, sehingga konfigurasi tidak perlu diubah hanya untuk satu kali eksekusi.
Doctest#
Dokumentasi fungsi dapat sekaligus menjadi tes melalui doctest. Contoh iex> yang ditulis pada atribut @doc dieksekusi ulang sebagai tes, dan hasil yang tertulis harus cocok dengan nilai yang benar-benar dikembalikan oleh fungsi. Pola ini menjaga contoh dokumentasi tetap benar, karena contoh yang usang akan membuat tes gagal. Proyek berikut memuat modul Statistik dengan dua contoh pada dokumentasinya.
# membuat proyek contoh dengan modul berdokumen
cd /tmp && rm -rf statistik && mix new statistik >/dev/null
cat > statistik/lib/statistik.ex <<'EOF'
defmodule Statistik do
@doc """
Menghitung rata-rata dari daftar bilangan.
## Contoh
iex> Statistik.rata_rata([10, 20, 30])
20.0
iex> Statistik.rata_rata([2, 4])
3.0
"""
def rata_rata(bilangan), do: Enum.sum(bilangan) / length(bilangan)
end
EOF
cat statistik/lib/statistik.exdefmodule Statistik do
@doc """
Menghitung rata-rata dari daftar bilangan.
## Contoh
iex> Statistik.rata_rata([10, 20, 30])
20.0
iex> Statistik.rata_rata([2, 4])
3.0
"""
def rata_rata(bilangan), do: Enum.sum(bilangan) / length(bilangan)
endBerkas uji proyek kemudian diganti agar memuat doctest Statistik, lalu tes dijalankan seperti biasa.
# mengganti berkas uji bawaan agar doctest dijalankan
cat > /tmp/statistik/test/statistik_test.exs <<'EOF'
defmodule StatistikTest do
use ExUnit.Case
doctest Statistik
end
EOF
cd /tmp/statistik && mix test 2>&1 | sed '/^Finished in/d; /^Running ExUnit with seed/d'Compiling 1 file (.ex)
Generated statistik app
..
Result: 2 passedSetiap contoh iex> dihitung sebagai satu tes, sehingga dua contoh pada dokumentasi menghasilkan Result: 2 passed. Karena seluruh tes berasal dari doctest, baris Result tidak menampilkan rincian tambahan, berbeda dengan transkrip sebelumnya yang memuat satu doctest dan satu tes yang ditulis manual. Apabila contoh tidak cocok dengan perilaku fungsi, ExUnit menampilkan baris dokumentasi yang gagal beserta nilai yang diharapkan dan nilai yang diperoleh, sehingga tempat kesalahannya mudah ditemukan.
Contoh pada dokumentasi juga boleh menyatakan error yang memang diharapkan, yaitu dengan menulis baris hasil dalam bentuk ** (JenisError) pesan. Doctest memeriksa jenis dan pesan errornya seketat pemeriksaan nilai biasa, sehingga kontrak kegagalan sebuah fungsi ikut teruji. Satu syarat penulisannya adalah contoh harus menjorok ke dalam pada string @doc, sebagaimana terlihat pada contoh iex> di atas.
Tag dan penyaringan#
Atribut @tag melabeli sebuah tes, dan label itu digunakan untuk menyaring tes yang dijalankan. Skrip berikut menandai satu tes dengan @tag :wip, yaitu label untuk pekerjaan yang masih berjalan, lalu mengecualikan label tersebut melalui ExUnit.configure sebelum eksekusi.
# mengecualikan tes berlabel wip dari eksekusi
ExUnit.start(autorun: false, seed: 0)
ExUnit.configure(exclude: [wip: true])
defmodule LaporanTest do
use ExUnit.Case, async: true
@tag :wip
test "laporan bulanan yang masih dikerjakan" do
assert String.upcase("a") == "A"
end
test "laporan harian yang sudah selesai" do
assert String.upcase("b") == "B"
end
@tag :integration
test "laporan yang memerlukan layanan luar" do
assert String.downcase("C") == "c"
end
end
{hasil, _tangkapan} = ExUnit.CaptureIO.with_io(fn -> ExUnit.run() end)
IO.inspect(Map.delete(hasil, :seed), label: "ringkasan")ringkasan: %{total: 3, failures: 0, excluded: 1, skipped: 0}Ringkasan menunjukkan total: 3 karena tiga tes tetap terdaftar, tetapi excluded: 1 karena tes berlabel :wip tidak dieksekusi. Nilai tag tidak harus berupa atom tunggal, karena pasangan kunci dan nilai seperti @tag laporan: :bulanan juga sah, dengan penyaringan yang ditulis sebagai exclude: [laporan: :bulanan]. Label juga dapat melekat pada seluruh modul melalui @moduletag, sehingga semua tes di dalamnya mendapat label yang sama. Dalam proyek Mix, konfigurasi exclude biasanya diletakkan pada berkas test/test_helper.exs sebelum tes dijalankan, sedangkan opsi mix test --exclude wip menyaring tanpa menyentuh berkas konfigurasi. Sebaliknya, opsi mix test --only integration hanya menjalankan tes berlabel tersebut dan mengecualikan sisanya secara otomatis.
Skrip uji mandiri#
Berkas uji tidak wajib berada di dalam proyek Mix. Dengan ExUnit.start dan ExUnit.run/0 yang sudah diperkenalkan, sebuah berkas .exs dapat berdiri sendiri dan dijalankan dengan perintah elixir kalkulator_test.exs. Berkas berikut menguji fungsi pembagian, termasuk kasus error yang diharapkan dan diperiksa melalui assert_raise.
# Tes mandiri untuk modul Kalkulator: dijalankan dengan perintah elixir kalkulator_test.exs
ExUnit.start(autorun: false, seed: 0)
defmodule Kalkulator do
def bagi(_a, 0), do: raise(ArithmeticError, "pembagian dengan nol")
def bagi(a, b), do: div(a, b)
end
defmodule KalkulatorTest do
use ExUnit.Case, async: true
test "pembagian dua bilangan bulat" do
assert Kalkulator.bagi(10, 2) == 5
end
test "pembagian dengan nol memunculkan error" do
assert_raise ArithmeticError, "pembagian dengan nol", fn ->
Kalkulator.bagi(10, 0)
end
end
end
{hasil, _tangkapan} = ExUnit.CaptureIO.with_io(fn -> ExUnit.run() end)
IO.inspect(Map.delete(hasil, :seed), label: "ringkasan")ringkasan: %{total: 2, failures: 0, excluded: 0, skipped: 0}Fungsi assert_raise/3 memeriksa bahwa blok yang diberikan benar-benar melempar ArithmeticError dengan pesan yang ditentukan. Pola ini digunakan untuk kegagalan yang merupakan bagian dari kontrak fungsi, bukan error tak terduga yang seharusnya dicegah. Ringkasan pada keluaran di atas menyatakan dua tes lulus tanpa kegagalan, dan berkas yang sama dapat dijalankan ulang kapan pun tanpa proyek maupun dependensi tambahan.
Bentuk berdiri sendiri seperti ini berguna untuk eksperimen cepat tanpa membuat proyek baru, serta untuk melampirkan kasus gagal yang pendek pada laporan isu pustaka pihak lain. Untuk proyek yang sesungguhnya, berkas uji tetap berada di folder test agar dapat dijalankan bersama seluruh tes lain melalui mix test, lengkap dengan konfigurasi, dependensi, dan lingkungannya.
Kesalahan umum#
Tes gagal hanya ketika dijalankan bersama tes lain. Modul asinkron menjalankan tes-tesnya secara paralel, sehingga tes yang lulus ketika dijalankan sendiri dapat gagal ketika dijalankan bersama. Penyebabnya biasanya state bersama, misalnya variabel modul yang ditulis oleh beberapa tes atau berkas yang sama yang diperbarui bersamaan. Perbaikannya adalah menyiapkan state baru untuk setiap tes melalui setup dan menghindari penulisan pada sumber daya yang digunakan bersama.
Gagal yang muncul dan hilang tanpa perubahan kode. ExUnit mengacak urutan tes berdasarkan seed, sehingga ketergantungan antar tes dapat lolos pada satu eksekusi dan gagal pada eksekusi berikutnya. Untuk menyelidikinya, jalankan ulang dengan seed yang sama melalui opsi mix test --seed 483234. Angka seed diambil dari baris Running ExUnit with seed pada eksekusi yang gagal. Setelah urutan yang bermasalah ditemukan, rapikan ketergantungannya agar tes lulus pada seed mana pun.
Membandingkan float dengan kesetaraan penuh. Aritmetika float tidak sepenuhnya presisi, karena 0.1 + 0.2 menghasilkan 0.30000000000000004, sehingga assert 0.1 + 0.2 == 0.3 gagal tanpa ada bug pada kode yang diuji. Bandingkan dengan toleransi melalui assert_in_delta/3, misalnya assert_in_delta 0.1 + 0.2, 0.3, 1.0e-9, sehingga tes menyatakan kebenaran numerik yang dimaksud, bukan representasi bitnya.
Ringkasan#
- Tes ditulis dalam modul
use ExUnit.Casedengan makrotest/2, danasync: truemenjalankan banyak tes secara paralel dalam process masing-masing. ExUnit.run/0mengembalikan peta statistiktotal,failures,excluded, danskippedyang dapat dicetak sebagai keluaran yang stabil.- Contoh
iex>pada@docmenjadi tes melaluidoctest, sehingga dokumentasi dan kode teruji bersama. @tagbeserta konfigurasiexcludemenyaring tes yang dieksekusi tanpa menghapus kodenya.
Referensi#
- ExUnit — Dokumentasi ExUnit — konfigurasi kerangka, seed, dan format keluarannya.
- ExUnit.Case — Dokumentasi ExUnit — opsi async, callback setup, dan template berkas uji.
- ExUnit.DocTest — Dokumentasi ExUnit — aturan penulisan contoh iex pada atribut @doc.
- mix test — Dokumentasi Mix — opsi baris perintah seperti –exclude dan –seed.
- Documentation, tests, and with — Dokumentasi Elixir — panduan resmi menulis dokumentasi dan doctest.