Crystal menyertakan framework spec, yaitu kerangka pengujian bawaan yang mengorganisir pengujian dengan describe dan it, menguji nilai dengan matcher setelah should, dan menangani exception dengan expect_raises. Topik ini membangun proyek kecil bernama konverter-suhu yang menguji modul konversi suhu, dimulai dari kerangka proyek hasil crystal init app, struktur folder spec, asersi beserta matchernya, contoh tertunda dengan pending, sampai tiga cara menjalankan spec. Setiap potongan kode dijalankan dan keluarannya ditempel apa adanya, sehingga bentuk laporan spec dapat dibaca langsung dari topik ini.
Proyek contoh dan folder spec#
Kerangka proyek dibuat dengan crystal init app. Perintah berikut membersihkan sisa proyek lama terlebih dahulu agar contoh dapat diulang dari keadaan bersih.
# bersihkan sisa proyek lama bila ada, lalu buat kerangka proyek
rm -rf konverter-suhu
crystal init app konverter-suhu create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/.gitignore
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/.editorconfig
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/LICENSE
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/README.md
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/shard.yml
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/src/konverter-suhu.cr
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/spec/spec_helper.cr
create /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/spec/konverter-suhu_spec.cr
Initialized empty Git repository in /home/kal/my-project/content/crystal/web-data-spec/framework-spec/konverter-suhu/.git/konverter-suhu/
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── shard.yml
├── spec/
│ ├── konverter-suhu_spec.cr
│ └── spec_helper.cr
└── src/
└── konverter-suhu.crFolder spec/ menampung berkas pengujian, sedangkan folder src/ menampung kode yang diuji. Berkas spec_helper.cr yang dibuat oleh init memuat require "spec" untuk memuat framework spec sekaligus require "../src/konverter-suhu" untuk memuat kode proyek, sehingga setiap berkas spec yang memuat spec_helper secara tidak langsung juga memuat seluruh kode proyek. Modul bawaan hasil init hanya berisi konstanta VERSION, sehingga isinya diganti dengan modul sungguhan berikut.
module Konverter::Suhu
VERSION = "0.1.0"
def self.celsius_ke_fahrenheit(celsius : Float64) : Float64
celsius * 9.0 / 5.0 + 32.0
end
def self.kategori(celsius : Float64) : String
if celsius < 0.0
"beku"
elsif celsius < 25.0
"sejuk"
else
"hangat"
end
end
def self.ambang(kategori : String) : Float64?
{"beku" => 0.0, "sejuk" => 10.0, "hangat" => 25.0, "panas" => 40.0}[kategori]?
end
def self.validasi!(celsius : Float64) : Nil
raise ArgumentError.new("celsius di bawah nol absolut") unless celsius >= -273.15
end
endModul ini memuat empat metode yang sengaja mencakup perilaku yang berbeda-beda. Metode celsius_ke_fahrenheit mengembalikan hasil pecahan, metode kategori mengembalikan String, metode ambang mengembalikan Float64? yang bisa bernilai nil untuk kategori yang tidak dikenal, sedangkan metode validasi! melempar exception ArgumentError apabila suhu berada di bawah nol absolut. Keempat perilaku itulah yang diuji pada bagian-bagian berikut.
describe dan it#
Satu berkas spec memuat kelompok describe yang berisi contoh it. Deskripsi pada describe menyebut objek yang diuji, sedangkan deskripsi pada it menyebut perilaku yang diharapkan dalam satu kalimat. describe boleh bersarang untuk memecah kelompok besar menjadi subkelompok per metode, dan Crystal juga menyediakan context sebagai nama lain describe yang lazim digunakan untuk menyebut keadaan tertentu. Blok berikut diambil dari berkas spec utama proyek ini.
# tidak-dijalankan
describe Konverter::Suhu do
describe "#celsius_ke_fahrenheit" do
it "mengonversi titik didih air" do
Konverter::Suhu.celsius_ke_fahrenheit(100.0).should eq(212.0)
end
end
endBerkas spec utama berikut memuat seluruh contoh untuk modul konverter, dari konversi titik beku dan titik didih, tipe hasil kategori, nilai nil dari ambang, sampai satu contoh tertunda di bagian akhir berkas.
require "./spec_helper"
describe Konverter::Suhu do
describe "#celsius_ke_fahrenheit" do
it "mengonversi titik beku air" do
Konverter::Suhu.celsius_ke_fahrenheit(0.0).should eq(32.0)
end
it "mengonversi titik didih air" do
Konverter::Suhu.celsius_ke_fahrenheit(100.0).should eq(212.0)
end
it "membandingkan hasil pecahan dengan be_close" do
Konverter::Suhu.celsius_ke_fahrenheit(37.0).should be_close(98.6, 0.01)
end
end
describe "#kategori" do
it "mengembalikan nilai bertipe String" do
hasil = Konverter::Suhu.kategori(18.0)
hasil.should be_a(String)
hasil.should eq("sejuk")
end
end
describe "#ambang" do
it "mengembalikan nil untuk kategori yang tidak dikenal" do
Konverter::Suhu.ambang("lembap").should be_nil
end
end
end
pending "konversi kelvin belum ditulis"Setiap it idealnya menguji satu perilaku dan tetap lolos apabila dijalankan sendirian, karena urutan eksekusi antarcontoh tidak dijamin. Deskripsi yang spesifik juga membantu, sebab laporan kegagalan menampilkan deskripsi itu apa adanya.
Asersi should dan matcher#
Asersi ditulis dengan should diikuti matcher, yaitu metode yang membandingkan nilai aktual dengan nilai yang diharapkan. Matcher eq memeriksa kesamaan nilai, matcher be_a memeriksa tipe, matcher be_nil memeriksa nilai nil, sedangkan matcher be_close(expected, delta) memeriksa pecahan dengan toleransi. Perbandingan pecahan memang perlu toleransi karena operasi aritmetika dapat menghasilkan representasi pecahan yang berbeda tipis dari nilai yang ditulis di kode.
# tidak-dijalankan
it "mengembalikan nilai bertipe String" do
hasil = Konverter::Suhu.kategori(18.0)
hasil.should be_a(String)
hasil.should eq("sejuk")
end# tidak-dijalankan
it "membandingkan hasil pecahan dengan be_close" do
Konverter::Suhu.celsius_ke_fahrenheit(37.0).should be_close(98.6, 0.01)
endPada contoh pertama, dua asersi berurutan memeriksa tipe lalu nilai dari variabel yang sama, dan contoh berhenti pada asersi pertama yang gagal. Pada contoh kedua, hasil 37.0 * 9.0 / 5.0 + 32.0 dianggap cukup dekat dengan 98.6 apabila selisihnya tidak melebihi 0.01. Satu contoh boleh memuat beberapa asersi, tetapi pemisahan satu perilaku per contoh membuat sumber kegagalan lebih mudah ditemukan.
Exception dengan expect_raises#
Asersi should menguji nilai yang dikembalikan, sedangkan metode yang melempar exception diuji dengan expect_raises. Metode ini menerima kelas exception dan pesan yang diharapkan, kemudian menjalankan blok yang diberikan. Contoh lolos apabila blok melempar exception yang cocok, dan gagal apabila exception tidak muncul atau pesannya berbeda. Berkas spec kedua proyek ini menguji metode validasi! dengan pola tersebut.
require "./spec_helper"
describe "#validasi!" do
it "melempar ArgumentError untuk suhu di bawah nol absolut" do
expect_raises(ArgumentError, "celsius di bawah nol absolut") do
Konverter::Suhu.validasi!(-300.0)
end
end
end# tidak-dijalankan
expect_raises(ArgumentError, "celsius di bawah nol absolut") do
Konverter::Suhu.validasi!(-300.0)
endPemanggilan metode yang melempar exception harus berada di dalam blok expect_raises. Apabila pemanggilan ditulis langsung di badan contoh, exception itu naik ke framework spec dan contoh dinyatakan gagal oleh exception tersebut, bukan tertangkap sebagai hasil pengujian.
Contoh tertunda dengan pending#
Metode pending mendaftarkan contoh yang perilakunya belum ditulis. Contoh semacam ini tetap muncul pada laporan tetapi tidak dieksekusi, sehingga daftar pekerjaan yang belum selesai terbaca dari laporan spec tanpa mengubah status lolos contoh lain. Baris berikut berada di bagian akhir berkas spec utama.
# tidak-dijalankan
pending "konversi kelvin belum ditulis"Pada laporan, contoh tertunda ditandai bintang, diperinci pada bagian Pending:, dan dihitung pada ringkasan sebagai 1 pending. Ketika perilakunya sudah ditulis, pending cukup diganti menjadi it, dan contoh itu ikut dijalankan pada eksekusi berikutnya.
Menjalankan spec#
Spec dijalankan dengan crystal spec yang mengompilasi seluruh berkas berakhiran _spec.cr di folder spec/ menjadi satu program, lalu menjalankan contoh-contohnya. Perintah berikut menyalin ketiga berkas contoh dari samping proyek ke tempat semestinya, kemudian menjalankan seluruh spec. Baris Finished in disaring dengan grep karena waktunya selalu berubah antareksekusi.
# masuk ke folder proyek
cd konverter-suhu
# salin berkas contoh di samping proyek ke tempat semestinya
cp ../src/konverter-suhu.cr src/konverter-suhu.cr
cp ../spec/konverter-suhu_spec.cr spec/konverter-suhu_spec.cr
cp ../spec/validasi_spec.cr spec/validasi_spec.cr
# jalankan seluruh spec
# baris "Finished in" disaring karena waktunya selalu berubah
crystal spec 2>&1 | grep -v "^Finished in"......*
Pending:
konversi kelvin belum ditulis
7 examples, 0 failures, 0 errors, 1 pendingBaris pertama berisi satu tanda per contoh, yaitu titik untuk contoh yang lolos dan bintang untuk contoh tertunda. Urutan tandanya mengikuti urutan berkas yang dikumpulkan crystal spec pada eksekusi tersebut dan dapat berbeda antarlingkungan; pada keluaran ini contoh validasi_spec.cr dieksekusi lebih dahulu, disusul lima contoh dan satu pending pada konverter-suhu_spec.cr. Ringkasan menyebut tujuh contoh tanpa kegagalan dan satu tertunda, yang cocok dengan lima contoh pada berkas utama, satu pada validasi_spec.cr, dan satu pending.
Spec dapat dibatasi pada satu berkas dengan menyebut jalurnya, dan pada satu contoh dengan menambahkan nomor baris setelah tanda titik dua.
# jalankan hanya satu berkas spec
crystal spec spec/konverter-suhu_spec.cr 2>&1 | grep -v "^Finished in".....*
Pending:
konversi kelvin belum ditulis
6 examples, 0 failures, 0 errors, 1 pending# jalankan hanya contoh yang diawali pada baris 5
crystal spec spec/konverter-suhu_spec.cr:5 2>&1 | grep -v "^Finished in".
1 examples, 0 failures, 0 errors, 0 pendingPembatasan berkas membuang contoh dari validasi_spec.cr, sehingga ringkasannya enam contoh. Pembatasan baris 5 menjalankan hanya contoh it yang diawali pada baris tersebut, sehingga ringkasannya satu contoh.
Kesalahan umum#
- Kompilasi berhenti dengan pesan
Error: undefined method 'expect' for top-level. Penyebabnya adalah menulis asersi gaya RSpec sepertiexpect(nilai).to eq(...), yang tidak disediakan framework spec Crystal. Tulis asersinya sebagainilai.should eq(...), dan gunakanexpect_raisesuntuk blok yang melempar exception. - Contoh gagal dengan perbedaan kecil pada nilai pecahan, misalnya hasil konversi yang seharusnya 98.6. Penyebabnya adalah matcher
eqyang membandingkan pecahan secara persis sementara hasil aritmetika pecahan dapat berbeda tipis. Gunakanbe_close(expected, delta)dengan toleransi yang wajar. - Contoh gagal dengan exception yang justru sedang diuji. Penyebabnya adalah pemanggilan metode yang melempar exception di badan contoh, di luar blok
expect_raises. Bungkus pemanggilannya dalam blokexpect_raisesagar exception itu tertangkap dan diperiksa. - Berkas spec baru tidak pernah dijalankan sama sekali. Penyebabnya adalah nama berkas tidak berakhiran _spec.cr atau lokasinya di luar folder spec/, sehingga tidak ditemukan oleh
crystal spec. Simpan berkas pengujian sebagai folder/spec/nama_spec.cr.
Ringkasan#
- Struktur spec terdiri atas
describeyang mengelompokkan danityang memuat satu contoh pengujian, dengan spec_helper.cr sebagai titik muat kode proyek. - Asersi ditulis dengan
shoulddiikuti matcher sepertieq,be_a,be_nil, danbe_close, sedangkan exception diuji dengan blokexpect_raises. pendingmendaftarkan contoh yang belum ditulis, ditandai bintang pada laporan, dan tidak dieksekusi.crystal specmenjalankan seluruh folder spec, satu berkas tertentu, atau satu contoh tertentu melalui formatjalur:baris.
Referensi#
- Referensi bahasa Crystal 1.22 — Crystal Lang — Panduan resmi perkakas baris perintah crystal dan framework spec.
- API Crystal 1.22.1 — Crystal Lang — Dokumentasi pustaka standar yang memuat modul spec beserta matcher-nya.