Rutin numerik memiliki sifat yang menjadikan pengujian otomatis penting: program tetap berjalan dan mencetak angka meskipun angka yang dicetak keliru. Topik ini membangun satu rangkaian pengujian (test suite) dengan pustaka test-drive pada proyek fpm. Rangkaian itu kemudian dijalankan terhadap implementasi yang sengaja dikelirukan, diperbaiki, dan dipakai sebagai gerbang pada continuous integration (CI). Kerangka pFUnit dibahas sebagai alternatif yang lebih lengkap. Seluruh kode tersedia di direktori topik ini dan setiap perintah dijalankan dari sana.
Struktur proyek pengujian#
Proyek pengujian memakai struktur fpm standar: fungsi yang diuji berada di src/, sedangkan berkas pengujian berada di test/. Manifest proyek mendeklarasikan test-drive sebagai dependensi git, sehingga fpm test dapat mengunduh dan memakainya tanpa pemasangan manual.
name = "uji-rms"
version = "0.1.0"
[dependencies]
test-drive = { git = "https://github.com/fortran-lang/test-drive" }Fungsi yang diuji adalah rms, akar kuadrat rata-rata (root mean square) dari sebuah vektor. Metrik ini lazim dipakai untuk menakar besar kesalahan sebuah solusi numerik karena mengagregasi simpangan setiap elemen menjadi satu angka, dan nilai rms yang kecil membatasi simpangan tiap elemen karena kontribusi kuadrat satu elemen yang menyimpang jauh langsung menaikkan hasilnya. Untuk vektor konstanta seperti [5.0, 5.0], nilainya sama dengan nilai elemennya, sebuah sifat yang menyediakan kasus uji pertama yang mudah dinilai.
module rms_mod
implicit none
private
public :: rms
contains
pure function rms(nilai) result(hasil)
real, intent(in) :: nilai(:)
real :: hasil
hasil = norm2(nilai) / sqrt(real(size(nilai)))
end function rms
end module rms_modModul pengujian menyusun daftar kasus uji dengan new_unittest, dan setiap kasus menerima error_type yang diisi bila pemeriksaan gagal. Prosedur check membandingkan kondisi logika dan melampirkan pesan bila gagal. Nilai acuan ditulis sebagai konstanta, dan pemeriksaannya membandingkan selisih dengan toleransi ketat alih-alih menggunakan ==, karena kesamaan penuh nilai floating point tidak dapat diandalkan.
Toleransi 1.0e-6 yang dipakai kedua kasus bersifat mutlak karena nilai acuannya berorde satu, sedangkan kesalahan pembulatan single precision pada perhitungan semacam ini jauh di bawah angka itu. Untuk besaran yang dapat sangat besar atau sangat kecil, toleransi relatif yang menakar selisih terhadap nilai acuannya lebih tepat karena menuntut ketelitian yang seragam pada skala mana pun. Pilihan toleransi dengan demikian merupakan bagian dari spesifikasi pengujian, bukan sekadar angka tempelan pada perbandingan.
module cek_rms
use testdrive, only : unittest_type, new_unittest
use rms_mod, only : rms
implicit none
private
public :: collect_suite
contains
subroutine collect_suite(tests)
type(unittest_type), allocatable, intent(out) :: tests(:)
tests = [ new_unittest("cek_konstanta", cek_konstanta), &
& new_unittest("cek_dua_nilai", cek_dua_nilai) ]
end subroutine collect_suite
subroutine cek_konstanta(error)
use testdrive, only : error_type, check
type(error_type), allocatable, intent(out) :: error
call check(error, abs(rms([5.0, 5.0]) - 5.0) < 1.0e-6, "rms vektor konstanta")
end subroutine cek_konstanta
subroutine cek_dua_nilai(error)
use testdrive, only : error_type, check
type(error_type), allocatable, intent(out) :: error
call check(error, abs(rms([3.0, 4.0]) - 3.5355339) < 1.0e-6, "rms dua nilai")
end subroutine cek_dua_nilai
end module cek_rmsProgram pengemudi pada pengemudi.f90 mengumpulkan rangkaian pengujian dengan new_testsuite, menjalankannya lewat run_testsuite, dan menghentikan program dengan kode keluar bukan nol bila ada kasus yang gagal. Variabel stat dikumpulkan lintas seluruh rangkaian, sehingga satu kegagalan pada satu kasus pun tidak dapat lolos diam-diam. Kode keluar inilah yang nantinya dibaca oleh CI.
program pengemudi
use, intrinsic :: iso_fortran_env, only : error_unit
use testdrive, only : run_testsuite, new_testsuite, testsuite_type
use cek_rms, only : collect_suite
implicit none
integer :: stat, is
type(testsuite_type), allocatable :: testsuites(:)
character(len=*), parameter :: fmt = '("Testing: ", a)'
testsuites = [ new_testsuite("cek_rms", collect_suite) ]
stat = 0
do is = 1, size(testsuites)
write(error_unit, fmt) testsuites(is)%name
call run_testsuite(testsuites(is)%collect, error_unit, stat)
end do
if (stat > 0) then
write(error_unit, '(i0, 1x, a)') stat, "test(s) failed!"
error stop 1
end if
end program pengemudiMenangkap implementasi yang keliru#
Cara paling meyakinkan untuk melihat nilai sebuah pengujian adalah menjalankannya terhadap kode yang keliru. Blok berikut menyusun proyek pada direktori sementara, tetapi dengan rms yang menghitung rata-rata aritmetika alih-alih akar kuadrat rata-rata. Kekeliruan yang ditanam sengaja dipilih yang dekat dengan aslinya: rata-rata aritmetika tetap menghasilkan angka yang tampak masuk akal, persis seperti kutu (bug) sungguhan yang biasanya memunculkan keluaran wajar alih-alih angka mustahil. Baris kemajuan bangun fpm disembunyikan dengan saringan grep karena urutannya bergantung pada penjadwalan bangun, dan hanya baris hasil pengujian yang ditampilkan.
# susun proyek pengujian pada direktori sementara
rm -rf /tmp/uji-rms && fpm new /tmp/uji-rms > /dev/null 2>&1
rm /tmp/uji-rms/test/check.f90
cp cek_rms.f90 pengemudi.f90 /tmp/uji-rms/test/
cat > /tmp/uji-rms/fpm.toml <<'EOF'
name = "uji-rms"
version = "0.1.0"
[dependencies]
test-drive = { git = "https://github.com/fortran-lang/test-drive" }
EOF
# rms yang keliru: rata-rata aritmetika
cat > /tmp/uji-rms/src/rms.f90 <<'EOF'
module rms_mod
implicit none
private
public :: rms
contains
pure function rms(nilai) result(hasil)
real, intent(in) :: nilai(:)
real :: hasil
hasil = sum(nilai) / real(size(nilai))
end function rms
end module rms_mod
EOF
# jalankan seluruh kasus uji
(cd /tmp/uji-rms && fpm test 2>&1 | grep -E "Testing|PASSED|FAILED|failed|STOP")Testing: cek_rms
... cek_konstanta [PASSED]
... cek_dua_nilai [FAILED]
1 test(s) failed!
ERROR STOP 1
<ERROR> *cmd_run*:stopping due to failed executions
STOP 1Keluaran tersebut memperlihatkan dua hal sekaligus. Kasus cek_dua_nilai menangkap kekeliruan karena akar kuadrat rata-rata dari [3, 4] adalah 3,5355, sedangkan rata-rata aritmetikanya 3,5. Sebaliknya kasus cek_konstanta tetap lolos pada kode yang keliru karena rata-rata aritmetika dari vektor konstanta memang sama dengan rms-nya, sehingga satu kasus uji yang lolos tidak pernah menjadi bukti kode yang benar. Program pengemudi juga berhenti dengan ERROR STOP 1, dan fpm meneruskannya sebagai kode keluar bukan nol.
Perbaikan dan disiplin pengujian pada CI#
Salinan rms.f90 yang benar dari direktori topik menggantikan versi keliru, dan seluruh rangkaian dijalankan ulang.
# ganti dengan rms yang benar, lalu jalankan ulang pengujian
cp rms.f90 /tmp/uji-rms/src/rms.f90
(cd /tmp/uji-rms && fpm test 2>&1 | grep -E "Testing|PASSED|FAILED|failed|STOP")Testing: cek_rms
... cek_konstanta [PASSED]
... cek_dua_nilai [PASSED]Kode keluar fpm test inilah yang menjadi gerbang CI: perubahan kode hanya digabungkan (merge) bila seluruh kasus uji lolos. Gerbang semacam ini menjawab kelemahan pemeriksaan manual: perbaikan yang menyelesaikan satu jalur dapat merusak jalur lain tanpa disadari penulisnya, dan tanpa pengujian yang dijalankan ulang otomatis, kerusakan itu baru ketahuan jauh setelah perubahan penyebabnya masuk. Pada workflow berbasis GitHub Actions, langkahnya cukup satu baris setelah lingkungan fpm disiapkan.
- run: fpm testKerangka pFUnit menawarkan pendekatan yang lebih lengkap untuk proyek besar. Fiturnya mencakup pernyataan siap pakai assertEquals untuk nilai floating point dengan toleransi, pengujian prosedur dengan argumen terikat (mock), dan pelaporan dalam format JUnit yang dibaca banyak server CI. pFUnit memakai pra-pemroses (preprocessor) untuk memperluas makro pernyataannya, sehingga pemasangannya lebih berat daripada test-drive yang cukup dideklarasikan pada fpm.toml.
Kesalahan umum#
- Membandingkan nilai floating point dengan
==. Perubahan urutan operasi yang tak terhindarkan antarkompiler membuat kesamaan ketat menjadi rapuh. Bandingkan selisih terhadap toleransi yang dinyatakan. - Menulis kasus uji yang tidak pernah bisa gagal, misalnya memanggil prosedur tanpa memeriksa hasilnya. Kasus seperti itu menaikkan jumlah kasus tanpa menaikkan jaminan apa pun.
- Mengabaikan kode keluar pengemudi. Tanpa
error stopsaat ada kegagalan, CI melihat pengujian yang gagal sebagai lolos dan menggabungkan kode yang keliru.
Ringkasan#
- test-drive menyusun kasus uji lewat
new_unittestdannew_testsuite, dengan setiap kasus menerimaerror_typeyang diisi prosedurcheckbila pemeriksaan gagal. - Rangkaian pada topik ini menangkap implementasi rms yang keliru melalui kasus
cek_dua_nilai, tetapi juga menunjukkan bahwa kasuscek_konstantalolos pada kode yang keliru. - Kode keluar
fpm testmenjadi gerbang CI, dan pFUnit menjadi alternatif untuk proyek yang butuh pernyataan numerik dan pelaporan JUnit.
Referensi#
- test-drive — fortran-lang — Dokumentasi dan contoh resmi pustaka test-drive yang dipakai pada topik ini.
- Manifest reference — fpm
— Spesifikasi bagian
[dependencies]padafpm.toml. - pFUnit — Goddard Fortran Ecosystem — Dokumentasi kerangka pFUnit beserta pernyataan numerik dan pelaporan JUnit-nya.