Framework spec

Menulis pengujian dengan describe, it, dan matcher expect.

Daftar isi

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.

bash
# bersihkan sisa proyek lama bila ada, lalu buat kerangka proyek
rm -rf konverter-suhu
crystal init app konverter-suhu
text Keluaran
    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/
text Struktur proyek konverter-suhu
konverter-suhu/
├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── shard.yml
├── spec/
│   ├── konverter-suhu_spec.cr
│   └── spec_helper.cr
└── src/
    └── konverter-suhu.cr

Folder 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.

crystal src/konverter-suhu.cr
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
end

Modul 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.

crystal Contoh: kelompok describe bersarang dengan contoh it
# 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
end

Berkas 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.

crystal spec/konverter-suhu_spec.cr
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.

crystal Contoh: matcher be_a dan eq pada satu contoh
# tidak-dijalankan
it "mengembalikan nilai bertipe String" do
  hasil = Konverter::Suhu.kategori(18.0)
  hasil.should be_a(String)
  hasil.should eq("sejuk")
end
crystal Contoh: matcher be_close untuk hasil pecahan
# tidak-dijalankan
it "membandingkan hasil pecahan dengan be_close" do
  Konverter::Suhu.celsius_ke_fahrenheit(37.0).should be_close(98.6, 0.01)
end

Pada 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.

crystal spec/validasi_spec.cr
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
crystal Contoh: expect_raises dengan kelas dan pesan
# tidak-dijalankan
expect_raises(ArgumentError, "celsius di bawah nol absolut") do
  Konverter::Suhu.validasi!(-300.0)
end

Pemanggilan 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.

crystal Contoh: contoh yang belum ditulis
# 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.

bash
# 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"
text Keluaran
......*

Pending:
  konversi kelvin belum ditulis

7 examples, 0 failures, 0 errors, 1 pending

Baris 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.

bash
# jalankan hanya satu berkas spec
crystal spec spec/konverter-suhu_spec.cr 2>&1 | grep -v "^Finished in"
text Keluaran
.....*

Pending:
  konversi kelvin belum ditulis

6 examples, 0 failures, 0 errors, 1 pending
bash
# jalankan hanya contoh yang diawali pada baris 5
crystal spec spec/konverter-suhu_spec.cr:5 2>&1 | grep -v "^Finished in"
text Keluaran
.

1 examples, 0 failures, 0 errors, 0 pending

Pembatasan 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 seperti expect(nilai).to eq(...), yang tidak disediakan framework spec Crystal. Tulis asersinya sebagai nilai.should eq(...), dan gunakan expect_raises untuk blok yang melempar exception.
  • Contoh gagal dengan perbedaan kecil pada nilai pecahan, misalnya hasil konversi yang seharusnya 98.6. Penyebabnya adalah matcher eq yang membandingkan pecahan secara persis sementara hasil aritmetika pecahan dapat berbeda tipis. Gunakan be_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 blok expect_raises agar 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 describe yang mengelompokkan dan it yang memuat satu contoh pengujian, dengan spec_helper.cr sebagai titik muat kode proyek.
  • Asersi ditulis dengan should diikuti matcher seperti eq, be_a, be_nil, dan be_close, sedangkan exception diuji dengan blok expect_raises.
  • pending mendaftarkan contoh yang belum ditulis, ditandai bintang pada laporan, dan tidak dieksekusi.
  • crystal spec menjalankan seluruh folder spec, satu berkas tertentu, atau satu contoh tertentu melalui format jalur:baris.

Referensi#