Tổ Chức Kiểm Thử

Như đã đề cập ở đầu chương, kiểm thử là một lĩnh vực phức tạp, và nhiều người sử dụng thuật ngữ và tổ chức khác nhau. Cộng đồng Rust nghĩ về kiểm thử theo hai loại chính: kiểm thử đơn vị và kiểm thử tích hợp. Kiểm thử đơn vị nhỏ và tập trung hơn, kiểm thử một module riêng biệt tại một thời điểm, và có thể kiểm thử các giao diện riêng tư. Kiểm thử tích hợp hoàn toàn bên ngoài thư viện của bạn và sử dụng code của bạn theo cách mà bất kỳ mã bên ngoài nào khác sẽ sử dụng, chỉ sử dụng giao diện công cộng và có thể thực hiện nhiều module trong mỗi bài kiểm thử.

Viết cả hai loại kiểm thử là quan trọng để đảm bảo rằng các thành phần của thư viện của bạn đang hoạt động như bạn mong đợi, cả riêng biệt và cùng nhau.

Kiểm Thử Đơn Vị

Mục đích của kiểm thử đơn vị là kiểm thử từng đơn vị code riêng biệt với phần còn lại của code để nhanh chóng xác định nơi code đang hoạt động và không hoạt động như mong đợi. Bạn sẽ đặt các kiểm thử đơn vị trong thư mục src trong mỗi tệp với code mà chúng đang kiểm thử. Quy ước là tạo một module có tên tests trong mỗi tệp để chứa các hàm kiểm thử và chú thích module bằng cfg(test).

Module Tests và #[cfg(test)]

Chú thích #[cfg(test)] trên module tests nói với Rust biên dịch và chạy code kiểm thử chỉ khi bạn chạy cargo test, không phải khi bạn chạy cargo build. Điều này tiết kiệm thời gian biên dịch khi bạn chỉ muốn xây dựng thư viện và tiết kiệm không gian trong kết quả biên dịch vì các kiểm thử không được bao gồm. Bạn sẽ thấy rằng vì kiểm thử tích hợp nằm trong một thư mục khác, chúng không cần chú thích #[cfg(test)]. Tuy nhiên, vì kiểm thử đơn vị nằm trong cùng tệp với mã, bạn sẽ sử dụng #[cfg(test)] để chỉ định rằng chúng không nên được bao gồm trong kết quả biên dịch.

Nhớ lại rằng khi chúng ta tạo dự án mới adder trong phần đầu tiên của chương này, Cargo đã tạo mã này cho chúng ta:

Tên tệp: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Trên module tests được tự động tạo ra, thuộc tính cfg là viết tắt của cấu hình và nói với Rust rằng mục tiếp theo chỉ nên được bao gồm khi có một tùy chọn cấu hình nhất định. Trong trường hợp này, tùy chọn cấu hình là test, được Rust cung cấp để biên dịch và chạy các bài kiểm thử. Bằng cách sử dụng thuộc tính cfg, Cargo chỉ biên dịch code kiểm thử của chúng ta nếu chúng ta chủ động chạy các bài kiểm thử với cargo test. Điều này bao gồm bất kỳ hàm trợ giúp nào có thể nằm trong module này, ngoài các hàm được chú thích với #[test].

Kiểm Thử Các Hàm Riêng Tư

Có tranh luận trong cộng đồng kiểm thử về việc liệu các hàm riêng tư có nên được kiểm thử trực tiếp hay không, và các ngôn ngữ khác làm cho việc kiểm thử các hàm riêng tư trở nên khó khăn hoặc không thể. Bất kể bạn tuân theo ý thức hệ kiểm thử nào, các quy tắc riêng tư của Rust cho phép bạn kiểm thử các hàm riêng tư. Hãy xem xét mã trong Listing 11-12 với hàm riêng tư internal_adder.

pub fn add_two(a: usize) -> usize {
    internal_adder(a, 2)
}

fn internal_adder(left: usize, right: usize) -> usize {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}

Lưu ý rằng hàm internal_adder không được đánh dấu là pub. Các bài kiểm thử chỉ là mã Rust, và module tests chỉ là một module khác. Như chúng ta đã thảo luận trong "Đường Dẫn cho Việc Tham Chiếu đến Một Mục trong Cây Module", các mục trong module con có thể sử dụng các mục trong module tổ tiên của chúng. Trong bài kiểm thử này, chúng ta đưa tất cả các mục của module cha của module tests vào phạm vi với use super::*, và sau đó bài kiểm thử có thể gọi internal_adder. Nếu bạn không nghĩ rằng các hàm riêng tư nên được kiểm thử, không có gì trong Rust buộc bạn phải làm như vậy.

Kiểm Thử Tích Hợp

Trong Rust, kiểm thử tích hợp hoàn toàn bên ngoài thư viện của bạn. Chúng sử dụng thư viện của bạn theo cách mà bất kỳ mã nào khác sẽ sử dụng, có nghĩa là chúng chỉ có thể gọi các hàm là một phần của API công khai của thư viện của bạn. Mục đích của chúng là kiểm tra liệu nhiều phần của thư viện của bạn có hoạt động cùng nhau một cách chính xác hay không. Các đơn vị mã hoạt động chính xác độc lập có thể gặp vấn đề khi được tích hợp, vì vậy việc kiểm thử bao phủ mã tích hợp cũng rất quan trọng. Để tạo kiểm thử tích hợp, trước tiên bạn cần một thư mục tests.

Thư Mục tests

Chúng ta tạo một thư mục tests ở cấp trên cùng của thư mục dự án, bên cạnh src. Cargo biết cách tìm các tệp kiểm thử tích hợp trong thư mục này. Sau đó, chúng ta có thể tạo nhiều tệp kiểm thử tùy thích, và Cargo sẽ biên dịch mỗi tệp dưới dạng một crate riêng biệt.

Hãy tạo một kiểm thử tích hợp. Với mã trong Listing 11-12 vẫn nằm trong tệp src/lib.rs, hãy tạo một thư mục tests, và tạo một tệp mới có tên tests/integration_test.rs. Cấu trúc thư mục của bạn nên trông như thế này:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

Nhập mã trong Listing 11-13 vào tệp tests/integration_test.rs.

use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}

Mỗi tệp trong thư mục tests là một crate riêng biệt, vì vậy chúng ta cần đưa thư viện của mình vào phạm vi của mỗi crate kiểm thử. Vì lý do đó, chúng ta thêm use adder::add_two; ở đầu mã, điều mà chúng ta không cần trong các kiểm thử đơn vị.

Chúng ta không cần chú thích bất kỳ mã nào trong tests/integration_test.rs với #[cfg(test)]. Cargo xử lý thư mục tests đặc biệt và chỉ biên dịch các tệp trong thư mục này khi chúng ta chạy cargo test. Chạy cargo test ngay bây giờ:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Ba phần của kết quả bao gồm các kiểm thử đơn vị, kiểm thử tích hợp và kiểm thử tài liệu. Lưu ý rằng nếu bất kỳ bài kiểm thử nào trong một phần không thành công, các phần tiếp theo sẽ không được chạy. Ví dụ, nếu một kiểm thử đơn vị không thành công, sẽ không có kết quả nào cho kiểm thử tích hợp và kiểm thử tài liệu vì những kiểm thử đó chỉ được chạy nếu tất cả các kiểm thử đơn vị đều thành công.

Phần đầu tiên cho các kiểm thử đơn vị giống như những gì chúng ta đã thấy: một dòng cho mỗi kiểm thử đơn vị (một có tên internal mà chúng ta đã thêm trong Listing 11-12) và sau đó là một dòng tóm tắt cho các kiểm thử đơn vị.

Phần kiểm thử tích hợp bắt đầu bằng dòng Running tests/integration_test.rs. Tiếp theo, có một dòng cho mỗi hàm kiểm thử trong kiểm thử tích hợp đó và một dòng tóm tắt cho kết quả của kiểm thử tích hợp ngay trước khi phần Doc-tests adder bắt đầu.

Mỗi tệp kiểm thử tích hợp có phần riêng của nó, vì vậy nếu chúng ta thêm nhiều tệp hơn trong thư mục tests, sẽ có nhiều phần kiểm thử tích hợp hơn.

Chúng ta vẫn có thể chạy một hàm kiểm thử tích hợp cụ thể bằng cách chỉ định tên của hàm kiểm thử làm đối số cho cargo test. Để chạy tất cả các kiểm thử trong một tệp kiểm thử tích hợp cụ thể, hãy sử dụng đối số --test của cargo test theo sau là tên của tệp:

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Lệnh này chỉ chạy các kiểm thử trong tệp tests/integration_test.rs.

Các Module Con trong Kiểm Thử Tích Hợp

Khi bạn thêm nhiều kiểm thử tích hợp, bạn có thể muốn tạo nhiều tệp trong thư mục tests để giúp tổ chức chúng; ví dụ, bạn có thể nhóm các hàm kiểm thử theo chức năng mà chúng đang kiểm thử. Như đã đề cập trước đây, mỗi tệp trong thư mục tests được biên dịch như là crate riêng biệt của nó, điều này hữu ích để tạo các phạm vi riêng biệt để bắt chước chặt chẽ hơn cách người dùng cuối sẽ sử dụng crate của bạn. Tuy nhiên, điều này có nghĩa là các tệp trong thư mục tests không chia sẻ cùng một hành vi như các tệp trong src, như bạn đã học trong Chương 7 về cách tách mã thành các module và tệp.

Hành vi khác nhau của các tệp thư mục tests là rõ ràng nhất khi bạn có một tập hợp các hàm trợ giúp để sử dụng trong nhiều tệp kiểm thử tích hợp và bạn cố gắng làm theo các bước trong phần "Tách Module thành Các Tệp Khác Nhau" của Chương 7 để trích xuất chúng vào một module chung. Ví dụ, nếu chúng ta tạo tests/common.rs và đặt một hàm có tên setup trong đó, chúng ta có thể thêm một số mã vào setup mà chúng ta muốn gọi từ nhiều hàm kiểm thử trong nhiều tệp kiểm thử:

Tên tệp: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

Khi chúng ta chạy các bài kiểm thử một lần nữa, chúng ta sẽ thấy một phần mới trong kết quả kiểm thử cho tệp common.rs, ngay cả khi tệp này không chứa bất kỳ hàm kiểm thử nào và chúng ta cũng không gọi hàm setup từ bất cứ đâu:

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Việc common xuất hiện trong kết quả kiểm thử với running 0 tests hiển thị cho nó không phải là điều chúng ta muốn. Chúng ta chỉ muốn chia sẻ một số mã với các tệp kiểm thử tích hợp khác. Để tránh làm cho common xuất hiện trong kết quả kiểm thử, thay vì tạo tests/common.rs, chúng ta sẽ tạo tests/common/mod.rs. Thư mục dự án bây giờ trông như thế này:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

Đây là quy ước đặt tên cũ mà Rust cũng hiểu mà chúng ta đã đề cập trong "Đường Dẫn Tệp Thay Thế" ở Chương 7. Đặt tên tệp theo cách này nói với Rust không xử lý module common như một tệp kiểm thử tích hợp. Khi chúng ta di chuyển mã hàm setup vào tests/common/mod.rs và xóa tệp tests/common.rs, phần trong kết quả kiểm thử sẽ không còn xuất hiện nữa. Các tệp trong thư mục con của thư mục tests không được biên dịch như các crate riêng biệt hoặc có phần trong kết quả kiểm thử.

Sau khi chúng ta đã tạo tests/common/mod.rs, chúng ta có thể sử dụng nó từ bất kỳ tệp kiểm thử tích hợp nào như một module. Đây là một ví dụ về việc gọi hàm setup từ kiểm thử it_adds_two trong tests/integration_test.rs:

Tên tệp: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

Lưu ý rằng khai báo mod common; giống như khai báo module mà chúng ta đã minh họa trong Listing 7-21. Sau đó, trong hàm kiểm thử, chúng ta có thể gọi hàm common::setup().

Kiểm Thử Tích Hợp cho Các Crate Nhị Phân

Nếu dự án của chúng ta là một crate nhị phân chỉ chứa một tệp src/main.rs và không có tệp src/lib.rs, chúng ta không thể tạo kiểm thử tích hợp trong thư mục tests và đưa các hàm được định nghĩa trong tệp src/main.rs vào phạm vi với một câu lệnh use. Chỉ có các crate thư viện mới để lộ các hàm mà các crate khác có thể sử dụng; các crate nhị phân được thiết kế để chạy độc lập.

Đây là một trong những lý do các dự án Rust cung cấp một tệp nhị phân có một tệp src/main.rs đơn giản gọi logic nằm trong tệp src/lib.rs. Sử dụng cấu trúc đó, kiểm thử tích hợp có thể kiểm thử crate thư viện với use để làm cho chức năng quan trọng có sẵn. Nếu chức năng quan trọng hoạt động, lượng mã nhỏ trong tệp src/main.rs cũng sẽ hoạt động, và lượng mã nhỏ đó không cần phải được kiểm thử.

Tóm Tắt

Các tính năng kiểm thử của Rust cung cấp một cách để chỉ định cách mã nên hoạt động để đảm bảo nó tiếp tục hoạt động như bạn mong đợi, ngay cả khi bạn thực hiện các thay đổi. Kiểm thử đơn vị kiểm tra các phần khác nhau của thư viện một cách riêng biệt và có thể kiểm tra các chi tiết triển khai riêng tư. Kiểm thử tích hợp kiểm tra xem nhiều phần của thư viện có hoạt động cùng nhau một cách chính xác hay không, và chúng sử dụng API công khai của thư viện để kiểm tra mã theo cùng cách mà mã bên ngoài sẽ sử dụng nó. Mặc dù hệ thống kiểu và quy tắc sở hữu của Rust giúp ngăn chặn một số loại lỗi, các kiểm thử vẫn quan trọng để giảm các lỗi logic liên quan đến cách mã của bạn được mong đợi sẽ hoạt động.

Hãy kết hợp kiến thức bạn đã học trong chương này và trong các chương trước để làm việc trên một dự án!