MỚI: Helius mua lại Light Protocol
Hướng dẫn kiểm thử chương trình Solana
Blog/Phát triển

Hướng dẫn kiểm thử chương trình Solana

Developer Experience Engineer0xIchigo trên X0xIchigo trên LinkedIn0xIchigo trên GitHub
Đọc trong 27 phút

Giới thiệu

Kiểm thử trong môi trường blockchain vượt ra ngoài mô hình kiểm thử phần mềm truyền thống, đặt ra những thách thức riêng và hậu quả nghiêm trọng hơn. Trong môi trường có thông lượng cao và độ trễ thấp của Solana, sai số cho phép rất nhỏ. Kiểm thử tự động không chỉ là một phương pháp hay nhất mà còn là yêu cầu thiết yếu để bảo đảm độ tin cậy và tính bảo mật của các chương trình hoạt động trong môi trường năng động và khắc nghiệt của Solana.

Bài viết này khám phá các loại kiểm thử tự động cốt lõi — Kiểm thử đơn vị, Kiểm thử tích hợp và Kiểm thử đầu cuối (E2E). Bài viết cũng trình bày cách viết các kiểm thử đơn vị cơ bản bằng JavaScript/TypeScript và Rust trước khi phân tích các framework kiểm thử Solana phổ biến. Cuối bài là một ví dụ thực tế kiểm thử chương trình trò chơi “King of the Hill”.

Nếu mới làm quen với Solana, bạn nên đọc trước các bài viết sau:

Bài viết này cũng bổ sung cho bài viết trước của chúng tôi về bảo mật chương trình Solana. Bạn nên đọc song song cả hai bài.

Kiểm thử là gì?

Kiểm thử được thực hiện để xác minh một đoạn mã hoặc toàn bộ ứng dụng hoạt động đúng như dự kiến. Có hai loại kiểm thử phổ biến:

  • Kiểm thử thủ công: quy trình do con người thực hiện, trong đó các trường hợp kiểm thử được chạy bởi nhà phát triển, chuyên viên đảm bảo chất lượng, chuyên gia kiểm thử xâm nhập hoặc bất kỳ người phụ trách nào khác
  • Kiểm thử tự động: quy trình tập trung vào mã, trong đó các tập lệnh được viết để thực thi các trường hợp kiểm thử đã xác định trước bằng chương trình

Kiểm thử thủ công là một quy trình rất linh hoạt, không phụ thuộc vào loại ứng dụng được kiểm thử. Phương pháp này phù hợp để kiểm thử tính năng mới, khả năng sử dụng và khả năng tiếp cận. Kiểm thử thủ công dựa vào trực giác của người kiểm thử về cách ứng dụng nên hoạt động. Tuy nhiên, do thiếu công cụ hỗ trợ trong quy trình, phương pháp này vốn chậm, dễ xảy ra lỗi, tốn thời gian và thường không đầy đủ (tức là không bao quát mọi tình huống). 

Kiểm thử tự động nhằm khắc phục những hạn chế của kiểm thử thủ công. Ví dụ, phương pháp này thường nhanh hơn, đặc biệt khi các kiểm thử được chạy song song. Kiểm thử tự động ít bị ảnh hưởng bởi lỗi của con người hơn vì tuân theo tập lệnh đã xác định trước. Khả năng xử lý hiệu quả số lượng lớn trường hợp kiểm thử giúp tăng phạm vi bao phủ và khiến đây trở thành một giải pháp có khả năng mở rộng cao. Tuy nhiên, do tính khách quan cứng nhắc, kiểm thử tự động kém chính xác hơn đối với các bài kiểm thử phụ thuộc vào tương tác, phán đoán hoặc tư duy phản biện của con người.

Trong phạm vi bài viết này, chúng ta tập trung vào kiểm thử tự động cho các chương trình Solana vì kiểm thử thủ công trên mainnet sẽ khá tốn kém, còn trên devnet sẽ mất rất nhiều thời gian. Tuy nhiên, cả kiểm thử thủ công và tự động đều nên là một phần của quy trình kiểm thử trước khi đưa mã vào môi trường production. Một quy trình kiểm thử vững chắc có thể giảm thiểu số lỗi lọt vào production bằng cách phát hiện chúng sớm hơn trong quá trình phát triển.

Có nhiều loại kiểm thử tự động, bao gồm:

  • Kiểm thử đơn vị
  • Kiểm thử tích hợp
  • Kiểm thử đầu cuối (E2E)

Kiểm thử đơn vị

Kiểm thử đơn vị là quy trình kiểm thử các đơn vị chức năng nhỏ nhất của mã để bảo đảm chúng hoạt động chính xác. Về lý tưởng, đơn vị là những khối cấu thành nhỏ nhất có thể của chương trình (ví dụ: từng hàm, module riêng lẻ), khi kết hợp lại sẽ tạo thành sản phẩm hoàn chỉnh. Ý tưởng cốt lõi là nếu kiểm thử kỹ lưỡng các khối cấu thành, toàn bộ chương trình sẽ hoạt động đúng như dự kiến. 

Kiểm thử đơn vị là nền tảng của quá trình phát triển trên Solana vì nó bảo đảm từng phần của một chương trình hoạt động đúng như dự kiến. Khả năng tái sử dụng các kiểm thử này giúp bảo đảm tính năng mới hoặc bản cập nhật tuân thủ đặc tả dự án và kỳ vọng của người dùng được xác định trong trường hợp kiểm thử. Vì vậy, kiểm thử đơn vị vốn khuyến khích tối ưu hóa và tái cấu trúc mã để các cải tiến mới không ảnh hưởng bất lợi đến chức năng của chương trình. Kiểm thử đơn vị không chỉ bảo đảm từng đoạn mã hoạt động chính xác trong các điều kiện kiểm thử khác nhau mà còn giúp các tương tác blockchain diễn ra hiệu quả và an toàn. Phát hiện lỗi sớm qua kiểm thử đơn vị rất quan trọng vì ngăn các lỗ hổng tiềm ẩn lọt vào production. 

Nhiều framework kiểm thử giúp tinh giản quy trình kiểm thử đơn vị bằng cách đơn giản hóa việc mô phỏng điều kiện mạng và quản lý trạng thái chương trình, như chúng ta sẽ thấy ở phần sau. Các nhà phát triển Solana có thể đạt được độ tin cậy và hiệu năng mã cao thông qua kiểm thử đơn vị.

Kiểm thử tích hợp

Kiểm thử tích hợp mở rộng phạm vi ra ngoài kiểm thử đơn vị để xem xét cách các đơn vị khác nhau của chương trình phối hợp với nhau. Việc xác minh các hàm và module của chương trình hoạt động cùng nhau là rất quan trọng trong quá trình phát triển trên Solana, nơi các tương tác giữa chương trình thường phức tạp và có hệ quả tài chính. Kiểm thử tích hợp nhằm xác định và giải quyết những vấn đề không dễ nhận thấy khi kiểm thử riêng từng đơn vị nhưng xuất hiện khi các thành phần tương tác. Những vấn đề này có thể bao gồm định dạng dữ liệu không khớp, kiểu dữ liệu không nhất quán, phần phụ thuộc của chương trình hoặc sự cố với API của bên thứ ba.

Trong bối cảnh Solana, nơi các chương trình vốn tương tác với những chương trình, ví và oracle khác, kiểm thử tích hợp xác minh rằng các tương tác này diễn ra đúng như dự kiến. Dù từng đơn vị hoạt động hoàn hảo, việc kết hợp chúng vẫn có thể tạo ra hành vi hoặc sự kém hiệu quả ngoài dự kiến trong các điều kiện mô phỏng. Nhà phát triển có thể dùng nhiều framework kiểm thử để mô phỏng các luồng transaction và tương tác chương trình khác nhau, bám sát tình huống thực tế. Ví dụ, Bankrun là một framework kiểm thử gọn nhẹ và mạnh mẽ, cho phép nhà phát triển di chuyển tiến lùi theo thời gian và thiết lập dữ liệu account linh hoạt. Những khả năng này không có khi sử dụng solana-test-validator. Kiểm thử tích hợp đóng vai trò then chốt để bảo đảm chương trình vững chắc, đáng tin cậy và sẵn sàng đáp ứng các yêu cầu từ điều kiện mạng của Solana.

Kiểm thử đầu cuối (E2E)

Kiểm thử đầu cuối (E2E) là giai đoạn hoàn thiện của quy trình kiểm thử. Phương pháp này tập trung đánh giá toàn bộ luồng hoạt động của chương trình như khi diễn ra trong các tình huống thực tế. Phương pháp kiểm thử này khác với kiểm thử đơn vị và tích hợp vì xem xét chương trình từ góc nhìn của người dùng — mọi luồng và tính năng mà người dùng cuối có thể gặp phải đều phải hoạt động đúng như dự kiến. 

Kiểm thử E2E rất cần thiết để xác minh rằng chương trình đáp ứng các yêu cầu chức năng và mang lại trải nghiệm liền mạch cho người dùng. Giai đoạn này giúp phát hiện những vấn đề có thể chưa xuất hiện trong kiểm thử đơn vị hoặc tích hợp, chẳng hạn như độ trễ xử lý transaction, sự cố khi duy trì trạng thái, việc tối ưu compute unit hoặc điều kiện mạng ngoài dự kiến. Mặc dù kiểm thử E2E thường được áp dụng cho toàn bộ dApp, việc kiểm thử các luồng hoạt động của chương trình và xác minh cách transaction của người dùng tương tác với nhiều hàm và module khác nhau vẫn rất quan trọng để xây dựng một chương trình thành công và an toàn.

Kết hợp các phương pháp kiểm thử

Việc sử dụng chiến lược kiểm thử nhiều lớp và kết hợp kiểm thử đơn vị, tích hợp cùng E2E trong quá trình phát triển là rất quan trọng. Mỗi phương pháp kiểm thử đảm nhiệm một vai trò riêng trong vòng đời phát triển, giải quyết các khía cạnh khác nhau về chức năng và hiệu năng của chương trình.

Kiểm thử đơn vị là nền tảng của phương pháp kiểm thử nhiều lớp, cho phép nhà phát triển nhanh chóng xác định và giải quyết vấn đề ở cấp độ mã chi tiết nhất. Dù rất hiệu quả trong việc bảo đảm tính đúng đắn khách quan của từng hàm hoặc module, phương pháp này không tính đến cách các đơn vị phối hợp với nhau hoặc cách chúng hòa vào trải nghiệm của người dùng.

Kiểm thử tích hợp thu hẹp khoảng cách này bằng cách đánh giá cách các đơn vị khác nhau tương tác để phát hiện vấn đề phát sinh khi tích hợp những thành phần đó. Tuy nhiên, chỉ riêng phương pháp này có thể chưa phản ánh đầy đủ trải nghiệm của người dùng cuối hoặc hành vi của chương trình trong điều kiện thực tế.

Kiểm thử E2E bổ trợ cho cả kiểm thử đơn vị và tích hợp bằng cách mô phỏng tình huống sử dụng thực tế và kiểm thử toàn bộ ứng dụng. Cách tiếp cận này rất hữu ích để đánh giá trải nghiệm tổng thể của người dùng nhưng không cung cấp thông tin chuyên sâu ở cấp độ chi tiết cần thiết để nhanh chóng xác định và giải quyết từng vấn đề cụ thể.

Bằng cách tích hợp các phương pháp này, nhà phát triển có thể tạo ra một framework kiểm thử vững chắc, bao quát toàn bộ các vấn đề tiềm ẩn. Cách tiếp cận toàn diện không chỉ nâng cao chất lượng và tính bảo mật của chương trình mà còn tinh giản quy trình phát triển. Nhà phát triển có thể nhanh chóng đưa ra quyết định và điều chỉnh, với sự tin tưởng rằng các thay đổi sẽ được kiểm tra ở nhiều cấp độ. Kết hợp các phương pháp này là điều thiết yếu để bảo đảm một chương trình Solana có nền tảng kỹ thuật vững chắc và phù hợp với kỳ vọng của người dùng trong điều kiện thực tế trước khi triển khai.

Viết kiểm thử tốt

Viết kiểm thử hiệu quả là yếu tố quan trọng để phát triển các chương trình Solana đáng tin cậy và an toàn. Bản chất của một kiểm thử tốt nằm ở việc tập trung vào hành vi cần kiểm thử thay vì framework được sử dụng hoặc chi tiết triển khai của mã. Nhà phát triển có thể xây dựng một chiến lược kiểm thử hiệu quả nhằm nâng cao chất lượng mã bằng cách kết hợp các nguyên tắc của Phát triển hướng kiểm thử (TDD), mẫu Arrange-Act-Assert (AAA) và các phương pháp tốt nhất trong ngành.

Phát triển hướng kiểm thử (TDD)

TDD là một phương pháp phát triển phần mềm vững chắc, lấy việc viết kiểm thử làm định hướng. Nói cách khác, ý tưởng chính là viết kiểm thử trước khi viết mã thực tế. Chu trình TDD thường gồm ba bước:

  • Viết một kiểm thử không đạt: Quá trình phát triển nên bắt đầu bằng việc viết kiểm thử cho phần chức năng tiếp theo mà nhà phát triển muốn thêm. Kiểm thử chắc chắn sẽ không đạt vì chức năng cần kiểm thử chưa tồn tại
  • Triển khai mã: Nhà phát triển nên viết lượng mã tối thiểu cần thiết để kiểm thử đạt. Mục tiêu ở đây là tốc độ và sự đơn giản
  • Tái cấu trúc: Sau khi kiểm thử đạt, nhà phát triển nên tái cấu trúc mã để cải thiện cấu trúc và độ rõ ràng mà không làm thay đổi hành vi. Kiểm thử đạt đóng vai trò như một mạng lưới an toàn, ngăn việc đưa vào các thay đổi gây lỗi. Bước này có thể bao gồm loại bỏ mã trùng lặp, chia phương thức thành các đơn vị nhỏ hơn, sắp xếp lại hệ thống phân cấp kế thừa hoặc đặt tên sao cho tự mô tả. Trong bối cảnh Solana, bước này có thể bao gồm tối ưu lượng CU được yêu cầu cho một transaction, giảm số lượng CPI hoặc tinh giản luồng transaction.

Mặc dù TDD không bắt buộc để xây dựng các chương trình Solana tốt, nhà phát triển nên cân nhắc triết lý của phương pháp này — TDD thúc đẩy cách tiếp cận tỉ mỉ khi xây dựng chương trình, tính lặp của nó tạo điều kiện cho quy trình phát triển linh hoạt và thích ứng, đồng thời hoàn toàn phù hợp với yêu cầu về độ chính xác, tính bảo mật và hiệu quả trong quá trình phát triển chương trình. TDD khuyến khích nhà phát triển viết mã gọn gàng, tập trung hơn và được tối ưu cho các yêu cầu riêng của Solana về mạng và hiệu năng (ví dụ: tối ưu CU). 

Mẫu Arrange-Act-Assert (AAA)

Mẫu AAA cung cấp một cấu trúc đơn giản nhưng mạnh mẽ để viết các kiểm thử rõ ràng, súc tích và hiệu quả. Về cốt lõi, mẫu này khuyến khích phương pháp viết kiểm thử có kỷ luật, được chia thành ba giai đoạn riêng biệt:

  • Arrange: Bắt đầu bằng cách thiết lập môi trường kiểm thử và chuẩn bị mọi đầu vào liên quan. Việc này có thể bao gồm tạo account, mô phỏng số dư account hoặc chuẩn bị instruction. Mục tiêu là tạo ra một tình huống được kiểm soát để mô phỏng các điều kiện mà hành vi sẽ được kiểm thử
  • Act: Thực thi hành vi cần kiểm thử. Trọng tâm ở đây là hành động kích hoạt hành vi cần được kiểm thử. Ví dụ, điều gì xảy ra khi tôi gọi hàm x và truyền vào account y?
  • Assert: Đánh giá kết quả của hành động so với kết quả mong đợi. Bước này rất quan trọng để xác minh kiểm thử đạt hay không đạt. Chẳng hạn, assertion có thể trải dài từ kiểm tra một giá trị đơn giản đến xác thực phức tạp liên quan đến nhiều thay đổi trạng thái. Cách triển khai các assertion này cuối cùng phụ thuộc vào framework hoặc giao thức được sử dụng. Lighthouse là một chương trình cung cấp các instruction assertion có thể được thêm vào transaction để xác định trạng thái không mong muốn, kết quả mô phỏng giả mạo hoặc chi tiêu quá mức. Chúng ta sẽ khám phá sâu hơn lợi ích và những điểm phức tạp của Lighthouse trong một bài viết riêng.

Điểm mạnh của mẫu AAA nằm ở khả năng thích ứng, giúp mẫu này hữu ích cho kiểm thử đơn vị, tích hợp và E2E. Ví dụ:

  • Kiểm thử đơn vị: Một kiểm thử cho hàm nhất định có thể arrange bằng cách thiết lập trạng thái chương trình, act bằng cách gọi hàm và assert bằng cách kiểm tra giá trị trả về của hàm hoặc các thay đổi trạng thái phát sinh
  • Kiểm thử tích hợp: Kiểm thử tương tác giữa nhiều chương trình có thể bao gồm arrange bằng cách triển khai các chương trình và thiết lập trạng thái ban đầu, act bằng cách thực thi các transaction liên quan và assert bằng cách xác minh trạng thái cuối cùng của từng chương trình có liên quan
  • Kiểm thử E2E: Một kiểm thử E2E cho chương trình có thể arrange bằng cách thiết lập trạng thái chương trình, act bằng cách thực hiện toàn bộ luồng người dùng dự kiến (ví dụ: tạo account, tạo đề xuất, bỏ phiếu cho đề xuất đó, kết thúc giai đoạn bỏ phiếu của đề xuất, v.v.) và assert bằng cách so sánh kết quả của luồng với kết quả mong đợi

Mẫu AAA rất quan trọng đối với quá trình phát triển chương trình. Mẫu này áp dụng cách tiếp cận kiểm thử lấy hành vi làm trọng tâm, điều cần thiết để kiểm tra xem chương trình có hoạt động đúng như dự kiến hay không. Các kiểm thử được cấu trúc theo AAA dễ hiểu và dễ bảo trì hơn vì mỗi bước được phân chia rõ ràng thành thiết lập, hành động và xác minh. Hơn nữa, AAA thúc đẩy việc tạo ra các kiểm thử độc lập, tách rời và tập trung vào những hành vi hoặc tương tác cụ thể.

Các phương pháp tốt nhất trong ngành

Viết kiểm thử tốt không chỉ dành riêng cho quá trình phát triển trên Solana. Chúng ta có thể rút ra những bài học chung từ phát triển phần mềm và áp dụng vào việc kiểm thử chương trình Solana, tập trung kiểm thử hành vi mong muốn thay vì sa đà vào chi tiết triển khai. 

Ví dụ, kiểm thử đơn vị thường nên nhắm đến giao diện công khai của một phương thức, cung cấp các đối số cụ thể và xác minh kết quả đúng như mong đợi. Cách tiếp cận này bảo đảm kiểm thử đơn vị vẫn hợp lệ ngay cả khi phần triển khai nội bộ của phương thức thay đổi, miễn là hành vi vẫn nhất quán. Trong bối cảnh phát triển trên Solana, điều này có nghĩa là những thay đổi đối với logic chương trình nhưng không ảnh hưởng đến hành vi bên ngoài của chương trình sẽ không yêu cầu tái cấu trúc kiểm thử.

Ngoài ra, một sai lầm phổ biến khi viết kiểm thử đơn vị là khiến chúng phụ thuộc quá nhiều vào cơ chế nội bộ của phương thức được kiểm thử. Điều này bao gồm việc kỳ vọng một số phương thức riêng tư được gọi đúng một số lần cụ thể hoặc được viết theo một cách nhất định. Những kiểm thử này quá mong manh và dễ thất bại khi mã được tái cấu trúc, ngay cả khi hành vi thực tế của phương thức được kiểm thử không thay đổi. Thay vào đó, nên tập trung vào kết quả và tác dụng phụ có thể quan sát từ bên ngoài của phương thức. Đây là lúc có thể dùng các công cụ đo độ bao phủ mã để bảo đảm kiểm thử toàn diện mà không phụ thuộc quá mức vào cơ chế nội bộ.

Áp dụng các phương pháp tốt nhất này trong quá trình phát triển trên Solana giúp nâng cao độ vững chắc và khả năng thích ứng của chương trình. Tập trung kiểm thử hành vi thay vì chi tiết triển khai giúp mã bền vững và dễ bảo trì hơn, điều thiết yếu khi triển khai mã lên một môi trường mạng năng động như Solana. Cách tiếp cận này bảo đảm các thay đổi trong logic chương trình không đòi hỏi kiểm thử lại trên diện rộng và chương trình đã sẵn sàng để triển khai.

Bây giờ, hãy bắt đầu viết một số kiểm thử.

Viết kiểm thử đơn vị cơ bản

Kiểm thử đơn vị trong Rust

Rust áp dụng một cách tiếp cận độc đáo cho kiểm thử đơn vị, khuyến khích nhà phát triển đặt kiểm thử trong cùng tệp với mã. Việc này được thực hiện thông qua module tests và được kiểm soát bằng attribute #[cfg(test)]. Attribute kiểm thử bảo đảm các kiểm thử này chỉ được biên dịch và chạy khi phần mềm được kiểm thử rõ ràng bằng lệnh cargo test (tức là không chạy với lệnh cargo build). Nhà phát triển cũng có thể chọn loại trừ kiểm thử khỏi lượt chạy kiểm thử thông thường bằng attribute #[ignore]. Điều này hữu ích cho các kiểm thử đặc biệt chậm nhưng vẫn cho phép chạy chúng khi được gọi rõ ràng qua lệnh cargo test -- --ignored.

Ví dụ, hãy xem hàm Rust sau:

Mã
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);
            }
        }
    }
}

Sắp xếp nổi bọt là một loại thuật toán sắp xếp liên tục lặp qua các phần tử trong danh sách, so sánh phần tử hiện tại với phần tử kế tiếp và hoán đổi giá trị của chúng nếu cần. Nếu muốn kiểm thử hàm này để bảo đảm nó hoạt động đúng như dự kiến, chúng ta có thể viết các kiểm thử sau:

Mã
#[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"]);
    }
}

Ví dụ này cho thấy module tests được chú thích bằng attribute #[cfg(test)] như thế nào. Trong module, chúng ta nhập tất cả item công khai từ module cha vào phạm vi của module kiểm thử hiện tại bằng use super::*;. Sau đó, chúng ta có một số trường hợp kiểm thử để assert hình dạng mong đợi của các vector đã sắp xếp. Rust cung cấp một số macro cho assertion, chẳng hạn như assert! để kiểm tra tính đúng nói chung, assert_eq! để kiểm tra bằng nhau và assert_ne! để kiểm tra không bằng nhau. Những assertion này là nền tảng của chiến lược kiểm thử trong Rust — đó thực sự là tất cả những gì bạn cần để bắt đầu viết kiểm thử. 

Với một ví dụ rất cơ bản, hãy tưởng tượng bạn có một hàm xác định account có đủ số dư để thanh toán cho một transaction nhất định hay không:

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

Hàm này nhận hai đối số: số dư hiện tại của một account nhất định và phí transaction dự kiến. Hàm trả về true nếu số dư account đủ để chi trả phí transaction, hoặc false nếu không đủ. Có thể kiểm thử điều này khá dễ dàng bằng kiểm thử đơn vị sau:

Mã
#[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));
    }
}

Trong kiểm thử đầu tiên, chúng ta assert rằng has_sufficient_balance trả về true khi số dư account cao hơn đáng kể so với phí transaction, cho thấy có đủ tiền để chi trả transaction. Trong kiểm thử thứ hai, chúng ta assert rằng has_sufficient_funds trả về false khi số dư account thấp hơn phí transaction, cho thấy không đủ tiền để chi trả transaction. 

Những lưu ý quan trọng khác khi kiểm thử trong Rust

Rust có attribute #[should_panic] để đánh dấu các kiểm thử được dự kiến sẽ panic trong một số điều kiện nhất định. Điều này hữu ích khi kiểm thử các luồng xử lý lỗi và chỉ định thông báo panic mong đợi:

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

Không giống nhiều ngôn ngữ khác, Rust cho phép kiểm thử trực tiếp các hàm riêng tư. Điều này giúp kiểm thử đơn vị chi tiết hơn vì mọi khía cạnh chức năng của mã đều có thể được kiểm thử.

Rust cũng hỗ trợ các kỹ thuật tổ chức kiểm thử nâng cao hơn:

  • Module lồng nhau: Với các dự án phức tạp, kiểm thử có thể được tổ chức thành các module lồng nhau, tạo nên cấu trúc phân cấp rõ ràng phản ánh cách tổ chức của dự án
  • Kiểm thử dựa trên kết quả: Rust hỗ trợ khả năng để kiểm thử trả về kiểu Result<(), E>. Điều này cho phép nhà phát triển sử dụng toán tử ? trong kiểm thử, giúp việc xử lý lỗi giàu tính biểu đạt hơn

Kiểm thử đơn vị trong TypeScript bằng Mocha và Chai

TypeScript đã trở thành lựa chọn phổ biến để kiểm thử chương trình khi Anchor hoàn toàn chiếm ưu thế với vai trò là ngôn ngữ chung của hoạt động phát triển Rust trên Solana. Khi dùng lệnh anchor init, framework kiểm thử Mocha và thư viện assertion Chai được khởi tạo theo mặc định cho các dự án Anchor mới. 

Mocha là một framework kiểm thử JavaScript giàu tính năng chạy trên Node.js. Điều này giúp việc kiểm thử bất đồng bộ trở nên rất đơn giản. Công dụng chính của Mocha trong quá trình phát triển trên Solana là kiểm thử logic phía máy khách của dApp và các tương tác blockchain khác. 

Chai là một thư viện assertion có thể kết hợp với bất kỳ framework kiểm thử JavaScript nào, chẳng hạn như Mocha. Thư viện này cung cấp cho nhà phát triển nhiều hàm để biểu diễn assertion theo cách dễ đọc. Các interface expect, should và assert của Chai cho phép nhà phát triển viết các kiểm thử toàn diện, trực quan cả khi đọc lẫn viết. Chai sử dụng chuỗi ngôn ngữ (tức là các getter có thể nối chuỗi) với interface expect và should để cải thiện khả năng đọc của assertion. Nhờ Chai, expect({a: 1, b: 2}).to.not.have.any.keys(“c”, “d”); là một assertion hợp lệ và rất dễ đọc.

Ví dụ, nếu tạo một dự án hello_world bằng lệnh anchor init hello_world, tệp kiểm thử hello_world.ts sau sẽ được tạo trong thư mục hello_world/tests:

Mã
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);
  });
});

Hãy cùng phân tích ý nghĩa của tất cả nội dung này.

Mocha dùng các block describe để nhóm kiểm thử và các hàm it để định nghĩa trường hợp kiểm thử. Ví dụ này tuân theo mẫu AAA để cung cấp cách tiếp cận kiểm thử có cấu trúc:

  • Arrange: Ở đây, anchor.setProvider(anchor.AnchorProvider.env()); cấu hình máy khách Anchor để sử dụng provider mặc định của môi trường, thường trỏ tới một trình xác thực kiểm thử Solana cục bộ. Sau đó, khai báo const program khởi tạo một phiên bản của chương trình cần kiểm thử, cho phép chúng ta gọi các phương thức của chương trình trong kiểm thử
  • Act: Trong trường hợp kiểm thử “Is initialized!”, chúng ta gọi phương thức initialize của chương trình và gửi transaction
  • Assert: Trong trường hợp kiểm thử này, chúng ta ghi transaction signature vào nhật ký mà không cung cấp assertion. Thông thường, đây là giai đoạn chúng ta tích hợp Chai để thực hiện assertion. Trong một ví dụ rất cơ bản, chúng ta có thể sửa đổi mã kiểm thử mặc định bằng một assertion như expect(tx).to.be.a(“string”);. Một kiểm thử chi tiết hơn có thể truy xuất và kiểm tra trạng thái chương trình sau khi khởi tạo, rồi assert rằng trạng thái đó khớp với các giá trị mong đợi

Việc kết hợp Mocha và Chai cùng với mẫu AAA được cấu hình mặc định trong các dự án Anchor tạo nên một framework vững chắc để kiểm thử chương trình. Bằng cách arrange môi trường kiểm thử rõ ràng, act bằng cách gọi các phương thức của chương trình và assert kết quả, nhà phát triển Solana có thể bảo đảm chương trình hoạt động ổn định và đáng tin cậy.

Ví dụ, hãy tưởng tượng bạn đang phát triển một chương trình cho phép người dùng gửi và rút SOL khỏi một vault trong bối cảnh Solana. Sau đây là ví dụ về kiểm thử được viết bằng TypeScript với Mocha và Chai để bảo đảm chức năng gửi tiền hoạt động đúng như mong đợi:

Mã
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);
        });
    });
});

Ví dụ này kiểm thử một hàm giả định depositSOL, chịu trách nhiệm xử lý việc gửi SOL vào vault. Kiểm thử assert rằng số dư của vault tăng đúng lượng sau khi gửi. Chúng ta sử dụng hàm getVaultBalance, một hàm tiện ích giả định dùng để lấy số dư hiện tại của vault.

Những lưu ý quan trọng khác khi kiểm thử trong TypeScript bằng Mocha và Chai

Hệ thống kiểu tĩnh của TypeScript đôi khi có thể khiến việc viết kiểm thử trở nên khó khăn hơn, đặc biệt khi xử lý các kiểu phức tạp hoặc chưa được định nghĩa rõ. Hãy dùng type assertion để tránh các vấn đề liên quan đến kiểu trong kiểm thử. Tuy nhiên, cần bảo đảm các assertion này không che giấu những lỗi runtime tiềm ẩn có thể phát sinh do kiểu không chính xác.

Khi mô phỏng đối tượng hoặc hàm trong TypeScript, hãy bảo đảm các thực thể mô phỏng tuân thủ đúng kiểu. Các thư viện như ts-sinon hoặc ts-mockito có thể giúp tạo mock an toàn về kiểu để kiểm thử vẫn chính xác và phản ánh hành vi thực tế của chương trình.

Mocha cung cấp các phương thức only và skip để chỉ chạy hoặc bỏ qua các kiểm thử cụ thể. Dù tiện lợi trong quá trình phát triển, bạn rất dễ vô tình commit chúng vào production, khiến lượt chạy kiểm thử không đầy đủ. Luôn kiểm tra only hoặc skip trong các kiểm thử trước khi đẩy lên production. Ngoài ra, hãy thận trọng khi sử dụng các hook của Mocha (tức là beforeEach, afterEach, before, after) với mã bất đồng bộ. Hãy bảo đảm promise được xử lý chính xác bằng async/await, hoặc gọi phương thức callback done để tránh promise chưa được giải quyết hoặc callback không được gọi.

Khi sử dụng expect().to.deep.equal() của Chai, hãy lưu ý hành vi của nó với các đối tượng chứa thuộc tính được tạo động, chẳng hạn như ngày tháng hoặc giá trị ngẫu nhiên. Các thuộc tính này có thể khiến những kiểm thử kỳ vọng phép so sánh sâu bằng nhau thất bại ngoài dự kiến. Hãy cân nhắc dùng expect().to.include() của Chai để thực hiện assertion có mục tiêu cụ thể hơn khi phù hợp.

Các framework kiểm thử Solana phổ biến

Bankrun

Một ngân hàng chịu trách nhiệm theo dõi tài khoản client, quản lý việc thực thi chương trình, đồng thời duy trì tính toàn vẹn và tiến trình của sổ cái Solana. Về cơ bản, đây là ảnh chụp nhanh của sổ cái tại một thời điểm nhất định, bao quát trạng thái hình thành từ các giao dịch của một block cụ thể. 

Bankrun là một framework kiểm thử gọn nhẹ, linh hoạt được viết bằng Node.js cho các chương trình Solana. Framework này chú trọng tính dễ sử dụng và tốc độ, cho phép nhà phát triển nhanh chóng viết và chạy kiểm thử cho chương trình. Giá trị thực sự của Bankrun nằm ở chỗ đây là một framework kiểm thử giúp nhà phát triển mô phỏng và tương tác với các ngân hàng Solana trong một môi trường được kiểm soát và hiệu quả. Bankrun đơn giản hóa quy trình kiểm thử bằng cách mô phỏng hoạt động của các ngân hàng Solana mà không tạo ra chi phí vận hành thường thấy khi thiết lập một môi trường như vậy.

Thiết kế của Bankrun dựa trên một BanksServer gọn nhẹ, mô phỏng hành vi của một node RPC nhưng có hiệu năng và độ linh hoạt cao hơn đáng kể. Nhà phát triển có thể tương tác với server này thông qua BanksClient. Client này cung cấp một bộ công cụ toàn diện, với các phương thức để truy xuất số dư tài khoản, trạng thái giao dịch và mô phỏng giao dịch. Đáng chú ý, phương thức tryProcessTransaction cho phép xử lý các giao dịch được dự kiến sẽ thất bại mà không đưa ra bất kỳ lỗi JavaScript nào. Nhờ đó, nhà phát triển có thể xác nhận trực tiếp các chế độ lỗi hoặc thông báo log cụ thể.

Kho lưu trữ Futarchy trên GitHub của Meta-DAO là một ví dụ điển hình về việc sử dụng Bankrun để kiểm thử mã sẵn sàng cho môi trường production.

Tích hợp với Anchor

Việc tích hợp Bankrun với Anchor rất đơn giản. Khi sử dụng startAnchor, nhà phát triển có thể tự động triển khai tất cả chương trình trong một workspace Anchor vào môi trường kiểm thử. Điều này đảm bảo các bài kiểm thử phản ánh chính xác hành vi của chương trình trong một môi trường Solana hoàn chỉnh. Tài liệu Bankrun cung cấp ví dụ mã sau:

Mã
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);
});

Gói anchor-bankrun là một tiện ích mở rộng mạnh mẽ hỗ trợ Anchor và Bankrun bằng cách export một lớp BankrunProvider có thể dùng để thay thế trực tiếp AnchorProvider trong quá trình kiểm thử.

Ghi tài khoản tùy ý bằng Bankrun

Một tính năng nổi bật của Bankrun là khả năng ghi dữ liệu tài khoản tùy ý. Chức năng này cho phép nhà phát triển vượt qua các giới hạn về trạng thái tài khoản, mang lại mức độ linh hoạt chưa từng có. Ví dụ, nhà phát triển có thể mô phỏng một tài khoản nắm giữ lượng USDC đáng kể mà không cần sở hữu keypair mint USDC. Khả năng này rất hữu ích cho việc kiểm thử vì loại bỏ nhu cầu thao tác với token thực, qua đó đơn giản hóa quy trình thiết lập các tình huống phức tạp. 

Tài liệu Bankrun cung cấp ví dụ mã về một mint USDC vô hạn, minh họa khả năng này thông qua hàm start. Hàm start chuẩn bị môi trường kiểm thử bằng cách triển khai chương trình và thiết lập dữ liệu tài khoản theo chỉ định.

Du hành thời gian

Một tính năng độc lập khác là khả năng du hành thời gian của Bankrun (tức là điều chỉnh khái niệm thời gian phục vụ mục đích kiểm thử). Khả năng điều chỉnh thời gian cho phép nhà phát triển tua nhanh hoặc tua ngược đồng hồ của cluster Solana (tức sysvar Clock) để mô phỏng tức thì các điều kiện thời gian cụ thể. Khả năng này rất quan trọng khi kiểm thử các chương trình vận hành theo logic dựa trên thời gian, bao gồm lịch vesting, khóa token hoặc bất kỳ chức năng nào được kích hoạt khi đạt đến một thời điểm cụ thể.

Việc du hành thời gian trở nên đơn giản nhờ phương thức setClock. Phương thức này cho phép nhà phát triển đặt thời gian hiện tại của cluster thành một Unix timestamp được xác định trước, qua đó chuyển toàn bộ môi trường kiểm thử đến thời điểm đó trong quá khứ hoặc tương lai. Các hoạt động và giao dịch trong bài kiểm thử tiếp tục diễn ra như thể thời gian đã chỉ định là thời gian hiện tại, cho phép đánh giá chính xác hành vi của chương trình trong những điều kiện đó.

Dưới đây là một ví dụ rất cơ bản về cách du hành thời gian với Bankrun:

Mã
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 so với solana-test-validator

Việc lựa chọn giữa Bankrun và solana-test-validator chủ yếu phụ thuộc vào yêu cầu cụ thể của từng tình huống kiểm thử. Tốc độ, tính linh hoạt và các tính năng chuyên biệt của Bankrun khiến framework này trở thành lựa chọn ưu tiên cho hầu hết tình huống phát triển, đặc biệt là những tình huống cần lặp lại nhanh hoặc mô phỏng chi tiết. Tuy nhiên, solana-test-validator vẫn phù hợp với các bài kiểm thử phụ thuộc vào hành vi thực tế của validator và sử dụng những phương thức RPC mà BanksServer không hỗ trợ.

solana-program-test

Crate solana-program-test cung cấp một framework kiểm thử dựa trên Rust, được thiết kế riêng cho các chương trình Solana. Framework này tập trung vào BanksClient. Tương tự Bankrun, framework mô phỏng hoạt động của một ngân hàng Solana, cho phép nhà phát triển triển khai, tương tác và đánh giá hành vi của chương trình trong các điều kiện kiểm thử mô phỏng mainnet. Bổ trợ cho BanksClient là struct ProgramTest, một tiện ích dùng để khởi tạo môi trường kiểm thử. Cụ thể, tiện ích này hỗ trợ phát triển các chương trình được chỉ định và thiết lập những tài khoản cần thiết. Các struct bổ sung như BanksTransactionResultWithMetadata, InvokeContext và ProgramTestContext cung cấp thông tin chuyên sâu và ngữ cảnh phong phú về các giao dịch được xử lý trong quá trình kiểm thử, giúp cải thiện toàn bộ quy trình gỡ lỗi và xác minh. 

Để đơn giản hóa việc phát triển và kiểm thử cục bộ, solana-program-test tự động tải trước một số chương trình:

  • SPL Token (và phiên bản 2022)
  • SPL Memo (phiên bản 1.0 và 3.0)
  • SPL Associated Token Account

Các chương trình được tải trước này giúp quá trình thiết lập kiểm thử nhanh hơn và tập trung hơn vì không cần thiết lập thủ công những chương trình phổ biến này.

Marginfi có kho lưu trữ GitHub chứa một số ví dụ rất hay về cách triển khai solana-program-test cho mã sẵn sàng chạy trên môi trường production. Hướng dẫn phát triển của Bonfida cũng trình bày chi tiết cách viết kiểm thử tích hợp bằng framework solana-program-test.

solana-test-framework

solana-test-framework là một tiện ích mở rộng của solana-program-test do Halborn phát triển. Framework này được thiết kế để nâng cao môi trường kiểm thử bằng cách bổ sung một số phương thức tiện ích cho BanksClient, RpcClient, ProgramTest và ProgramTestContext. Giống như Bankrun, chẳng hạn, các tiện ích mở rộng cho ProgramTestContext hỗ trợ những tình huống kiểm thử nâng cao, trong đó nhà phát triển có thể chuyển đến các timestamp cụ thể và cập nhật giá oracle.

Các tiện ích mở rộng này mang lại những cải tiến sau:

  • Quản lý giao dịch: Đơn giản hóa việc tạo, ký và thanh toán giao dịch thông qua transaction_from_instructions
  • Giải tuần tự hóa tài khoản: Hỗ trợ dễ dàng truy xuất và giải tuần tự hóa các tài khoản Anchor và Borsh tương ứng bằng get_account_with_anchor and get_account_with_borsh
  • Tạo tài khoản và triển khai chương trình: Cho phép nhà phát triển thiết lập môi trường kiểm thử hiệu quả bằng các hàm như create_account, create_token_mint, create_token_account và deploy_program.

solana-test-framework  hỗ trợ cả cluster bên ngoài lẫn runtime mô phỏng. Framework tương thích với nhiều phiên bản Solana và Anchor, bao gồm Solana từ phiên bản 1.9 đến 1.14 cùng các phiên bản Anchor tương ứng cho 1.9, 1.10 và 1.14. 

Tình huống kiểm thử mẫu

Chương trình

Hãy lấy chương trình sau làm ví dụ:

Mã
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,
}

Chương trình này triển khai một trò chơi “King of the Hill” đơn giản trên Solana. Trò chơi cho phép người dùng trở thành “vua” bằng cách gửi vào quỹ giải thưởng nhiều SOL hơn vị vua hiện tại. Khi một vị vua mới lên thay, số SOL mà vị vua trước đó đã gửi sẽ được hoàn trả cho họ.

Chương trình hoạt động như sau:

  • Khởi tạo: Hàm này thiết lập trò chơi với một vị vua ban đầu (tức người chơi đầu tiên khởi tạo trò chơi) và một số tiền thưởng ban đầu. Số tiền thưởng ban đầu phải lớn hơn 0. Sau đó, hàm chuyển số tiền thưởng ban đầu từ vị vua ban đầu vào quỹ giải thưởng
  • Trở thành vua: Hàm này cho phép người chơi mới trở thành vua bằng cách đặt giá bằng lượng SOL lớn hơn tiền thưởng hiện tại. Hàm chuyển tiền thưởng hiện tại cho vị vua mãn nhiệm và cập nhật quỹ giải thưởng bằng giá đặt của vị vua mới, qua đó đưa người chơi này lên làm vua. Giá đặt phải cao hơn tiền thưởng hiện tại

Viết bài kiểm thử

Chúng ta có thể kiểm thử thành công trò chơi King of the Hill bằng đoạn mã sau:

Mã
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.");
  })
});

Hãy phân tích từng phần.

Đầu tiên, chúng ta bắt đầu bằng việc import và thiết lập môi trường kiểm thử trong Anchor. Trong ví dụ này, tôi kiểm thử bằng TypeScript, sử dụng Mocha và Chai trên localhost:

Mã
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");

Chúng ta sử dụng describe để nhóm các trường hợp kiểm thử. Chúng ta cũng thiết lập client để sử dụng cluster cục bộ; cấu hình chương trình chính xác; lần lượt khởi tạo các biến cho vị vua ban đầu, vị vua mới, PDA trạng thái trò chơi và PDA quỹ giải thưởng; đồng thời tạo một hàm tiện ích để airdrop dễ dàng hơn:

Mã
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

});

Tiếp theo, chúng ta sử dụng hook before để thiết lập và cấp vốn cho các keypair của vị vua ban đầu và vị vua mới, đồng thời suy ra các PDA cho trạng thái trò chơi và quỹ giải thưởng. Block này sẽ chạy một lần trước các trường hợp kiểm thử, cho phép chúng ta đơn giản hóa từng trường hợp theo mô hình AAA một cách rõ ràng hơn:

Mã
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
    );
});

Trường hợp kiểm thử đầu tiên rất đơn giản — chúng ta kiểm tra xem trò chơi có được khởi tạo chính xác hay không. Ở bước chuẩn bị, chúng ta cấp vốn cho các PDA trạng thái trò chơi và quỹ giải thưởng để có thể tương tác với chúng sau đó, đồng thời đặt tiền thưởng ban đầu là 1 SOL. Tiếp theo, chúng ta thực thi bằng cách gọi phương thức initialize với initialPrize. Đối với các tài khoản, chúng ta truyền vào PDA trạng thái trò chơi, vị vua ban đầu, PDA quỹ giải thưởng và system program. Vị vua ban đầu là signer cho thao tác này. Sau đó, chúng ta xác nhận rằng trạng thái trò chơi đã cập nhật chính xác vị vua và tiền thưởng:

Mã
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()
    );
});

Trường hợp kiểm thử tiếp theo đảm bảo một người khác có thể trở thành vua. Ở bước chuẩn bị, bài kiểm thử lấy số dư ban đầu của vị vua và đặt tiền thưởng mới là 2 SOL. Tiếp theo, bài kiểm thử gọi hàm becomeKing và truyền vào số tiền thưởng mới nhất. Đối với các tài khoản, chúng ta truyền vào PDA trạng thái trò chơi, public key của vị vua hiện tại, vị vua mới với vai trò người thanh toán, PDA quỹ giải thưởng và system program. Vị vua mới được đặt làm signer. Cuối cùng, bài kiểm thử xác nhận rằng vị vua nhận lại số SOL ban đầu mà họ đã đóng góp để trở thành vua, đồng thời kiểm tra trạng thái trò chơi đã được cập nhật chính xác:

Mã
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.");
})

Trong tình huống kiểm thử này, chúng ta đã xem xét các chức năng cốt lõi của chương trình King of the Hill. Thông qua kiểm thử đơn vị, chúng ta xác thực tính toàn vẹn của logic chương trình ở cấp độ chi tiết. Bằng cách kiểm tra việc thay đổi vị vua diễn ra chính xác, chúng ta tiếp tục xác nhận chương trình hoạt động đúng như dự kiến trong các điều kiện thực tế được mô phỏng. Những bài kiểm thử này nhấn mạnh tầm quan trọng của một chiến lược kiểm thử toàn diện nhằm đảm bảo chất lượng và chức năng của chương trình King of the Hill. 

Kết luận

Kiểm thử là nền tảng để phát triển các chương trình Solana an toàn, đáng tin cậy và hiệu quả. Trong bài viết này, chúng ta đã tìm hiểu tầm quan trọng của việc kết hợp kiểm thử đơn vị, kiểm thử tích hợp và kiểm thử E2E để bao quát mọi khía cạnh trong vòng đời phát triển chương trình. Bằng cách tích hợp các phương pháp này và tận dụng những framework kiểm thử mạnh mẽ như Bankrun, solana-program-test và solana-test-framework, nhà phát triển có thể cải thiện đáng kể chất lượng của các chương trình Solana. Khi tiếp tục hành trình phát triển trên Solana, hãy vận dụng những nguyên tắc, phương pháp và ví dụ được trình bày ở đây để xây dựng các chương trình mạnh mẽ, hiệu quả và an toàn.

Nếu đã đọc đến đây, cảm ơn bạn, anon! Hãy nhập địa chỉ email bên dưới để không bao giờ bỏ lỡ thông tin cập nhật về những điểm mới trên Solana. Sẵn sàng tìm hiểu sâu hơn? Khám phá các bài viết mới nhất trên blog Helius và tiếp tục hành trình Solana ngay hôm nay.

Tài nguyên bổ sung

Đăng ký nhận tin từ Helius

Luôn cập nhật những thông tin mới nhất về phát triển Solana và nhận thông báo khi chúng tôi đăng bài

Hình ảnh phóng to