dart test menjalankan test yang ditulis dengan package test pada sebuah proyek Dart, lalu melaporkan hasilnya dalam keluaran yang ringkas. Topik ini membahas penyiapan proyek dengan dependensi test, pengelompokan test dengan group, penulisan ekspektasi dengan expect, equals, dan throwsA, pembacaan keluaran lulus maupun keluaran kegagalan, serta kebiasaan menulis test sebelum kodenya. Seluruh contoh dijalankan dengan Dart SDK 3.13 pada proyek antrean loket sederhana, sehingga setiap keluaran dapat direproduksi ulang.
Menyiapkan proyek dengan dependensi test#
dart test mengeksekusi test yang ditulis dengan package test, yaitu package resmi yang menyediakan fungsi test, group, dan expect. Package tersebut tidak disediakan secara default pada proyek baru, sehingga proyek yang membutuhkannya harus mendeklarasikannya pada pubspec.yaml. Tempat yang dianjurkan adalah dev_dependencies, bagian dependensi yang hanya diperlukan saat pengembangan dan tidak dibutuhkan ketika aplikasi dijalankan oleh pengguna akhir. Pada proyek hasil dart create, perintah dart pub add test menambahkan entri yang sama secara otomatis, sedangkan contoh pada topik ini menuliskan entri itu secara manual.
Proyek latihannya bernama antrean_loket dan berisi satu kelas antrean untuk loket tiket. Berkas pubspec.yaml berikut memuat nama package, deskripsi, versi, batasan versi SDK, dan dependensi test pada bagian dev_dependencies. Batasan test: ^1.33.0 berarti versi 1.33.0 ke atas pada keluarga versi yang sama, dan pub akan memilih versi terbaru yang cocok saat resolusi berlangsung.
name: antrean_loket
description: Antrean loket sederhana untuk latihan dart test.
version: 1.0.0
environment:
sdk: ^3.13.0
dev_dependencies:
test: ^1.33.0Blok berikut membuat proyek tersebut dari nol, lalu menjalankan dart pub get untuk mengunduh package test beserta seluruh dependensi turunannya.
# buat proyek antrean loket dari nol
rm -rf ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
mkdir -p ~/contoh/alat-dan-kualitas/dart-test/antrean_loket/lib ~/contoh/alat-dan-kualitas/dart-test/antrean_loket/test
cd ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
export PATH=/home/kal/dart-sdk/bin:$PATH
cat > pubspec.yaml <<'EOF'
name: antrean_loket
description: Antrean loket sederhana untuk latihan dart test.
version: 1.0.0
environment:
sdk: ^3.13.0
dev_dependencies:
test: ^1.33.0
EOF
dart pub getResolving dependencies...
Downloading packages...
+ _fe_analyzer_shared 111.0.0
+ analyzer 14.7.0
+ args 2.7.0
+ async 2.13.1
+ boolean_selector 2.1.2
+ cli_config 0.2.0
+ cli_util 0.6.0
+ collection 1.19.1
+ convert 3.1.2
+ coverage 1.16.0
+ crypto 3.0.8
+ ffi 2.2.0
+ file 7.0.1
+ frontend_server_client 4.2.0
+ glob 2.2.0
+ http_multi_server 3.2.3
+ http_parser 4.1.2
+ io 1.1.0
+ logging 1.3.0
+ matcher 0.12.20
+ meta 1.19.0
+ mime 2.1.0
+ node_preamble 2.0.2
+ package_config 3.0.0
+ path 1.9.1
+ pool 1.5.3
+ pub_semver 2.2.1
+ shelf 1.4.3
+ shelf_packages_handler 3.0.2
+ shelf_static 1.2.0
+ shelf_web_socket 3.0.0
+ source_map_stack_trace 2.1.2
+ source_maps 0.10.14
+ source_span 1.10.2
+ stack_trace 1.12.2
+ stream_channel 2.1.4
+ string_scanner 1.4.1
+ term_glyph 1.2.2
+ test 1.33.0
+ test_api 0.7.15
+ test_core 0.6.21
+ typed_data 1.4.0
+ vm_service 15.4.0
+ watcher 1.2.1
+ web 1.1.2
+ web_socket 1.0.1
+ web_socket_channel 3.0.3
+ webkit_inspection_protocol 1.2.1
+ yaml 3.1.4
Changed 49 dependencies!Keluaran dimulai dengan Resolving dependencies..., yaitu pub menghitung kombinasi versi yang cocok untuk seluruh package. Daftar di bawahnya memuat 49 package, dan hampir semuanya adalah dependensi turunan yang dibutuhkan oleh test, misalnya pustaka matcher untuk menyatakan syarat ekspektasi dan stack_trace untuk jejak kegagalan. Baris penutupnya, Changed 49 dependencies!, melaporkan jumlah package yang baru masuk ke proyek. Folder .dart_tool dibuat oleh perintah ini dan berisi pemetaan nama package ke lokasinya pada pub cache, sehingga dart test tahu dari mana package:test harus diimpor.
Mengelompokkan test dengan group#
Fungsi group dari package test mengelompokkan test yang menguji satu hal yang sama, misalnya satu kelas atau satu fungsi. Nama group digabungkan dengan nama setiap test di dalamnya saat keluaran ditampilkan, sehingga asal setiap hasil mudah dilacak. Fungsi test menyatakan satu perilaku yang ditegaskan, dan test di dalam group menyiapkan objeknya masing-masing agar satu test tidak memengaruhi test lain. Berkas test diletakkan pada folder test dan wajib berakhiran _test.dart, karena dart test hanya mencari berkas dengan pola tersebut secara default.
Kode yang diuji adalah kelas AntreanLoket berikut, yaitu kelas antrean sederhana yang menyimpan nama pengunjung dan melayani satu nama terdepan setiap kali layani dipanggil.
class AntreanLoket {
final List<String> _nama = [];
void datang(String nama) {
_nama.add(nama);
}
String layani() {
if (_nama.isEmpty) {
throw StateError('antrean kosong');
}
return _nama.removeAt(0);
}
int get jumlah => _nama.length;
}Berkas test berikut mengelompokkan tiga test dalam satu group bernama AntreanLoket. Test pertama menegaskan jumlah antrean bertambah satu setelah satu nama datang. Test kedua menegaskan bahwa pelayanan mengikuti urutan kedatangan. Test ketiga menegaskan bahwa pemanggilan layani pada antrean kosong melempar error bertipe StateError.
import 'package:antrean_loket/antrean.dart';
import 'package:test/test.dart';
void main() {
group('AntreanLoket', () {
test('menambahkan nama ke antrean', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
expect(antrean.jumlah, equals(1));
});
test('melayani sesuai urutan kedatangan', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
antrean.datang('Budi');
expect(antrean.layani(), equals('Ali'));
expect(antrean.layani(), equals('Budi'));
});
test('melempar error saat antrean kosong', () {
final antrean = AntreanLoket();
expect(() => antrean.layani(), throwsA(isA<StateError>()));
});
});
}Blok berikut menuliskan kedua berkas itu ke dalam proyek dan menjalankan dart test.
# tulis kode yang diuji beserta berkas testnya
cd ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
export PATH=/home/kal/dart-sdk/bin:$PATH
cat > lib/antrean.dart <<'EOF'
class AntreanLoket {
final List<String> _nama = [];
void datang(String nama) {
_nama.add(nama);
}
String layani() {
if (_nama.isEmpty) {
throw StateError('antrean kosong');
}
return _nama.removeAt(0);
}
int get jumlah => _nama.length;
}
EOF
cat > test/antrean_test.dart <<'EOF'
import 'package:antrean_loket/antrean.dart';
import 'package:test/test.dart';
void main() {
group('AntreanLoket', () {
test('menambahkan nama ke antrean', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
expect(antrean.jumlah, equals(1));
});
test('melayani sesuai urutan kedatangan', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
antrean.datang('Budi');
expect(antrean.layani(), equals('Ali'));
expect(antrean.layani(), equals('Budi'));
});
test('melempar error saat antrean kosong', () {
final antrean = AntreanLoket();
expect(() => antrean.layani(), throwsA(isA<StateError>()));
});
});
}
EOF
dart test00:00 +0: loading test/antrean_test.dart
00:00 +0: test/antrean_test.dart: AntreanLoket menambahkan nama ke antrean
00:00 +1: test/antrean_test.dart: AntreanLoket melayani sesuai urutan kedatangan
00:00 +2: test/antrean_test.dart: AntreanLoket melempar error saat antrean kosong
00:00 +3: All tests passed!Keluaran memuat satu baris untuk setiap test yang selesai dijalankan, diawali baris loading yang menandakan berkas test sedang disiapkan. Nama pada setiap baris tersusun dari tiga bagian, yaitu lokasi berkas, nama group, dan nama test, misalnya test/antrean_test.dart: AntreanLoket menambahkan nama ke antrean. Angka setelah waktu seperti +2 menyatakan jumlah test yang telah lulus sampai baris itu dicetak. Baris terakhirnya, All tests passed!, menegaskan ketiga test lulus dan perintah ditutup dengan kode keluar nol.
Ekspektasi dengan expect, equals, dan throwsA#
Fungsi expect menilai apakah sebuah nilai memenuhi syarat yang dinyatakan oleh matcher, yaitu objek kecil yang tugasnya memeriksa satu sifat nilai. Argumen pertamanya adalah nilai aktual yang diperoleh dari kode yang diuji, sedangkan argumen keduanya adalah matcher yang menjadi syaratnya. Matcher equals menyatakan kesamaan nilai, dan pembandingannya bersifat mendalam pada koleksi, sehingga dua List dengan isi sama dinilai sama. Apabila syaratnya tidak terpenuhi, test langsung gagal dan pesannya menjelaskan nilai yang diharapkan beserta nilai aktualnya.
Untuk perilaku yang berupa pelemparan error, syaratnya dinyatakan dengan throwsA yang menerima pemanggilan fungsi sebagai argumen pertamanya. Baris expect(() => antrean.layani(), throwsA(isA<StateError>())) pada berkas test menegaskan dua hal sekaligus, yaitu pemanggilan layani pada antrean kosong melempar error dan errornya bertipe StateError. Matcher isA menilai tipe objek, sehingga kombinasi throwsA(isA ...) tidak hanya melihat adanya pelemparan tetapi juga jenisnya. Package test memuat puluhan matcher lain, misalnya isNotEmpty untuk koleksi dan closeTo untuk bilangan desimal, sehingga sebagian besar syarat dapat ditulis tanpa percabangan manual.
Keluaran kegagalan uji#
Keluaran kegagalan adalah pesan yang sering dibaca selama pengembangan, sehingga bentuknya perlu dikenali dengan cara sengaja memicu satu kegagalan. Kegagalan yang umum terjadi adalah ekspektasi yang tidak sesuai dengan perilaku kode. Blok berikut mengubah ekspektasi pertama pada test kedua dari equals('Ali') menjadi equals('Budi'), menjalankan dart test, lalu mengembalikan isi berkas ke versi yang benar.
# ubah ekspektasi menjadi nilai yang salah lalu jalankan test
cd ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
export PATH=/home/kal/dart-sdk/bin:$PATH
cat > test/antrean_test.dart <<'EOF'
import 'package:antrean_loket/antrean.dart';
import 'package:test/test.dart';
void main() {
group('AntreanLoket', () {
test('menambahkan nama ke antrean', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
expect(antrean.jumlah, equals(1));
});
test('melayani sesuai urutan kedatangan', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
antrean.datang('Budi');
expect(antrean.layani(), equals('Budi'));
expect(antrean.layani(), equals('Budi'));
});
test('melempar error saat antrean kosong', () {
final antrean = AntreanLoket();
expect(() => antrean.layani(), throwsA(isA<StateError>()));
});
});
}
EOF
dart test
# kembalikan isi berkas test ke versi yang benar
cat > test/antrean_test.dart <<'EOF'
import 'package:antrean_loket/antrean.dart';
import 'package:test/test.dart';
void main() {
group('AntreanLoket', () {
test('menambahkan nama ke antrean', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
expect(antrean.jumlah, equals(1));
});
test('melayani sesuai urutan kedatangan', () {
final antrean = AntreanLoket();
antrean.datang('Ali');
antrean.datang('Budi');
expect(antrean.layani(), equals('Ali'));
expect(antrean.layani(), equals('Budi'));
});
test('melempar error saat antrean kosong', () {
final antrean = AntreanLoket();
expect(() => antrean.layani(), throwsA(isA<StateError>()));
});
});
}
EOF00:00 +0: loading test/antrean_test.dart
00:00 +0: test/antrean_test.dart: AntreanLoket menambahkan nama ke antrean
00:00 +1: test/antrean_test.dart: AntreanLoket melayani sesuai urutan kedatangan
00:00 +1 -1: test/antrean_test.dart: AntreanLoket melayani sesuai urutan kedatangan [E]
Expected: 'Budi'
Actual: 'Ali'
Which: is different.
Expected: Budi
Actual: Ali
^
Differ at offset 0
package:matcher expect
test/antrean_test.dart 16:7 main.<fn>.<fn>
00:00 +1 -1: test/antrean_test.dart: AntreanLoket melempar error saat antrean kosong
00:00 +2 -1: Some tests failed.
Failing tests:
test/antrean_test.dart: AntreanLoket melayani sesuai urutan kedatangan
Consider enabling the flag chain-stack-traces to receive more detailed exceptions.
For example, 'dart test --chain-stack-traces'.Penanda [E] pada baris test melaporkan kegagalan, dan blok yang menjorok di bawahnya memuat penyebabnya. Baris Expected dan Actual menampilkan nilai yang disyaratkan serta nilai yang benar-benar diperoleh, sedangkan baris Which memperlihatkan letak perbedaannya, yaitu Differ at offset 0 karena huruf pertamanya berbeda. Baris test/antrean_test.dart 16:7 menunjuk baris dan kolom persis tempat expect dipanggil, sehingga lokasi perbaikannya langsung diketahui. Angka +2 -1 pada baris akhir berarti dua test lulus dan satu test gagal, dan keluaran ditutup dengan kalimat Some tests failed. beserta daftar test yang gagal. Perintah juga ditutup dengan kode keluar bukan nol sehingga kegagalan ini dapat dideteksi oleh alat otomatis.
Kebiasaan test-driven#
Kebiasaan test-driven development meletakkan penulisan test sebelum penulisan kodenya. Siklusnya sederhana, yaitu menulis test yang menegaskan perilaku baru, menjalankannya untuk melihat kegagalan, menulis kode minimum sampai test lulus, lalu merapikan kode tanpa mengubah perilakunya. Keluaran kegagalan pada bagian sebelumnya memperlihatkan langkah pertama siklus tersebut, yaitu test yang menegaskan perilaku yang belum dipenuhi kode. Kegagalan yang informatif menandakan testnya sudah benar, sehingga pekerjaan berikutnya hanya membuat test itu lulus.
Selama siklus berjalan, menjalankan seluruh test setiap kali kode berubah membuat umpan balik lambat, padahal yang sedang dikerjakan biasanya hanya satu test. Opsi --plain-name hanya menjalankan test yang namanya memuat potongan nama yang diberikan. Blok berikut mengeksekusi satu dari tiga test yang ada, dan keluarannya menunjukkan satu test saja yang berjalan.
# jalankan hanya satu test yang disorot
cd ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
export PATH=/home/kal/dart-sdk/bin:$PATH
dart test --plain-name 'menambahkan nama ke antrean'00:00 +0: loading test/antrean_test.dart
00:00 +0: test/antrean_test.dart: AntreanLoket menambahkan nama ke antrean
00:00 +1: All tests passed!Kesalahan umum#
Kesalahan yang umum ditemui adalah memanggil fungsi test atau expect tanpa mengimpor package test. Fungsi-fungsi itu tidak dikenal oleh Dart sehingga berkas test tidak dapat dikompilasi, dan dart test menampilkan pesan kompilasi beserta penanda bahwa berkasnya gagal dimuat. Blok berikut membuat berkas test kecil tanpa import package:test, menjalankannya dengan menyebut lokasinya secara eksplisit, lalu menghapus berkas percobaan tersebut.
# buat berkas test tanpa import package:test lalu jalankan
cd ~/contoh/alat-dan-kualitas/dart-test/antrean_loket
export PATH=/home/kal/dart-sdk/bin:$PATH
cat > test/cek_cepat_test.dart <<'EOF'
import 'package:antrean_loket/antrean.dart';
void main() {
test('antrean baru masih kosong', () {
expect(AntreanLoket().jumlah, equals(0));
});
}
EOF
dart test test/cek_cepat_test.dart
# hapus berkas percobaan
rm test/cek_cepat_test.dart00:00 +0: loading test/cek_cepat_test.dart
00:00 +0 -1: loading test/cek_cepat_test.dart [E]
Failed to load "test/cek_cepat_test.dart":
test/cek_cepat_test.dart:5:35: Error: Method not found: 'equals'.
expect(AntreanLoket().jumlah, equals(0));
^^^^^^
test/cek_cepat_test.dart:5:5: Error: Method not found: 'expect'.
expect(AntreanLoket().jumlah, equals(0));
^^^^^^
test/cek_cepat_test.dart:4:3: Error: Method not found: 'test'.
test('antrean baru masih kosong', () {
^^^^
00:00 +0 -1: Some tests failed.
Failing tests:
test/cek_cepat_test.dart: loading test/cek_cepat_test.dartBaris Failed to load melaporkan berkas yang tidak dapat dikompilasi, dan setiap baris Error di bawahnya menunjuk fungsi yang tidak ditemukan beserta baris dan kolomnya. Tiga fungsi yang dilaporkan, yaitu test, expect, dan equals, semuanya berasal dari package test yang tidak diimpor. Perbaikannya adalah menambahkan import 'package:test/test.dart'; di bagian atas berkas, sejajar dengan import 'package:antrean_loket/antrean.dart'; yang membawa kelas yang diuji. Pesan semacam ini muncul sebelum test mana pun dieksekusi, sehingga counter keluarannya tertahan pada +0 -1.
Ringkasan#
- dart test mengeksekusi berkas berakhiran _test.dart pada folder test, dengan package test yang dideklarasikan pada dev_dependencies dan diunduh oleh dart pub get.
- Fungsi group mengelompokkan test dan menggabungkan nama berkas, nama group, serta nama test pada keluaran, sedangkan expect menilai nilai aktual dengan matcher seperti equals, isA, dan throwsA.
- Keluaran kegagalan memuat penanda [E], pasangan Expected dan Actual, letak perbedaan pada baris Which, lokasi baris expect, counter seperti +2 -1, kalimat Some tests failed., serta kode keluar bukan nol.
- Opsi –plain-name menjalankan satu test saja selama iterasi, sedangkan dart test tanpa opsi dijalankan sebelum commit untuk memastikan seluruh test lulus.
Referensi#
- Dart testing — dart.dev — Panduan resmi penulisan test di Dart, termasuk konvensi folder test, penamaan berkas, dan dev_dependencies.
- test package — pub.dev — Halaman resmi package test yang memuat daftar matcher serta contoh pengelompokan test.
- dart tool — dart.dev — Referensi perintah pada alat dart, termasuk dart test beserta opsinya seperti –plain-name.