Testing

Testing materi yang baik untuk dipelajari sekarang setelah memahami tentang module. Melakukan test pada code Anda sangatlah mudah di Rust, karena Anda bisa menuliskan testnya tepat setelah penulisan codenya.

Cara termudah untuk menuliskan test adalah menambahkan #[test] di atas functionnya. Seperti inilah contohnya:


#![allow(unused)]
fn main() {
#[test]
fn two_is_two() {
    assert_eq!(2, 2);
}
}

Tapi jika Anda mencoba menjalankannya di Playground, ia akan memberikan error: error[E0601]: `main` function not found in crate `playground. Itu dikarenakan Anda tidak bisa menggunakan Run untuk menjalankan testnya, semestinya Anda menggunakan Test. Dan juga, Anda tidak menggunakan function main() untuk testnya - testnya ditulis di luar main(). Untuk menjalankan ini di Playground, klik pada 路路路 disebelah RUN dan ubah ke Test. Sekarang jika Anda mengkliknya, maka testnya akan dijalankan. (Jika Anda sudah menginstall Rust, Anda bisa menggunakan cargo test untuk melakukan ini)

Inilah outputnya:

running 1 test
test two_is_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Mari kita ubah assert_eq!(2, 2) menjadi assert_eq!(2, 3) dan kita lihat apa yang akan kita dapatkan. Jika testnya gagal, Anda akan mendapatkan informasi:

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

assert_eq!(left, right) adalah cara yang paling umum digunakan untuk melakukan test pada sebuah function di Rust. Jika ia tidak bekerja, ia akan menunjukkanvalue yang berbeda: left (kiri) bernilai 2, tapi right (kanan) bernilai 3.

Apa yang dimaksud dengan RUST_BACKTRACE=1? Ini adalah sebuah setting pada komputer untuk memberikan lebih banyak informasi tentang error tersebut. Playground juga memiliki fitur itu: klik pada 路路路 di sebelah STABLE dan set Backtracenya menjadi ENABLED. Jika Anda melakukan itu, ia akan memberikanmu sangat banyak informasi:

running 1 test
test two_is_two ... FAILED

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: 2 == 3', src/lib.rs:3:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
   1: backtrace::backtrace::trace_unsynchronized
             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print_fmt
             at src/libstd/sys_common/backtrace.rs:78
   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
             at src/libstd/sys_common/backtrace.rs:59
   4: core::fmt::write
             at src/libcore/fmt/mod.rs:1076
   5: std::io::Write::write_fmt
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/io/mod.rs:1537
   6: std::io::impls::<impl std::io::Write for alloc::boxed::Box<W>>::write_fmt
             at src/libstd/io/impls.rs:176
   7: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:62
   8: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:49
   9: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:198
  10: std::panicking::default_hook
             at src/libstd/panicking.rs:215
  11: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:486
  12: std::panicking::begin_panic
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:410
  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2
  15: core::ops::function::FnOnce::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libcore/ops/function.rs:232
  16: <alloc::boxed::Box<F> as core::ops::function::FnOnce<A>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/liballoc/boxed.rs:1076
  17: <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:318
  18: std::panicking::try::do_call
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:297
  19: std::panicking::try
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panicking.rs:274
  20: std::panic::catch_unwind
             at /rustc/c367798cfd3817ca6ae908ce675d1d99242af148/src/libstd/panic.rs:394
  21: test::run_test_in_process
             at src/libtest/lib.rs:541
  22: test::run_test::run_test_inner::{{closure}}
             at src/libtest/lib.rs:450
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Anda tidak perlu menggunakan backtrace kecuali Anda benar-benar tidak menemukan dimana problemnya berasal. Tapi sebenarnya Anda tidak perlu untuk memahami semua yang tertulis di situ. Jika Anda tetap membaca hasil backtrace tersebut, Anda nantinya akan melihat line 13 dimana ia mengatakan playground - itu adalah bagian dimana backtrace berbicara tentang code kita. Sedangkan yang lainnya itu adalah tentang apa yang Rust lakukan pada library yang lain untuk menjalankan program kita. Namun dua line ini menunjukkan Anda bahwa ia melihat pada baris 2 dan baris 3 yang ada di playground, yang mana adalah petunjuk untuk memeriksa apakah ada kesalahan disana. Inilah bagian dari backtrace tersebut:

  13: playground::two_is_two
             at src/lib.rs:3
  14: playground::two_is_two::{{closure}}
             at src/lib.rs:2

Edit: Rust memutakhirkan backtrace messagenya di awal 2021 untuk hanya menampilkan information yang paling penting. Dan sekarang ia jadi lebih mudah untuk dibaca:

failures:

---- two_is_two stdout ----
thread 'two_is_two' panicked at 'assertion failed: `(left == right)`
  left: `2`,
 right: `3`', src/lib.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/std/src/panicking.rs:493:5
   1: core::panicking::panic_fmt
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/panicking.rs:92:14
   2: playground::two_is_two
             at ./src/lib.rs:3:5
   3: playground::two_is_two::{{closure}}
             at ./src/lib.rs:2:1
   4: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
   5: core::ops::function::FnOnce::call_once
             at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.


failures:
    two_is_two

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

Sekarang kita ubah lagi backtracenya menjadi dan kembali ke test yang biasanya. Sekarang kita akan menuliskan function lainnya, dan menggunakan function test untuk mengetestnya. Berikut adalah contohnya:


#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
#[test]
fn it_returns_two() {
    assert_eq!(return_two(), 2);
}

fn return_six() -> i8 {
    4 + return_two()
}
#[test]
fn it_returns_six() {
    assert_eq!(return_six(), 6)
}
}

Dan ini adalah hasil dari menjalankan kedua function test tersebut:

running 2 tests
test it_returns_two ... ok
test it_returns_six ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Tentu saja ini tidak sulit untuk dipahami.

Biasanya kita ingin menempatkan test yang kita buat di module mereka sendiri. Untuk melakukan hal ini, gunakan keyword mod dan tambahkan #[cfg(test)] di atasnya (ingat: cfg berarti "configure"/"pengaturan"). Anda juga perlu untuk selalu menuliskan #[test] di bagian atas setiap test. Ini karena nantinya di saat Anda sudah menginstall Rust, Anda bisa melakukan testing yang jauh lebih rumit. Anda bisa mejalankan satu test saja, atau semuanya, atau hanya beberapa saja. Juga jangan lupa untuk menuliskan use super::*; karena module test perlu menggunakan function-function di atasnya. Sekarang codenya menjadi seperti ini:


#![allow(unused)]
fn main() {
fn return_two() -> i8 {
    2
}
fn return_six() -> i8 {
    4 + return_two()
}

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

    #[test]
    fn it_returns_six() {
        assert_eq!(return_six(), 6)
    }
    #[test]
    fn it_returns_two() {
        assert_eq!(return_two(), 2);
    }
}
}

Test-driven development

Anda mungkin pernah mendengar tentang "test-driven development" di saat membaca tentang Rust atau bahasa pemrograman yang lain. Itu adalah salah satu cara untuk menulis program, dan beberapa orang menyukainya, sedangkan beberapa lagi lebih menyukai cara lain. "Test-driven development" berarti "tulis testnya terlebih dahulu, lalu tulis codenya kemudian". Saat Anda menggunakan cara ini, Anda akan memiliki banyak test yang merepresentasikan apa yang Anda inginkan pada code yang Anda tuliskan nantinya. Kemudian Anda mulai menulis codenya, dan menjalankan testing untuk melihat apakah Anda melakukannya dengan benar. Kemudian test selalu ada untuk menunjukkan kepada Anda jika terjadi kesalahan saat Anda menambahkan dan menulis ulang code Anda. Ini cukup mudah di Rust karena compiler memberikan banyak informasi tentang apa yang harus diperbaiki. Mari kita tulis contoh kecil test-driven development dan kita lihat seperti apa bentuk codenya.

Mari bayangkan sebuah calculator yang mengambil inputan dari user. Ia bisa melakukan penjumlahan (+) dan juga pengurangan (-). Jika user menuliskan "5 + 6" ia semestinya mengembalikan 11, jika user menuliskan "5 + 6 - 7" maka semestinya mengembalikan 4, dst. Jadi, kita akan mulai dengan function testnya. Anda juga bisa melihat bahwa nama function di dalam test biasanya lumayan panjang. Ini dikarenakan kita ingin menjalankan begitu banyak test, dan kita ingin mengetahui test yang mana saja yang gagal.

Kita akan bayangkan ada satu function bernama math() yang akan melakukan apapun. Ia akan mengembalikan i32 (kita tidak menggunakan floats). Karena ia perlu mengembalikan sesuatu, kita hanya akan mengembalikan 6 setiap saat. Kemudian kita menuliskan tiga buah function test. Dan tentu saja semuanya akan gagal. Sekarang codenya terlihat seperti berikut:


#![allow(unused)]
fn main() {
fn math(input: &str) -> i32 {
    6
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
}
}

Inilah informasi yang diberikan oleh test tersebut:

running 3 tests
test tests::one_minus_minus_one_is_two ... FAILED
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_plus_one_is_two ... FAILED

dan juga informasi failure lainnya yang menuliskan tentang thread 'tests::one_plus_one_is_two' panicked at 'assertion failed: `(left == right)` . Kita tidak perlu menuliskan itu semua di sini.

Sekarang pikirkan tentang bagaimana membuat kalkulator. Kita akan menerima angka apapun, dan simbol +-. Kita juga memperbolehkan penggunaan spasi, selain dari itu, karakter apapun tidak diperbolehkan. Jadi, mari kita mulai dengan const yang berisi semua valuenya. Kemudian kita menggunakan .chars() untuk melakukan iterasi berdasarkan karakter, dan .all() untuk memastikan karakter yang dimasukkan merupakan bagian dari karakter yang boleh dimasukkan.

Kemudian kita akan menambahkan test yang harus memunculkan panic. Untuk melakukan itu, tambahkan attribute #[should_panic]: sekarang jika ia panic, testnya akan berhasil.

Sekarang codenya menjadi seperti ini:


#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- "; // Jangan lupakan spasi pada bagian akhir dari kumpulan karakter

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
        panic!("Please only input numbers, +-, or spaces");
    }
    6 // kita tetap mengembalikan 6 untuk sekarang
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }

    #[test]
    #[should_panic]  // ini adalah test yang baru kita buat - seharusnya ia panic
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Sekarang, di saat kita menjalankan testnya kita mendapatkan hasil berikut:

running 4 tests
test tests::one_minus_two_is_minus_one ... FAILED
test tests::one_minus_minus_one_is_two ... FAILED
test tests::panics_when_characters_not_right ... ok
test tests::one_plus_one_is_two ... FAILED

Satu test berhasil! Function math() kita selanjutnya akan menerima inputan yang benar.

Langkah selanjutnya adalah menuliskan aplikasi kalkulator yang sebenarnya. Bagian menarik dari menuliskan testnya di awal adalah code dari programnya dibuat jauh setelahnya. Pertama, kita akan membuat aturan-aturan untuk kalkulator kita. Kita menginginkan aturan-aturan berikut:

  • Semua space kosong akan dihapus. Ini mudah dilakukan menggunakan .filter()
  • Inputnya harus diubah menjadi Vec. + tidak perlu dijadikan input. Namun di saat program melihat +, program harus tahu bahwa angka yang sebelumnya telah selesai. Contohnya, input 11+1 akan diperlakukan seperti ini: 1) Ada 1, push angka tersebut ke string kosong. 2) Selanjutnya ada 1 lagi, push lagi ke dalam string (sekarang string tersebut adalah "11"). 3) Ada tanda +, program mengetahui bahwa string angka yang sebelumnya telah selesai. String tersebut dipush ke dalam vec, dan kemudian menghapus string tersebut.
  • Program kalkulator harus menghitung jumlah dari tanda -. Jika jumlahnya ganjil (1, 3, 5...), artinya itu adalah sebuah pengurangan. Jika ia berjumlah genap (2, 4, 6...) maka itu adalah sebuah penjumlahan. Sehingga "1--9" harus memberikan 10, bukan -8.
  • Program harus menghapus apapun setelah angka terakhir. 5+5+++++---- dibuat dari semua karakter yang berada di dalam OKAY_CHARACTERS, tapi ia haruslah mengembalikan 5+5. Hal ini mudah dilakukan dengan menggunakan .trim_end_matches(), dimana ia bisa menghapus apapun (tuliskan karakter yang ingin dihapus) dibagian akhir &str.

(Ah ya, .trim_end_matches() dan .trim_start_matches() adalah method yang sama dengan trim_right_matches() dan trim_left_matches(). Namun kemudian orang-orang menyadari bahwa beberapa bahasa dituliskan dari kanan ke kiri (Persian, Hebrew, dll.) jadi kanan dan kiri dirasa kurang cocok untuk hal ini. Anda mungkin masih akan menemukan method lama tersebut di beberapa code, namun itu sebenarnya adalah code yang sama dengan yang versi end dan start.)

Jadinya kita akan membuat code yang kita buat lolos dari semua test. Setelah lolos dari test-test tersebut, kita bisa melakukan "refactor". Refactor artinya membuat codenya menjadi lebih baik, biasanya dengan menggunakan struct, enum dan juga method. Inilah code yang ditulis agar kita bisa melewati semua test tersebut:


#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric())
    {
        panic!("Please only input numbers, +-, or spaces.");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>(); // Hapus + dan - yang ada pada bagian akhir, dan juga semua spasi
    let mut result_vec = vec![]; // Hasilnya akan masuk ke vec ini
    let mut push_string = String::new(); // Ini adalah string yang kita push setiap saat. Kita akan tetap menggunakannya di dalam loop.
    for character in input.chars() {
        match character {
            '+' => {
                if !push_string.is_empty() { // Jika stringnya kosong, kita tidak menginginkan "" dipush ke dalam result_vec
                    result_vec.push(push_string.clone()); // Namun jika ia tidak kosong, semestinya ia adalah angka. Push ke dalam vec
                    push_string.clear(); // Kemudian clear stringnya
                }
            },
            '-' => { // Jika kita mendapatkan tanda -,
                if push_string.contains('-') || push_string.is_empty() { // periksa untuk mengetahui apakah ia kosong atau memiliki tanda -
                    push_string.push(character) // jika ya, maka push
                } else { // sebaliknya, ia tentunya berisi angka
                result_vec.push(push_string.clone()); // push angkanya ke dalam result_vec, clear dan kemudian push tanda -
                push_string.clear();
                push_string.push(character);
                }
            },
            number => { // number disini maksudnya adalah "apapun yang match". kita menggunakan nama "number disini"
                if push_string.contains('-') { // kita mungkin saja memiliki beberapa karakter - untuk di push pertama kali
                    result_vec.push(push_string.clone());
                    push_string.clear();
                    push_string.push(number);
                } else { // Namun jika kita tidak melakukannya, itu berarti kita bisa push numbernya ke dalam push_string
                    push_string.push(number);
                }
            },
        }
    }
    result_vec.push(push_string); // Push untuk terakhir kalinya setelah loopnya selesai. Kita tidak memerlukan .clone() karena kita tidak memerlukannya lagi

    let mut total = 0; // Sekarang saatnya kita melakukan operasi matematika. Mulai dengan total
    let mut adds = true; // true = tambah, false = kurang
    let mut math_iter = result_vec.into_iter();
    while let Some(entry) = math_iter.next() { // lakukan iter pada semua itemnya
        if entry.contains('-') { // Jika ia memiliki karakter - , periksa apakah jumlahnya genap atau ganjil
            if entry.chars().count() % 2 == 1 {
                adds = match adds {
                    true => false,
                    false => true
                };
                continue; // ke item yang selanjutnya
            } else {
                continue;
            }
        }
        if adds == true {
            total += entry.parse::<i32>().unwrap(); // Jika tidak ada '-', ia semestinya adalah sebuah angka. Jadinya kita aman untuk melakukan unwrap
        } else {
            total -= entry.parse::<i32>().unwrap();
            adds = true;  // Setelah melakukan pengurangan, ubah kembali addsnya menjadi true.
        }
    }
    total // Akhirnya, return totalnya
}
   /// Kita akan menambahkan beberapa test untuk memastikan program kita telah berjalan dengan benar

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0); // Ini adalah test yang baru
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8); // Ini adalah test yang baru
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Dan sekarang semua testnya telah terlewati!

running 6 tests
test tests::one_minus_minus_one_is_two ... ok
test tests::nine_plus_nine_minus_nine_minus_nine_is_zero ... ok
test tests::one_minus_two_is_minus_one ... ok
test tests::eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end ... ok
test tests::one_plus_one_is_two ... ok
test tests::panics_when_characters_not_right ... ok

test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Anda bisa melihat bahwa ada proses "bolak-balik" di saat kita melakukan test-driven development. Kira-kira seperti ini:

  • Pertama, kita tuliskan semua test yang terbersit di pikiran kita.
  • Kemudian kita mulai menuliskan codenya.
  • Di saat proses penulisan code, kita mendapatkan ide untuk menambahkan test-test baru.
  • Anda tambahkan lagi testnya, dan testnya terus bertambah seiring codenya bertambah. Semakin banyak test yang Anda miliki, semakin sering code Anda diperiksa.

Tentu saja, test tidak memeriksa semuanya dan salah jika kita berpikir bahwa "lolos di semua test = codenya sempurna". Tapi test sangatlah berguna di saat code kita mengalami perubahan. Apabila Anda mengubah codenya kemudian dan menjalankan testnya, jika ada salah satunya yang tidak bekerja maka kita akan tahu yang mana yang semestinya kita perbaiki.

Sekarang kita bisa melakukan refactor codenya sedikit demi sedikit. Salah satu cara yang baik untuk melakukan refactor adalah menggunakan clippy. Jika Anda menginstall Rust maka Anda bisa menuliskan perintah cargo clippy, dan jika Anda menggunakan Playground maka klik pada TOOLS dan pilih Clippy. Clippy melihat pada code yang kita buat dan memberikan kita tips untuk membuat codenya menjadi lebih sederhana. Code yang kita buat tidak memiliki kesalahan, namun kita bisa membuatnya menjadi lebih baik.

Clippy memberi tahu kita tentang dua hal:

warning: this loop could be written as a `for` loop
  --> src/lib.rs:44:5
   |
44 |     while let Some(entry) = math_iter.next() { // Iter through the items
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: try: `for entry in math_iter`
   |
   = note: `#[warn(clippy::while_let_on_iterator)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#while_let_on_iterator

warning: equality checks against true are unnecessary
  --> src/lib.rs:53:12
   |
53 |         if adds == true {
   |            ^^^^^^^^^^^^ help: try simplifying it as shown: `adds`
   |
   = note: `#[warn(clippy::bool_comparison)]` on by default
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#bool_comparison

Ini benar: for entry in math_iter lebih simple daripada while let Some(entry) = math_iter.next(). Dan sebuah loop for sebenarnya adalah sebuah iterator sehingga kita tidak punya alasan untuk menuliskan .iter(). Terima kasih, clippy! :D Dan juga kita tidak perlu untuk membuat math_iter: kita hanya perlu menuliskan for entry in result_vec.

Sekarang kita mulai refactor yang sesungguhnya. Alih-alih menggunakan variabel yang terpisah, kita akan membuat struct Calculator. Ia akan memiliki semua variabel yang kita gunakan. Kita akan mengubah dua nama untuk membuatnya menjadi jelas. result_vec akan menjadi results, dan push_string akan menjadi current_input (current berarti "sekarang"). Dan sejauh ini ia hanya memiliki satu method: new.


#![allow(unused)]
fn main() {
// 馃毀
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}
}

Sekarang code kita menjadi agak panjang, namun menjadi lebih mudah untuk dibaca. Contohnya, if adds sekarang menjadi if calculator.adds, yang mana menjadi seperti membaca bahasa Inggris pada umumnya. Codenya menjadi seperti berikut:


#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.current_input.push(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.current_input.clear();
                calculator.current_input.push(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.current_input.clear();
                    calculator.current_input.push(number);
                } else {
                    calculator.current_input.push(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Akhirnya kita menambahkan 2 method baru. Yang satu bernama .clear() dan melakukan clear terhadap current_input(). Yang satu lagi bernama push_char() dan melakukan push terhadap input ke dalam current_input(). Inilah code yang telah sepenuhnya direfactor:


#![allow(unused)]
fn main() {
#[derive(Clone)]
struct Calculator {
    results: Vec<String>,
    current_input: String,
    total: i32,
    adds: bool,
}

impl Calculator {
    fn new() -> Self {
        Self {
            results: vec![],
            current_input: String::new(),
            total: 0,
            adds: true,
        }
    }

    fn clear(&mut self) {
        self.current_input.clear();
    }

    fn push_char(&mut self, character: char) {
        self.current_input.push(character);
    }
}

const OKAY_CHARACTERS: &str = "1234567890+- ";

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) ||
       !input.chars().take(2).any(|character| character.is_numeric()) {
        panic!("Please only input numbers, +-, or spaces");
    }

    let input = input.trim_end_matches(|x| "+- ".contains(x)).chars().filter(|x| *x != ' ').collect::<String>();
    let mut calculator = Calculator::new();

    for character in input.chars() {
        match character {
            '+' => {
                if !calculator.current_input.is_empty() {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                }
            },
            '-' => {
                if calculator.current_input.contains('-') || calculator.current_input.is_empty() {
                    calculator.push_char(character)
                } else {
                calculator.results.push(calculator.current_input.clone());
                calculator.clear();
                calculator.push_char(character);
                }
            },
            number => {
                if calculator.current_input.contains('-') {
                    calculator.results.push(calculator.current_input.clone());
                    calculator.clear();
                    calculator.push_char(number);
                } else {
                    calculator.push_char(number);
                }
            },
        }
    }
    calculator.results.push(calculator.current_input);

    for entry in calculator.results {
        if entry.contains('-') {
            if entry.chars().count() % 2 == 1 {
                calculator.adds = match calculator.adds {
                    true => false,
                    false => true
                };
                continue;
            } else {
                continue;
            }
        }
        if calculator.adds {
            calculator.total += entry.parse::<i32>().unwrap();
        } else {
            calculator.total -= entry.parse::<i32>().unwrap();
            calculator.adds = true;
        }
    }
    calculator.total
}

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

    #[test]
    fn one_plus_one_is_two() {
        assert_eq!(math("1 + 1"), 2);
    }
    #[test]
    fn one_minus_two_is_minus_one() {
        assert_eq!(math("1 - 2"), -1);
    }
    #[test]
    fn one_minus_minus_one_is_two() {
        assert_eq!(math("1 - -1"), 2);
    }
    #[test]
    fn nine_plus_nine_minus_nine_minus_nine_is_zero() {
        assert_eq!(math("9+9-9-9"), 0);
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8);
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

Mungkin ini cukup baik untuk sekarang ini. Kita bisa menuliskan lebih banyak method, namun baris code seperti calculator.results.push(calculator.current_input.clone()); sudah sangat cukup jelas. Refactor yang baik adalah jika Anda masih bisa dengan mudah membaca codenya setelah Anda telah selesai merefactornya. Anda tentunya tidak ingin melakukan refactor hanya untuk membuat codenya terlihat pendek: contohnya, clc.clr() lebih buruk dibanding calculator.clear().