测试

现在我们已经了解了模块,就可以谈谈测试了。在Rust中测试你的代码是非常容易的,因为你可以在你的代码旁边写测试。

开始测试的最简单的方法是在一个函数上面添加#[test]。下面是一个简单的例子。


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

但如果你试图在playground中运行它,它给出了一个错误。error[E0601]: `main` function not found in crate `playground. 这是因为你不使用 Run 来进行测试,你使用 Test 。另外,你不使用 main() 函数进行测试 - 它们在外面运行。要在Playground中运行这个,点击 RUN 旁边的···,然后把它改为 Test 。现在如果你点击它,它将运行测试。(如果你已经安装了 Rust,你将输入 cargo test 来做这个测试)

这里是输出:

running 1 test
test two_is_two ... ok

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

让我们把assert_eq!(2, 2)改成assert_eq!(2, 3),看看会有什么结果。当测试失败时,你会得到更多的信息。

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)是Rust中测试一个函数的主要方法。如果它不工作,它将显示不同的值:左边有2,但右边有3。

RUST_BACKTRACE=1是什么意思?这是计算机上的一个设置,可以提供更多关于错误的信息。幸好playground也有:点击STABLE旁边的···,然后设置回溯为ENABLED。如果你这样做,它会给你很多的信息。

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

除非你真的找不到问题所在,否则你不需要使用回溯。但幸运的是你也不需要全部理解。 如果你继续阅读,你最终会看到第13行,那里写着playground--那是它提到的你的代码的位置。其他的都是关于Rust为了运行你的程序,在其他库中所做的事情。但是这两行告诉你,它看的是playground的第2行和第3行,这是一个提示,要检查那里。这里是那个部分:

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

编辑:Rust在2021年初改进了其回溯信息,只显示最有意义的信息。现在它更容易阅读。

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

现在我们再把回溯关闭,回到常规测试。现在我们要写一些其他函数,并使用测试函数来测试它们。这里有几个:


#![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)
}
}

现在,都能运行:

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

这不是太难。

通常你会想把你的测试放在自己的模块中。要做到这一点,请使用相同的 mod 关键字,并在其上方添加 #[cfg(test)](记住:cfg 的意思是 "配置")。你还要在每个测试上面继续写#[test]。这是因为以后当你安装Rust时,你可以做更复杂的测试。你将可以运行一个测试,或者所有的测试,或者运行几个测试。另外别忘了写use super::*;,因为测试模块需要使用上面的函数。现在它看起来会是这样的。


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

测试驱动的开发

在阅读Rust或其他语言时,你可能会看到 "测试驱动开发"这个词。这是编写程序的一种方式,有些人喜欢它,而有些人则喜欢其他的方式。"测试驱动开发"的意思是 "先写测试,再写代码"。当你这样做的时候,你会有很多关于你想要你的代码做的所有事情的测试代码。然后你开始写代码,并运行测试,看看你是否做对了。然后,当你添加和重写代码时,如果有什么地方出了问题,测试代码会一直在那里向你展示。这在Rust中是非常容易的,因为编译器给出了很多待修复内容的信息。让我们写一个测试驱动开发的小例子,看看它是什么样子的。

让我们想象一个接受用户输入的计算器。它可以加(+),也可以减(-)。如果用户写 "5+6",它应该返回11,如果用户写 "5+6-7",它应该返回4,以此类推。所以我们先从测试函数开始。你也可以看到,测试中的函数名通常都相当长。这是因为你可能会运行很多测试,你想了解哪些测试失败了。

我们想象一下,一个名为math()的函数就可以完成所有的工作。它将返回一个 i32(我们不会使用浮点数)。因为它需要返回一些东西,所以我们每次都只返回 6。然后我们将写三个测试函数。当然,它们都会失败。现在的代码是这样的。


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

它给了我们这个信息。

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

以及thread 'tests::one_plus_one_is_two' panicked at 'assertion failed: `(left == right)` 的所有信息。我们不需要在这里全部打印出来。

现在要考虑如何创建计算器。我们将接受任何数字,以及符号+-。我们将允许空格,但不允许其他任何东西。所以,让我们从包含所有数值的const开始。然后我们将使用 .chars() 按字符进行迭代,并使用 .all() 确保它们都在里面。

然后,我们将添加一个会崩溃的测试。要做到这一点,添加 #[should_panic] 属性:现在如果它崩溃,测试将成功。

现在代码看起来像这样:


#![allow(unused)]
fn main() {
const OKAY_CHARACTERS: &str = "1234567890+- "; // Don't forget the space at the end

fn math(input: &str) -> i32 {
    if !input.chars().all(|character| OKAY_CHARACTERS.contains(character)) {
        panic!("Please only input numbers, +-, or spaces");
    }
    6 // we still return a 6 for now
}

[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]  // Here is our new test - it should panic
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

现在,当我们运行测试时,我们得到这样的结果。

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

一个成功了! 我们的math()函数现在只能接受好的输入了。

下一步是编写实际的计算器。这就是先有测试的有趣之处:实际的代码要晚很多。首先,我们将把计算器的逻辑放在一起。我们要做到以下几点。

  • 所有的空位都应该被删除。这在.filter()中很容易实现。
  • 所有输入应该变成一个Vec+不需要成为输入,但是当程序看到+时,应该知道这个数字已经完成了。例如,输入+应该这样做:
    1. 看到1,把它推到一个空字符串中。
    2. 看到另一个1,把它推入字符串中(现在是 "11")。
    3. 看到一个+,知道这个数字已经结束。它会把字符串推入vec中,然后清空字符串。
  • 程序必须计算出-的数量。奇数(1,3,5...)表示减法,偶数(2,4,6...)表示加法。所以 "1--9"应该是10,而不是-8。
  • 程序应该删除最后一个数字后面的任何东西。5+5+++++----是由OKAY_CHARACTERS中的所有字符组成的,但它应该变成5+5.trim_end_matches()就很简单了,你把&str末尾符合的东西都去掉。

顺便说一下,.trim_end_matches().trim_start_matches()曾经是trim_right_matches()trim_left_matches()。但后来人们注意到有些语言是从右到左(波斯语、希伯来语等),所以左右都是错的。你可能还能在一些代码中看到旧的名字,但它们是一样的)。)

首先我们只想通过所有的测试。通过测试后,我们就可以 "重构"了。重构的意思是让代码变得更好,通常是通过结构、枚举和方法等方式。下面是我们使测试通过的代码。


#![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>(); // Remove + and - at the end, and all spaces
    let mut result_vec = vec![]; // Results go in here
    let mut push_string = String::new(); // This is the string we push in every time. We will keep reusing it in the loop.
    for character in input.chars() {
        match character {
            '+' => {
                if !push_string.is_empty() { // If the string is empty, we don't want to push "" into result_vec
                    result_vec.push(push_string.clone()); // But if it's not empty, it will be a number. Push it into the vec
                    push_string.clear(); // Then clear the string
                }
            },
            '-' => { // If we get a -,
                if push_string.contains('-') || push_string.is_empty() { // check to see if it's empty or has a -
                    push_string.push(character) // if so, then push it in
                } else { // otherwise, it will contain a number
                result_vec.push(push_string.clone()); // so push the number into result_vec, clear it and then push -
                push_string.clear();
                push_string.push(character);
                }
            },
            number => { // number here means "anything else that matches". We selected the name here
                if push_string.contains('-') { // We might have some - characters to push in first
                    result_vec.push(push_string.clone());
                    push_string.clear();
                    push_string.push(number);
                } else { // But if we don't, that means we can push the number in
                    push_string.push(number);
                }
            },
        }
    }
    result_vec.push(push_string); // Push one last time after the loop is over. Don't need to .clone() because we don't use it anymore

    let mut total = 0; // Now it's time to do math. Start with a total
    let mut adds = true; // true = add, false = subtract
    let mut math_iter = result_vec.into_iter();
    while let Some(entry) = math_iter.next() { // Iter through the items
        if entry.contains('-') { // If it has a - character, check if it's even or odd
            if entry.chars().count() % 2 == 1 {
                adds = match adds {
                    true => false,
                    false => true
                };
                continue; // Go to the next item
            } else {
                continue;
            }
        }
        if adds == true {
            total += entry.parse::<i32>().unwrap(); // If there is no '-', it must be a number. So we are safe to unwrap
        } else {
            total -= entry.parse::<i32>().unwrap();
            adds = true;  // After subtracting, reset adds to true.
        }
    }
    total // Finally, return the total
}
   /// We'll add a few more tests just to make sure

#[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); // This is a new test
    }
    #[test]
    fn eight_minus_nine_plus_nine_is_eight_even_with_characters_on_the_end() {
        assert_eq!(math("8  - 9     +9-----+++++"), 8); // This is a new test
    }
    #[test]
    #[should_panic]
    fn panics_when_characters_not_right() {
        math("7 + seven");
    }
}
}

现在测试通过了!

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

你可以看到,在测试驱动的开发中,有一个来回的过程。它是这样的。

  • 首先你要写出所有你能想到的测试
  • 然后你开始写代码。
  • 当你写代码的时候,你会有其他测试的想法。
  • 你添加测试,你的测试随着你的发展而增长。你的测试越多,你的代码被检查的次数就越多。

当然,测试并不能检查所有的东西,认为 "通过所有测试=代码是完美的"是错误的。但是,测试对于你修改代码的时候是非常好的。如果你以后修改了代码,然后运行测试,如果其中一个测试不成功,你就会知道该怎么修复。

现在我们可以重写(重构)一下代码。一个好的方法是用clippy开始。如果你安装了Rust,那么你可以输入cargo clippy,如果你使用的是Playground,那么点击TOOLS,选择Clippy。Clippy会查看你的代码,并给你提示,让你的代码更简单。我们的代码没有任何错误,但它可以更好。

Clippy会告诉我们两件事。

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

这是真的:for entry in math_iterwhile let Some(entry) = math_iter.next()简单得多。而for循环实际上是一个迭代器,所以我们没有任何理由写.iter()。谢谢你,clippy! 而且我们也不需要做math_iter:我们可以直接写for entry in result_vec

现在我们将开始一些真正的重构。我们将创建一个 Calculator 结构体,而不是单独的变量。这将拥有我们使用的所有变量。我们将改变两个名字以使其更加清晰。result_vec将变成resultspush_string将变成current_input(current的意思是 "现在")。而到目前为止,它只有一种方法: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,
        }
    }
}
}

现在我们的代码其实比较长,但更容易读懂。比如,if adds现在是if calculator.adds,这就跟读英文完全一样。它的样子是这样的:


#![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");
    }
}
}

最后我们增加两个新方法。一个叫做 .clear(),清除 current_input()。另一个叫做 push_char(),把输入推到 current_input() 上。这是我们重构后的代码。


#![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");
    }
}
}

现在大概已经够好了。我们可以写更多的方法,但是像calculator.results.push(calculator.current_input.clone());这样的行已经很清楚了。重构最好是在你完成后还能轻松阅读代码的时候。你不希望只是为了让代码变短而重构:例如,clc.clr()就比calculator.clear()差很多。