BARU: Helius mengakuisisi Light Protocol
Panduan Menguji Program Solana
Blog/Pengembangan

Panduan Menguji Program Solana

Developer Experience Engineer0xIchigo di X0xIchigo di LinkedIn0xIchigo di GitHub
Bacaan 27 menit

Pendahuluan

Pengujian di lingkungan blockchain melampaui paradigma pengujian perangkat lunak tradisional serta menghadirkan tantangan unik dan konsekuensi yang lebih besar. Dalam lingkungan Solana yang memiliki throughput tinggi dan latensi rendah, margin kesalahannya sangat sempit. Pengujian otomatis bukan sekadar praktik terbaik, melainkan kebutuhan mendasar untuk memastikan keandalan dan keamanan program yang beroperasi di lingkungan Solana yang dinamis dan tidak mengenal toleransi.

Artikel ini membahas jenis utama pengujian otomatis — Pengujian Unit, Pengujian Integrasi, dan Pengujian Menyeluruh (E2E). Artikel ini juga membahas cara menulis pengujian unit dasar dalam JavaScript/TypeScript dan Rust sebelum menganalisis framework pengujian Solana yang populer. Artikel ini diakhiri dengan contoh praktis yang menguji program game “King of the Hill”.

Jika Anda baru mengenal Solana, saya menyarankan agar Anda membaca postingan blog berikut terlebih dahulu:

Artikel ini juga melengkapi artikel kami sebelumnya tentang keamanan program Solana. Saya menyarankan agar Anda membaca keduanya secara bersamaan.

Apa itu pengujian?

Pengujian dilakukan untuk memastikan bahwa suatu bagian kode atau keseluruhan aplikasi bekerja sebagaimana mestinya. Ada dua jenis pengujian umum:

  • Pengujian Manual: proses yang berpusat pada manusia, dengan kasus pengujian dijalankan oleh developer, analis jaminan kualitas, penguji penetrasi, atau pihak lain yang bertanggung jawab
  • Pengujian Otomatis: proses yang berfokus pada kode, dengan skrip ditulis untuk menjalankan kasus pengujian yang telah ditentukan secara terprogram

Pengujian manual merupakan proses yang sangat fleksibel dan tidak bergantung pada jenis aplikasi yang diuji. Metode ini cocok untuk menguji fitur baru, kegunaan, dan aksesibilitas. Pengujian manual bergantung pada intuisi penguji mengenai cara aplikasi seharusnya berperilaku. Namun, metode ini pada dasarnya lambat, rentan terhadap kesalahan, memakan waktu, dan sering kali tidak lengkap (yaitu, tidak semua skenario tercakup) karena minimnya alat yang digunakan dalam proses pengujian. 

Pengujian otomatis bertujuan mengatasi kekurangan pengujian manual. Misalnya, metode ini biasanya lebih cepat, terutama jika pengujian dijalankan secara paralel. Pengujian otomatis tidak terlalu rentan terhadap kesalahan manusia karena mengikuti skrip yang telah ditentukan. Metode ini meningkatkan cakupan pengujian berkat kemampuannya menangani banyak kasus pengujian secara efisien sehingga menjadi solusi yang sangat mudah diskalakan. Namun, karena objektivitasnya yang kaku, pengujian otomatis kurang akurat untuk pengujian yang mengandalkan interaksi manusia, penilaian, atau penalaran kritis.

Untuk keperluan artikel ini, kita berfokus pada pengujian otomatis untuk program Solana karena mengujinya secara manual di mainnet akan cukup mahal dan mengujinya secara manual di devnet akan sangat memakan waktu. Namun, pengujian manual dan otomatis harus menjadi bagian dari proses pengujian sebelum kode dirilis ke produksi. Proses pengujian yang kuat dapat meminimalkan jumlah bug yang masuk ke produksi dengan mengidentifikasinya lebih awal selama pengembangan.

Ada berbagai jenis pengujian otomatis, yaitu:

  • Pengujian Unit
  • Pengujian Integrasi
  • Pengujian Menyeluruh (E2E)

Pengujian Unit

Pengujian unit adalah proses menguji unit fungsional terkecil dari kode untuk memastikan bahwa unit tersebut berfungsi dengan benar. Idealnya, unit adalah komponen penyusun terkecil dari suatu program (misalnya, fungsi atau modul individual) yang jika digabungkan akan membentuk produk akhir. Gagasan utamanya adalah bahwa keseluruhan program seharusnya bekerja sebagaimana mestinya jika kita menguji komponen penyusunnya secara menyeluruh. 

Pengujian unit merupakan fondasi pengembangan Solana karena memastikan setiap bagian dari suatu program berperilaku sebagaimana mestinya. Penggunaan kembali pengujian ini memastikan bahwa fitur baru atau pembaruan mematuhi spesifikasi proyek dan ekspektasi pengguna sebagaimana ditetapkan dalam kasus pengujian. Karena itu, pengujian unit secara alami mendorong optimalisasi dan refaktor kode agar peningkatan baru tidak berdampak buruk terhadap fungsionalitas program. Pengujian unit tidak hanya menjamin setiap segmen kode berfungsi dengan benar dalam berbagai kondisi pengujian, tetapi juga memastikan interaksi blockchain yang efisien dan aman. Mendeteksi bug sejak dini melalui pengujian unit sangat penting karena mencegah potensi kerentanan mencapai produksi. 

Berbagai framework pengujian membantu menyederhanakan pengujian unit dengan memudahkan proses simulasi kondisi jaringan dan pengelolaan status program, seperti yang akan kita lihat nanti dalam artikel ini. Developer Solana dapat mencapai tingkat keandalan dan performa kode yang tinggi melalui pengujian unit.

Pengujian Integrasi

Pengujian integrasi melangkah lebih jauh dari pengujian unit dengan memeriksa cara berbagai unit dalam suatu program bekerja bersama. Memastikan fungsi dan modul program bekerja bersama sangat penting dalam pengembangan Solana, karena interaksi program sering kali kompleks dan memiliki konsekuensi finansial. Pengujian integrasi bertujuan mengidentifikasi dan mengatasi masalah yang tidak langsung terlihat saat unit diuji secara terpisah, tetapi muncul ketika komponen berinteraksi. Masalah ini dapat mencakup ketidakcocokan format data, inkonsistensi tipe, dependensi program, atau masalah dengan API pihak ketiga.

Dalam konteks Solana, tempat program secara inheren berinteraksi dengan program lain, dompet, dan oracle, pengujian integrasi memastikan bahwa interaksi tersebut berlangsung sebagaimana mestinya. Meskipun setiap unit berfungsi sempurna, kombinasinya dapat menimbulkan perilaku tidak terduga atau inefisiensi yang muncul dalam kondisi simulasi ini. Developer dapat menggunakan berbagai framework pengujian untuk menyimulasikan beragam alur transaksi dan interaksi program sehingga sangat menyerupai skenario dunia nyata. Misalnya, Bankrun adalah framework pengujian ringan dan tangguh yang memungkinkan developer bergerak maju-mundur dalam waktu serta menetapkan data akun secara dinamis. Kemampuan ini tidak tersedia saat menggunakan solana-test-validator. Pengujian integrasi sangat penting untuk memastikan program tangguh, andal, dan siap menghadapi tuntutan kondisi jaringan Solana.

Pengujian Menyeluruh (E2E)

Pengujian Menyeluruh (E2E) merupakan puncak dari proses pengujian. Metode ini berfokus mengevaluasi keseluruhan alur operasional program sebagaimana berlangsung dalam skenario dunia nyata. Metodologi pengujian ini berbeda dari pengujian unit dan integrasi karena memeriksa program dari perspektif pengguna — semua kemungkinan alur dan fitur yang akan ditemui pengguna akhir harus bekerja sebagaimana mestinya. 

Pengujian E2E sangat penting untuk memastikan bahwa program memenuhi persyaratan fungsionalnya dan memberikan pengalaman pengguna yang lancar. Fase pengujian ini membantu mengungkap masalah yang mungkin tidak terlihat selama pengujian unit atau integrasi, seperti keterlambatan pemrosesan transaksi, masalah dalam mempertahankan status, optimalisasi unit komputasi, atau kondisi jaringan yang tidak terduga. Meskipun pengujian E2E biasanya diterapkan untuk menguji seluruh dApp, menguji alur operasional program dan memastikan cara transaksi pengguna berinteraksi dengan berbagai fungsi serta modul sangatlah penting untuk membangun program yang berhasil dan aman.

Menggabungkan Metodologi Pengujian Ini

Sangat penting untuk menggunakan strategi pengujian berlapis serta menggabungkan pengujian unit, integrasi, dan E2E dalam proses pengembangan Anda. Setiap metodologi pengujian memiliki peran tersendiri dalam siklus pengembangan dan menangani aspek yang berbeda dari fungsionalitas serta performa program.

Pengujian unit merupakan fondasi pendekatan pengujian berlapis yang memungkinkan developer dengan cepat mengidentifikasi dan mengatasi masalah pada tingkat kode paling terperinci. Meskipun unggul dalam memastikan kebenaran objektif setiap fungsi atau modul, pengujian ini tidak memperhitungkan cara unit-unit tersebut bekerja bersama atau kesesuaiannya dengan pengalaman pengguna.

Pengujian integrasi menjembatani kesenjangan ini dengan mengevaluasi cara berbagai unit berinteraksi satu sama lain untuk mengungkap masalah yang muncul ketika komponen tersebut diintegrasikan. Namun, pengujian ini saja mungkin tidak sepenuhnya menangkap pengalaman pengguna akhir atau perilaku program dalam kondisi dunia nyata.

Pengujian E2E melengkapi pengujian unit dan integrasi dengan menyimulasikan skenario pengguna di dunia nyata serta menguji aplikasi secara keseluruhan. Pendekatan ini sangat berharga untuk menilai pengalaman pengguna secara keseluruhan, tetapi tidak memberikan wawasan terperinci yang diperlukan untuk mengidentifikasi dan mengatasi masalah tertentu dengan cepat.

Dengan mengintegrasikan metodologi ini, developer dapat membuat framework pengujian tangguh yang mencakup seluruh spektrum potensi masalah. Pendekatan komprehensif tidak hanya meningkatkan kualitas dan keamanan program, tetapi juga menyederhanakan proses pengembangan. Developer dapat mengambil keputusan dan melakukan revisi dengan cepat karena yakin bahwa perubahan mereka akan diperiksa pada berbagai tingkat. Menggabungkan metodologi ini sangat penting untuk memastikan program Solana layak secara teknis dan selaras dengan ekspektasi pengguna dalam kondisi dunia nyata sebelum deployment.

Menulis pengujian yang efektif sangat penting untuk mengembangkan program Solana yang andal dan aman. Inti dari pengujian yang baik terletak pada fokus terhadap perilaku yang diuji, bukan framework yang digunakan atau detail implementasi kode. Developer dapat membuat strategi pengujian efisien yang meningkatkan kualitas kode dengan mengintegrasikan prinsip Test-Driven Development (TDD), pola Arrange-Act-Assert (AAA), dan praktik terbaik industri.

Test-Driven Development (TDD)

TDD adalah pendekatan pengembangan perangkat lunak tangguh yang dipandu oleh penulisan pengujian. Dengan kata lain, gagasan utamanya adalah menulis pengujian sebelum menulis kode yang sebenarnya. Siklus TDD biasanya mencakup tiga langkah:

  • Tulis Pengujian yang Gagal: Pengembangan harus dimulai dengan menulis pengujian untuk bagian fungsionalitas berikutnya yang ingin ditambahkan developer. Pengujian tersebut pasti gagal karena fungsionalitas yang diuji belum tersedia
  • Implementasikan Kode: Developer harus menulis kode seminimal mungkin agar pengujian berhasil. Tujuannya adalah kecepatan dan kesederhanaan
  • Refaktor: Setelah pengujian berhasil, developer harus melakukan refaktor kode untuk memperbaiki struktur dan kejelasannya tanpa mengubah perilakunya. Pengujian yang berhasil berfungsi sebagai jaring pengaman agar tidak menimbulkan perubahan yang merusak. Langkah ini dapat mencakup menghapus kode duplikat, membagi metode menjadi unit yang lebih kecil, mengatur ulang hierarki pewarisan, atau membuat nama yang menjelaskan fungsinya sendiri. Dalam konteks Solana, langkah ini dapat melibatkan optimalisasi jumlah CU yang diminta untuk transaksi tertentu, pengurangan jumlah CPI, atau penyederhanaan alur transaksi.

Meskipun TDD tidak wajib untuk membangun program Solana yang baik, developer sebaiknya mempertimbangkan filosofinya — TDD mendorong pendekatan yang cermat dalam membangun program, pendekatan iteratifnya mendukung proses pengembangan yang fleksibel dan adaptif, serta sangat selaras dengan kebutuhan akan presisi, keamanan, dan efisiensi dalam pengembangan program. TDD mendorong developer menulis kode yang lebih bersih, lebih terfokus, dan dioptimalkan untuk persyaratan jaringan serta performa Solana yang unik (misalnya, mengoptimalkan CU). 

Pola Arrange-Act-Assert (AAA)

Pola AAA menawarkan struktur sederhana tetapi efektif untuk menulis pengujian yang jelas, ringkas, dan ampuh. Pada intinya, pola ini mendorong pendekatan disiplin dalam menulis pengujian yang terbagi menjadi tiga fase berbeda:

  • Arrange: Mulailah dengan menyiapkan lingkungan pengujian dan input yang relevan. Hal ini mungkin melibatkan pembuatan akun, simulasi saldo akun, atau persiapan instruksi. Tujuannya adalah membuat skenario terkendali yang meniru kondisi tempat perilaku tersebut akan diuji
  • Act: Jalankan perilaku yang sedang diuji. Fokusnya adalah tindakan yang memicu perilaku yang ingin diuji. Misalnya, apa yang terjadi ketika saya memanggil fungsi x dengan meneruskan akun y?
  • Assert: Evaluasi hasil tindakan terhadap hasil yang diharapkan. Langkah ini sangat penting untuk memastikan apakah pengujian berhasil atau gagal. Misalnya, assertion dapat berkisar dari pemeriksaan nilai sederhana hingga validasi kompleks yang melibatkan beberapa perubahan status. Cara penerapan assertion ini pada akhirnya bergantung pada framework atau protokol yang digunakan. Lighthouse adalah program yang menyediakan instruksi assertion yang dapat ditambahkan ke transaksi untuk mengidentifikasi status yang tidak diinginkan, hasil simulasi palsu, atau pengeluaran berlebih. Kita akan membahas manfaat dan kompleksitas Lighthouse secara lebih mendalam dalam artikel terpisah.

Kekuatan pola AAA terletak pada kemampuannya beradaptasi sehingga berguna untuk pengujian unit, integrasi, dan E2E. Contohnya:

  • Pengujian Unit: Pengujian untuk fungsi tertentu dapat melakukan arrange dengan menyiapkan status program, act dengan memanggil fungsi, dan assert dengan memeriksa nilai kembalian fungsi atau perubahan status yang dihasilkan
  • Pengujian Integrasi: Pengujian interaksi antara beberapa program dapat melibatkan arrange dengan melakukan deployment program dan menetapkan status awalnya, act dengan menjalankan transaksi yang relevan, serta assert dengan memverifikasi status akhir setiap program yang terlibat
  • Pengujian E2E: Pengujian E2E suatu program dapat melakukan arrange dengan menyiapkan status program, act dengan menjalani keseluruhan alur pengguna yang diharapkan (misalnya, membuat akun, membuat proposal, memberikan suara pada proposal tersebut, mengakhiri fase pemungutan suara untuk suatu proposal, dan sebagainya), serta assert dengan membandingkan hasil alur dengan hasil yang diharapkan

Pola AAA sangat penting dalam pengembangan program. Pola ini menerapkan pendekatan pengujian yang berpusat pada perilaku, yang diperlukan untuk menguji apakah program akan bertindak sebagaimana mestinya. Pengujian yang disusun berdasarkan AAA lebih mudah dipahami dan dipelihara karena setiap langkah dibagi dengan jelas menjadi penyiapan, tindakan, dan verifikasi. Selain itu, AAA mendorong pembuatan pengujian independen dan terpisah yang berfokus pada perilaku atau interaksi tertentu.

Praktik Terbaik Industri

Menulis pengujian yang baik tidak hanya berlaku untuk pengembangan Solana. Kita dapat mengambil pelajaran umum dari pengembangan perangkat lunak dan menerapkannya untuk menguji program Solana, dengan berfokus pada pengujian perilaku yang diharapkan alih-alih terhambat oleh detail implementasi. 

Misalnya, pengujian unit umumnya harus menargetkan antarmuka publik suatu metode, memberikan argumen tertentu, dan memastikan hasilnya sesuai harapan. Pendekatan ini memastikan pengujian unit tetap valid meskipun implementasi internal metode berubah, selama perilakunya tetap konsisten. Dalam konteks pengembangan Solana, ini berarti perubahan pada logika program yang tidak memengaruhi perilaku eksternal program seharusnya tidak memerlukan refaktor apa pun.

Selain itu, kendala umum saat menulis pengujian unit adalah membuatnya terlalu bergantung pada cara kerja internal metode yang diuji. Ini mencakup ekspektasi bahwa metode privat tertentu dipanggil beberapa kali atau ditulis dengan cara tertentu. Pengujian ini terlalu rapuh dan rentan gagal akibat refaktor kode apa pun, bahkan ketika perilaku sebenarnya dari metode yang diuji tetap tidak berubah. Sebaliknya, fokus harus diarahkan pada hasil dan efek samping metode yang dapat diamati dari luar. Di sinilah alat cakupan kode dapat digunakan untuk memastikan pengujian bersifat komprehensif tanpa terlalu bergantung pada mekanisme internal.

Menerapkan praktik terbaik ini dalam pengembangan Solana meningkatkan ketangguhan dan kemampuan adaptasi program. Berfokus pada pengujian perilaku daripada detail implementasi menghasilkan kode yang lebih tangguh dan mudah dipelihara, yang sangat penting ketika melakukan deployment kode ke lingkungan jaringan dinamis seperti Solana. Pendekatan ini memastikan perubahan dalam logika program tidak mengharuskan pengujian ulang secara ekstensif dan program siap untuk deployment.

Sekarang, mari mulai menulis beberapa pengujian.

Pengujian Unit dalam Rust

Rust menerapkan pendekatan unik untuk pengujian unit dengan mendorong developer menempatkan pengujian dalam file yang sama dengan kode mereka. Hal ini dilakukan melalui modul tests dan dibatasi dengan atribut #[cfg(test)]. Atribut pengujian memastikan pengujian ini hanya dikompilasi dan dijalankan ketika perangkat lunak diuji secara eksplisit dengan perintah cargo test (yaitu, pengujian tidak dijalankan dengan perintah cargo build). Developer juga dapat memilih untuk mengecualikan pengujian dari proses pengujian reguler dengan atribut #[ignore]. Ini berguna untuk pengujian yang sangat lambat dan tetap memungkinkan pengujian tersebut dijalankan ketika dipanggil secara eksplisit melalui perintah cargo test -- --ignored.

Sebagai contoh, perhatikan fungsi Rust berikut:

Kode
pub fn bubble_sort<T: Ord>(array: &mut [T]) {
    if array.is_empty() {
        return;
    }

    for i in 0..array.len() {
        for j in 0..array.len() - 1 - i {
            if array[j] > array[j + 1] {
                array.swap(j, j + 1);
            }
        }
    }
}

Bubble sort adalah jenis algoritma pengurutan yang berulang kali menelusuri elemen dalam daftar, membandingkan elemen saat ini dengan elemen setelahnya, dan menukar nilainya jika diperlukan. Jika ingin menguji fungsi ini untuk memastikan bahwa fungsi tersebut bekerja sebagaimana mestinya, kita dapat menulis pengujian berikut:

Kode
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_bubble_sort() {

        let mut test1 = vec![12, 39, 4, 36, 777];
        assert_eq!(bubble_sort(&mut test1), vec![4, 12, 36, 39, 777]);

        let mut test2 = vec![21, 55, 14, -123, 32, 0];
        assert_eq!(bubble_sort(&mut test2), vec![-123, 0, 14, 21, 32, 55]);

        let mut test3 = vec!["Orange", "Pear", "Apple", "Grape", "Banana"];
        assert_eq!(bubble_sort(&mut test3), vec!["Apple", "Banana", "Grape", "Orange", "Pear"]);
    }
}

Contoh ini menunjukkan cara modul tests kita diberi anotasi dengan atribut #[cfg(test)]. Di dalam modul tersebut, kita mengimpor semua item publik dari modul induk ke cakupan modul pengujian saat ini dengan use super::*;. Kemudian, kita memiliki beberapa kasus pengujian untuk menyatakan tampilan vektor yang telah diurutkan. Rust menyediakan beberapa makro untuk assertion, seperti assert! untuk pemeriksaan kebenaran umum, assert_eq! untuk kesetaraan, dan assert_ne! untuk pemeriksaan ketidaksetaraan. Assertion ini merupakan tulang punggung strategi pengujian Rust — hanya inilah yang benar-benar Anda perlukan untuk mulai menulis pengujian. 

Dengan menggunakan contoh yang sangat sederhana, bayangkan Anda memiliki fungsi yang menentukan apakah suatu akun memiliki saldo yang cukup untuk membayar transaksi tertentu:

Kode
pub fn has_sufficient_balance(account_balance: u64, transaction_fee: u64) -> bool {
    account_balance >= transaction_fee
}

Fungsi ini menerima dua argumen: saldo saat ini dari akun tertentu dan biaya transaksi yang diperkirakan. Fungsi ini mengembalikan true jika saldo akun cukup untuk menanggung biaya transaksi atau false jika tidak. Hal ini dapat diuji dengan mudah menggunakan pengujian unit berikut:

Kode
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sufficient_funds_for_transaction() {
        let account_balance = 1_000_000;
        let transaction_fee = 5_000;

        assert!(has_sufficient_balance(account_balance, transaction_fee));
    }

    #[test]
    fn insufficient_funds_for_transaction() {
        let account_balance = 1_000;
        let transaction_fee = 5_000;

        assert!(!has_sufficient_balance(account_balance, transaction_fee));
    }
}

Dalam pengujian pertama, kita menyatakan bahwa has_sufficient_balance mengembalikan true ketika saldo akun jauh lebih tinggi daripada biaya transaksi, yang menunjukkan bahwa dana cukup untuk menanggung transaksi. Dalam pengujian kedua, kita menyatakan bahwa has_sufficient_funds mengembalikan false ketika saldo akun lebih rendah daripada biaya transaksi, yang menunjukkan bahwa dana tidak cukup untuk menanggung transaksi. 

Catatan Penting Lainnya untuk Pengujian dalam Rust

Rust memiliki atribut #[should_panic] untuk menandai pengujian yang diperkirakan mengalami panic dalam kondisi tertentu. Ini berguna untuk menguji alur penanganan error dan menentukan pesan panic yang diharapkan:

Kode
#[test]
#[should_panic(expected = "Divide-by-zero error")]
fn test_divide_by_zero() {
    divide_non_zero_result(0, 0);
}

Tidak seperti banyak bahasa lainnya, Rust memungkinkan pengujian langsung terhadap fungsi privat. Hal ini memungkinkan pengujian unit yang lebih mendetail karena setiap aspek fungsionalitas kode dapat dicakup oleh pengujian unit.

Rust juga mendukung teknik pengorganisasian pengujian yang lebih canggih:

  • Modul Bersarang: Untuk proyek yang kompleks, pengujian dapat diatur ke dalam modul bersarang sehingga menghasilkan struktur hierarkis yang jelas dan mencerminkan organisasi proyek
  • Pengujian Berbasis Result: Rust memungkinkan pengujian mengembalikan tipe Result<(), E>. Hal ini memungkinkan developer menggunakan operator ? dalam pengujian sehingga penanganan error menjadi lebih ekspresif

Pengujian Unit dalam TypeScript dengan Mocha dan Chai

TypeScript telah menjadi pilihan populer untuk menguji program seiring dominasi penuh Anchor sebagai lingua franca pengembangan Rust di Solana. Dengan perintah anchor init, framework pengujian Mocha dan pustaka assertion Chai diinisialisasi secara default untuk proyek Anchor baru. 

Mocha adalah framework pengujian JavaScript kaya fitur yang berjalan di Node.js. Hal ini membuat pengujian asinkron menjadi sangat mudah. Kegunaan utama Mocha dalam pengembangan Solana adalah untuk menguji logika sisi klien dApp dan interaksi blockchain lainnya. 

Chai adalah pustaka assertion yang dapat dipasangkan dengan framework pengujian JavaScript apa pun, seperti Mocha. Pustaka ini menyediakan berbagai fungsi bagi developer untuk mengekspresikan assertion dalam gaya yang mudah dibaca. Antarmuka expect, should, dan assert milik Chai memungkinkan developer menulis pengujian komprehensif yang intuitif untuk dibaca dan ditulis. Chai menggunakan rantai bahasa (yaitu, getter yang dapat dirangkai) bersama antarmuka expect dan should untuk meningkatkan keterbacaan assertion. Berkat Chai, penulisan expect({a: 1, b: 2}).to.not.have.any.keys(“c”, “d”); menjadi assertion yang sangat mudah dibaca dan valid.

Misalnya, jika kita membuat proyek hello_world menggunakan perintah anchor init hello_world, file pengujian hello_world.ts berikut akan dibuat dalam direktori hello_world/tests:

Kode
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { HelloWorld } from "../target/types/hello_world";

describe("hello_world", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.HelloWorld as Program<HelloWorld>;

  it("Is initialized!", async () => {
    // Add your test here.
    const tx = await program.methods.initialize().rpc();
    console.log("Your transaction signature", tx);
  });
});

Mari kita uraikan arti semua ini.

Mocha menggunakan blok describe untuk mengelompokkan pengujian dan fungsi it untuk menentukan kasus pengujian. Contoh ini mengikuti pola AAA untuk memberikan pendekatan pengujian yang terstruktur:

  • Arrange: Di sini, anchor.setProvider(anchor.AnchorProvider.env()); mengonfigurasi klien Anchor agar menggunakan provider default lingkungan, yang biasanya mengarah ke validator pengujian Solana lokal. Kemudian, deklarasi const program menginisialisasi instance program yang akan diuji sehingga kita dapat memanggil metodenya dalam pengujian tersebut
  • Act: Dalam kasus pengujian “Is initialized!”, kita memanggil metode initialize dari program kita dan mengirim transaksi
  • Assert: Dalam kasus pengujian ini, kita mencatat signature transaksi tanpa memberikan assertion. Biasanya, pada tahap inilah kita menggunakan Chai untuk assertion. Sebagai contoh yang sangat sederhana, kita dapat memodifikasi kode pengujian default dengan assertion seperti expect(tx).to.be.a(“string”);. Pengujian yang lebih mendetail dapat mengambil dan memeriksa status program setelah inisialisasi serta menyatakan bahwa status tersebut sesuai dengan nilai yang diharapkan

Kombinasi Mocha dan Chai, bersama pola AAA yang dikonfigurasi secara default dalam proyek Anchor, menawarkan framework tangguh untuk menguji program. Developer Solana dapat memastikan program berfungsi secara konsisten dan andal dengan menyusun lingkungan pengujian secara jelas, melakukan tindakan dengan memanggil metode program, dan menyatakan hasilnya.

Misalnya, bayangkan Anda sedang mengembangkan program yang memungkinkan pengguna menyetor dan menarik SOL dari vault dalam konteks Solana. Berikut tampilan pengujian yang ditulis dalam TypeScript menggunakan Mocha dan Chai untuk memastikan fungsionalitas setoran bekerja sebagaimana mestinya:

Kode
import { expect } from "chai";
import { PublicKey } from "@solana/web3.js";
import { depositSOL } from "../src/vault";

describe("Vault Program", function() {
    describe("Deposit functionality", function() {
        it("should correctly deposit SOL into the vault", async function() {
            const vaultPublicKey = new PublicKey(/* vault public key */);
            const userPublicKey = new PublicKey(/* user public key */);
            const depositAmount = 1; // 1 SOL

            const initialVaultBalance = await getVaultBalance(vaultPublicKey);
            await depositSOL(vaultPublicKey, userPublicKey, depositAmount);

            const finalVaultBalance = await getVaultBalance(vaultPublicKey);
            expect(finalVaultBalance).to.equal(initialVaultBalance + depositAmount);
        });
    });
});

Contoh ini menguji fungsi hipotetis depositSOL, yang menangani penyetoran SOL ke vault. Pengujian ini menyatakan bahwa saldo vault bertambah dengan jumlah yang benar setelah penyetoran. Kita menggunakan fungsi getVaultBalance, yaitu fungsi utilitas asumsi yang mengambil saldo vault saat ini.

Catatan Penting Lainnya untuk Pengujian dalam TypeScript dengan Mocha dan Chai

Sistem tipe statis TypeScript terkadang dapat membuat penulisan pengujian sedikit rumit, terutama saat menangani tipe yang kompleks atau tidak didefinisikan dengan baik. Gunakan type assertion untuk menghindari masalah terkait tipe dalam pengujian Anda. Namun, pastikan assertion ini tidak menyamarkan potensi error runtime yang dapat muncul akibat tipe yang salah.

Saat membuat mock objek atau fungsi dalam TypeScript, pastikan entitas mock mematuhi tipe yang benar. Pustaka seperti ts-sinon atau ts-mockito dapat membantu membuat mock yang type-safe agar pengujian tetap akurat dan mencerminkan perilaku program yang sebenarnya.

Mocha menyediakan metode only dan skip untuk menjalankan pengujian secara eksklusif atau melewati pengujian tertentu. Meskipun berguna selama pengembangan, metode ini mudah ter-commit ke produksi secara tidak sengaja sehingga proses pengujian menjadi tidak lengkap. Selalu tinjau pengujian untuk menemukan only atau skip sebelum mendorong pengujian ke produksi. Selain itu, berhati-hatilah saat menggunakan hook Mocha (yaitu, beforeEach, afterEach, before, after) dengan kode asinkron. Pastikan promise ditangani dengan benar menggunakan async/await, atau panggil metode callback done untuk menghindari promise yang tidak terselesaikan atau callback yang tidak dipanggil.

Saat menggunakan expect().to.deep.equal() milik Chai, perhatikan perilakunya terhadap objek yang berisi properti yang dihasilkan secara dinamis, seperti tanggal atau nilai acak. Properti ini dapat menyebabkan kegagalan tak terduga dalam pengujian yang mengharapkan kesetaraan mendalam. Pertimbangkan penggunaan expect().to.include() milik Chai untuk assertion yang lebih tertarget jika sesuai.

Framework Pengujian Solana Populer

Bankrun

Sebuah bank mengawasi pelacakan akun klien, mengelola eksekusi program, serta menjaga integritas dan perkembangan ledger Solana. Pada dasarnya, bank merupakan snapshot ledger pada titik waktu tertentu yang merangkum status hasil transaksi dari blok tertentu. 

Bankrun adalah framework pengujian ringan dan fleksibel yang ditulis dalam Node.js untuk program Solana. Framework ini mengutamakan kemudahan penggunaan dan kecepatan sehingga developer dapat menulis dan menjalankan pengujian terhadap program mereka dengan cepat. Nilai utama Bankrun adalah bahwa framework pengujian ini memungkinkan developer menyimulasikan dan berinteraksi dengan bank Solana dalam lingkungan yang terkendali dan efisien. Bankrun menyederhanakan proses pengujian dengan mereplikasi dinamika bank Solana tanpa overhead yang biasanya timbul saat menyiapkan lingkungan semacam itu.

Desain Bankrun ditopang oleh BanksServer ringan yang meniru perilaku node RPC, tetapi dengan performa dan fleksibilitas yang jauh lebih baik. Developer dapat berinteraksi dengan server ini melalui BanksClient. Klien ini menyediakan seperangkat alat lengkap dengan berbagai metode untuk mengambil saldo akun, status transaksi, dan menyimulasikan transaksi. Khususnya, metode tryProcessTransaction memungkinkan transaksi yang diperkirakan gagal untuk diproses tanpa memunculkan error JavaScript. Hal ini memungkinkan developer melakukan assertion terhadap mode kegagalan atau pesan log tertentu secara langsung.

Repositori GitHub Futarchy milik Meta-DAO merupakan contoh yang sangat baik tentang penggunaan Bankrun untuk menguji kode siap produksi.

Integrasi dengan Anchor

Mengintegrasikan Bankrun dengan Anchor sangat mudah. Dengan menggunakan startAnchor, developer dapat secara otomatis men-deploy semua program dalam workspace Anchor ke lingkungan pengujian. Hal ini memastikan pengujian mereplikasi perilaku program secara akurat dalam lingkungan Solana yang lengkap. Dokumentasi Bankrun menyediakan contoh kode berikut:

Kode
import { startAnchor } from "solana-bankrun";
import { PublicKey } from "@solana/web3.js";

test("anchor", async () => {
	const context = await startAnchor("tests/anchor-example", [], []);
	const programId = new PublicKey(
		"Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
	);
	const executableAccount = await context.banksClient.getAccount(programId);
	expect(executableAccount).not.toBeNull();
	expect(executableAccount?.executable).toBe(true);
});

Paket anchor-bankrun adalah ekstensi canggih yang mendukung Anchor dan Bankrun dengan mengekspor class BankrunProvider yang dapat digunakan sebagai pengganti langsung AnchorProvider selama pengujian.

Salah satu fitur unggulan Bankrun adalah kemampuannya untuk menulis data akun arbitrer. Fungsi ini memungkinkan developer melewati batasan status akun dengan menawarkan tingkat fleksibilitas yang belum pernah ada sebelumnya. Sebagai contoh, developer dapat menyimulasikan akun yang menyimpan USDC dalam jumlah besar tanpa memiliki keypair mint USDC. Kemampuan ini sangat bermanfaat untuk pengujian karena menghilangkan kebutuhan untuk memanipulasi token aktual sehingga menyederhanakan proses penyiapan skenario yang kompleks. 

Dokumentasi Bankrun menyediakan contoh kode mint USDC tanpa batas, yang menunjukkan kemampuan ini melalui fungsi start. Fungsi start menyiapkan lingkungan pengujian dengan men-deploy program dan mengatur data akun sesuai ketentuan.

Perjalanan Waktu

Fitur mandiri lainnya adalah kemampuan Bankrun untuk melakukan perjalanan waktu (yaitu memanipulasi konsep waktu untuk tujuan pengujian). Kemampuan memanipulasi waktu memungkinkan developer mempercepat atau memundurkan jam cluster Solana (yaitu sysvar Clock) untuk menyimulasikan kondisi temporal tertentu secara instan. Kemampuan ini sangat penting untuk menguji program yang beroperasi berdasarkan logika waktu, termasuk jadwal vesting, penguncian token, atau fungsi apa pun yang dipicu ketika mencapai titik waktu tertentu.

Perjalanan waktu dapat dilakukan dengan mudah berkat metode setClock. Metode ini memungkinkan developer mengatur waktu cluster saat ini ke timestamp Unix yang telah ditentukan, sehingga seluruh lingkungan pengujian berpindah ke momen di masa lalu atau masa depan tersebut. Operasi dan transaksi dalam pengujian tetap berjalan seolah-olah waktu yang ditentukan merupakan waktu saat ini, sehingga perilaku program dalam kondisi tersebut dapat dinilai secara akurat.

Berikut adalah contoh sangat sederhana yang menunjukkan cara melakukan perjalanan waktu dengan Bankrun:

Kode
import { start } from "solana-bankrun";
import { PublicKey, Transaction, SystemProgram } from "@solana/web3.js";

async function simulateTimeTravel(context, secondsForward) {
    const newTimestamp = context.clock.unixTimestamp + secondsForward;
    context.adjustClock(newTimestamp);
}

test("One Year Later...", async () => {
    const context = await start([], []);
    const { banksClient, payer } = context;

    // Simulate setting the cluster clock forward by one year (in seconds)
    const oneYearInSeconds = 365 * 24 * 60 * 60;
    await simulateTimeTravel(context, oneYearInSeconds);

    // Proceed with tests assuming the future time
    const transaction = new Transaction().add(
        SystemProgram.transfer({
            fromPubkey: payer.publicKey,
            toPubkey: PublicKey.unique(),
            lamports: 100,
        }),
    );

    transaction.recentBlockhash = context.lastBlockhash;
    transaction.sign(payer);

    await banksClient.processTransaction(transaction);

    // Add assertions here to test expected future behavior
});

Bankrun vs. solana-test-validator

Pilihan antara Bankrun dan solana-test-validator sangat bergantung pada persyaratan spesifik skenario pengujian. Kecepatan, fleksibilitas, dan fitur khusus Bankrun menjadikannya pilihan utama untuk sebagian besar skenario pengembangan, terutama yang memerlukan iterasi cepat atau simulasi mendetail. Namun, solana-test-validator tetap relevan untuk pengujian yang bergantung pada perilaku validator di dunia nyata dan penggunaan metode RPC yang tidak didukung oleh BanksServer.

solana-program-test

Crate solana-program-test menyediakan framework pengujian berbasis Rust yang dirancang khusus untuk program Solana. Framework ini berpusat pada BanksClient. Framework ini menyimulasikan operasi bank Solana sehingga developer dapat men-deploy, berinteraksi dengan, dan menilai perilaku program mereka dalam kondisi pengujian yang meniru mainnet, serupa dengan Bankrun. Sebagai pelengkap BanksClient, terdapat struct ProgramTest, sebuah utilitas untuk menginisialisasi lingkungan pengujian. Artinya, utilitas ini memfasilitasi pengembangan program yang ditentukan dan penyiapan akun yang diperlukan. Struct tambahan seperti BanksTransactionResultWithMetadata, InvokeContext, dan ProgramTestContext memberikan insight dan konteks lengkap untuk transaksi yang diproses selama pengujian, sehingga meningkatkan keseluruhan proses debugging dan verifikasi. 

Untuk menyederhanakan pengembangan dan pengujian lokal, solana-program-test secara otomatis melakukan pramuat beberapa program:

  • SPL Token (dan versi 2022-nya)
  • SPL Memo (versi 1.0 dan 3.0)
  • SPL Associated Token Account

Program yang telah dimuat sebelumnya ini menyediakan penyiapan pengujian yang lebih cepat dan terfokus karena developer tidak perlu menyiapkan program umum tersebut secara manual.

Repositori GitHub Marginfi memiliki beberapa contoh bagus tentang implementasi solana-program-test untuk kode siap produksi. Panduan pengembangan Bonfida juga menyediakan panduan langkah demi langkah yang sangat baik untuk menulis pengujian integrasi menggunakan framework solana-program-test.

solana-test-framework

solana-test-framework adalah ekstensi solana-program-test yang dikembangkan oleh Halborn. Framework ini dirancang untuk memperkaya lingkungan pengujian dengan memperluas BanksClient, RpcClient, ProgramTest, dan ProgramTestContext dengan beberapa metode praktis. Seperti Bankrun, ekstensi untuk ProgramTestContext, misalnya, memungkinkan skenario pengujian tingkat lanjut di mana developer dapat berpindah ke timestamp tertentu dan memperbarui harga oracle.

Ekstensi ini menyediakan peningkatan berikut:

  • Manajemen Transaksi: Menyederhanakan perakitan, penandatanganan, dan pembayaran transaksi melalui transaction_from_instructions
  • Deserialisasi Akun: Memudahkan pengambilan dan deserialisasi akun Anchor dan Borsh, masing-masing dengan get_account_with_anchor and get_account_with_borsh
  • Pembuatan Akun dan Deployment Program: Memungkinkan developer menyiapkan lingkungan pengujian secara efisien dengan fungsi seperti create_account, create_token_mint, create_token_account, dan deploy_program.

solana-test-framework  mendukung cluster eksternal dan runtime tersimulasi. Framework ini kompatibel dengan beberapa versi Solana dan Anchor, termasuk Solana versi 1.9 hingga 1.14 serta versi Anchor yang sesuai untuk 1.9, 1.10, dan 1.14. 

Contoh Skenario Pengujian

Program

Ambil program berikut sebagai contoh:

Kode
use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;
use solana_program::program::invoke;

declare_id!("3vMZa7r3CpHGejvXYbUpPXmm54FxCDPF1QAYnnzL88J9");

#[program]
pub mod king_of_the_hill {
    use super::*;

    pub fn initialize(ctx: Context<Initialize>, initial_prize: u64) -> Result<()> {
        // In case the person who went first didn't send any SOL as the initial prize
        require!(initial_prize > 0, ErrorCode::NeedAnInitialPrize);

        let game_state = &mut ctx.accounts.game_state;

        game_state.king = ctx.accounts.initial_king.key();
        game_state.prize = initial_prize;

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.initial_king.key(),
            &ctx.accounts.prize_pool.key(),
            initial_prize,
        );

        invoke(
            &transfer_instruction,
            &[
                ctx.accounts.initial_king.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        Ok(())
    }

    pub fn become_king(ctx: Context<BecomeKing>, new_prize: u64) -> Result<()> {
        require!(
            new_prize > ctx.accounts.game_state.prize,
            ErrorCode::BidTooLow
        );

        let transfer_to_pool_instruction = system_instruction::transfer(
            &ctx.accounts.payer.key(),
            &ctx.accounts.prize_pool.key(),
            new_prize,
        );

        // Send the new king's funds to the pool
        invoke(
            &transfer_to_pool_instruction,
            &[
                ctx.accounts.payer.to_account_info(),
                ctx.accounts.prize_pool.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        // Send the old king's funds back
        ctx.accounts.prize_pool.sub_lamports(ctx.accounts.game_state.prize);
        ctx.accounts.king.add_lamports(ctx.accounts.game_state.prize);

        ctx.accounts.game_state.king = ctx.accounts.payer.key();
        ctx.accounts.game_state.prize = new_prize;

        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        payer = initial_king,
        space = 8 + 32 + 8 + 1,
        seeds = [b"game_state"],
        bump,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    pub initial_king: Signer<'info>,
    #[account(
        init,
        payer = initial_king,
        space = 8 + 8,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct BecomeKing<'info> {
    #[account(
        mut,
        has_one = king,
    )]
    pub game_state: Account<'info, GameState>,
    #[account(mut)]
    /// CHECK: This is okay - it's only receiving SOL and we don't need any other access
    pub king: UncheckedAccount<'info>,
    #[account(mut)]
    pub payer: Signer<'info>,
    #[account(
        mut,
        seeds = [b"prize_pool"],
        bump,
    )]
    /// CHECK: This is okay - it's a PDA to store SOL and doesn't need a data layout
    pub prize_pool: UncheckedAccount<'info>,
    pub system_program: Program<'info, System>,
}

#[account]
pub struct GameState {
    pub king: Pubkey,
    pub prize: u64,
    pub prize_pool_bump: u8,
}

#[error_code]
pub enum ErrorCode {
    #[msg("The initial prize must be greater than zero")]
    NeedAnInitialPrize,
    #[msg("The bid must be higher than the current prize")]
    BidTooLow,
    #[msg("Invalid prize pool account")]
    InvalidPrizePoolAccount,
}

Program ini mengimplementasikan permainan sederhana “King of the Hill” di Solana. Permainan ini memungkinkan pengguna menjadi “raja” dengan mengirimkan lebih banyak SOL ke kumpulan hadiah dibandingkan raja saat ini. SOL yang dikirim oleh raja sebelumnya akan dikembalikan kepadanya ketika raja baru mengambil alih posisinya.

Fungsi program ini adalah sebagai berikut:

  • Inisialisasi: Fungsi ini menyiapkan permainan dengan raja awal (yaitu pemain pertama yang menginisialisasi permainan) dan jumlah hadiah awal. Hadiah awal harus lebih besar dari nol. Fungsi ini kemudian mentransfer hadiah awal dari raja awal ke kumpulan hadiah
  • Menjadi Raja: Fungsi ini memungkinkan pemain baru menjadi raja dengan menawar lebih banyak SOL daripada hadiah saat ini. Fungsi ini mentransfer hadiah saat ini kepada raja yang digantikan dan memperbarui kumpulan hadiah dengan tawaran raja baru, sehingga pemain tersebut menjadi raja baru. Tawaran ini harus lebih tinggi daripada hadiah saat ini

Kita dapat menguji permainan King of the Hill dengan kode berikut:

Kode
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.KingOfTheHill
as Program<KingOfTheHill>;

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

  before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
  });

  it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
  });

  it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
  })
});

Mari kita uraikan semuanya.

Pertama, kita memulai dengan mengimpor dan menyiapkan lingkungan pengujian di Anchor. Untuk contoh ini, saya melakukan pengujian dalam TypeScript menggunakan Mocha dan Chai di localhost:

Kode
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { KingOfTheHill } from "../target/types/king_of_the_hill";

import { assert } from "chai";

const web3 = require("@solana/web3.js");

Kita menggunakan describe untuk mengelompokkan kasus pengujian. Kita juga mengatur klien agar menggunakan cluster lokal; mengatur program dengan benar; menginisialisasi variabel untuk raja awal, raja baru, PDA status permainan, dan PDA kumpulan hadiah; serta membuat fungsi utilitas untuk mempermudah airdrop:

Kode
describe("King of the Hill Tests", () => {
  // Configure the client to use the local cluster.
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);

  const program = anchor.workspace.KingOfTheHill as Program<KingOfTheHill>;

  let initialKing, newKing;
  let gameStatePDA, prizePoolPDA;

  // Utility function for airdrops
  async function fundWallet(account, amount) {
    const publicKey = account.publicKey ? account.publicKey : account;

    await provider.connection.confirmTransaction(
      await provider.connection.requestAirdrop(publicKey, amount),
      "confirmed"
    );
  }

// Other code

});

Selanjutnya, kita menggunakan hook before untuk menyiapkan dan mendanai keypair raja awal dan raja baru, serta mendapatkan PDA untuk status permainan dan kumpulan hadiah. Blok ini akan dijalankan satu kali sebelum kasus pengujian, sehingga kita dapat menyederhanakan setiap kasus ke dalam pola AAA dengan lebih jelas:

Kode
before(async () => {
    initialKing = web3.Keypair.generate();
    newKing = web3.Keypair.generate();

    await fundWallet(initialKing, 25 * web3.LAMPORTS_PER_SOL);
    await fundWallet(newKing, 30 * web3.LAMPORTS_PER_SOL);

    [gameStatePDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("game_state")],
      program.programId
    );

    [prizePoolPDA] = web3.PublicKey.findProgramAddressSync(
      [Buffer.from("prize_pool")],
      program.programId
    );
});

Kasus pengujian pertama sangat sederhana—kita memeriksa apakah permainan berhasil diinisialisasi dengan benar. Pada tahap arrange, kita mendanai PDA status permainan dan kumpulan hadiah agar dapat berinteraksi dengannya nanti, lalu menetapkan hadiah awal sebesar 1 SOL. Kemudian, pada tahap act, kita memanggil metode initialize dengan initialPrize. Untuk akun, kita meneruskan PDA status permainan, raja awal, PDA kumpulan hadiah, dan program sistem. Raja awal menjadi penanda tangan untuk tindakan ini. Setelah itu, kita melakukan assertion bahwa status permainan telah memperbarui raja dan hadiah dengan benar:

Kode
it("Initializes the game correctly", async () => {
    // Arrange
    await fundWallet(gameStatePDA, 1 * web3.LAMPORTS_PER_SOL);
    await fundWallet(prizePoolPDA, 1 * web3.LAMPORTS_PER_SOL);

    let initialPrize = new anchor.BN(1 * web3.LAMPORTS_PER_SOL);

    // Act
    const tx = await program.methods
      .initialize(initialPrize)
      .accounts({
        gameState: gameStatePDA,
        initialKing: initialKing.publicKey,
        prizePool: prizePoolPDA,
        systemProgram: web3.SystemProgram.programId,
      })
      .signers([initialKing])
      .rpc();

    // Assert
    let gameState: any = await program.account.gameState.fetch(gameStatePDA);
    assert.equal(gameState.king.toBase58(), initialKing.publicKey.toBase58());
    assert.equal(
      gameState.prize.toString(),
      new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toString()
    );
});

Kasus pengujian berikutnya memastikan orang lain dapat menjadi raja. Pada tahap arrange, kita mengambil saldo awal raja dan menetapkan hadiah baru sebesar 2 SOL. Pada tahap act, kita memanggil fungsi becomeKing dan meneruskan jumlah hadiah terbaru. Untuk akun, kita meneruskan PDA status permainan, public key raja saat ini, raja baru sebagai pembayar, PDA kumpulan hadiah, dan program sistem. Raja baru ditetapkan sebagai penanda tangan. Pada tahap assert, kita memeriksa apakah raja menerima SOL awal yang mereka kontribusikan untuk menjadi raja, serta apakah status permainan telah diperbarui dengan benar:

Kode
it("Changes the king correctly", async () => {
    // Arrange
    const initialKingBalanceBefore = await provider.connection.getBalance(initialKing.publicKey);
    let newPrize = new anchor.BN(2 * web3.LAMPORTS_PER_SOL);

    // Act
    const becomeKingTx = await program.methods.becomeKing(newPrize)
      .accounts({
          gameState: gameStatePDA,
          king: initialKing.publicKey, // Correct usage of current king
          payer: newKing.publicKey, // New king who pays and becomes the king
          prizePool: prizePoolPDA,
          systemProgram: web3.SystemProgram.programId,
      })
      .signers([newKing]) // Signing by newKing
      .rpc();

    // Assert
    const initialKingBalanceAfter = await provider.connection.getBalance(initialKing.publicKey);

    const expectedBalance = initialKingBalanceBefore + new anchor.BN(1 * web3.LAMPORTS_PER_SOL).toNumber();
    assert.ok(initialKingBalanceAfter >= expectedBalance, "Old king did not receive the funds back correctly");

    // Fetch the updated game state.
    const updatedGameState = await program.account.gameState.fetch(gameStatePDA);

    // Assertions to confirm the state has updated as expected.
    assert.equal(updatedGameState.king.toBase58(), newKing.publicKey.toBase58(), "King should be updated to newKing.");
    assert.equal(updatedGameState.prize.toString(), newPrize.toString(), "Prize should be updated to newPrize.");
})

Dalam skenario pengujian ini, kita telah memeriksa fungsi inti program King of the Hill. Melalui pengujian unit, kita memvalidasi integritas logika program secara mendetail. Dengan menguji apakah pergantian raja berlangsung dengan benar, kita semakin memastikan bahwa program beroperasi sebagaimana mestinya dalam kondisi simulasi dunia nyata. Pengujian ini menegaskan pentingnya strategi pengujian yang menyeluruh untuk memastikan kualitas dan fungsi program King of the Hill. 

Kesimpulan

Pengujian adalah landasan untuk mengembangkan program Solana yang aman, andal, dan efisien. Dalam artikel ini, kita telah membahas pentingnya menggabungkan Pengujian Unit, Integrasi, dan E2E untuk mencakup semua aspek dalam siklus hidup pengembangan program. Dengan mengintegrasikan metodologi ini dan memanfaatkan framework pengujian canggih seperti Bankrun, solana-program-test, dan solana-test-framework, developer dapat meningkatkan kualitas program Solana mereka secara signifikan. Saat Anda melanjutkan perjalanan sebagai developer Solana, gunakan prinsip, praktik, dan contoh yang dibahas di sini sebagai panduan untuk membuat program yang tangguh, efisien, dan aman.

Jika Anda sudah membaca sejauh ini, terima kasih, anon! Pastikan Anda memasukkan alamat email di bawah agar tidak pernah melewatkan informasi terbaru tentang Solana. Siap mempelajari lebih dalam? Jelajahi artikel terbaru di blog Helius dan lanjutkan perjalanan Solana Anda hari ini.

Referensi Tambahan

Berlangganan Helius

Ikuti perkembangan terbaru dalam pengembangan Solana dan dapatkan pembaruan saat kami memublikasikan postingan

Gambar diperbesar