“Rust is a technology that uses knowledge from the past to solve problems of the future.” – Graydon Hoare
“The key to effective development is to keep making new and interesting mistakes.” – Tom Love
“Anything is possible.” – Ian Hickson
“Be conservative with what you send and forgiving with what you receive.” – John Postel,
“If you’re willing to limit the flexibility of your approach, there’s always a bonus.” – John Carmack
“From then on, when there was anything wrong with the computer, we said it had a bug in it.” – Grace Hopper
“Algorithms have to live up to their name.” – Donald Knuth
“An evolving system will increase its complexity unless steps are taken to optimize it.” – Meir Lehman
“Lisp is not a language, it’s a building material.” — Alan Kay
“You can do that, but you better know what you’re doing.” – A Rustacean
“In general, procedures should be careful with their words.” – Kernighan and Plauger
“It’s like a poem. You have to write it.” – e. w. Dijkstra
“The most important attribute of a program is whether it meets the needs of the user.” , c. a. r. Hoare
“Sometimes an elegant implementation is just a function. Not a method, a class, or a framework, just a function.” – John Carmack
“Being curious, reading widely, trying new things, a lot of what people call wisdom boils down to curiosity.” – Aaron Swartz
“In programming, it’s not about solving problems, it’s about deciding which problems to solve.” — Graham
“If debugging is the process of removing bugs, then programming must be the process of introducing bugs.” – e. w. Dijkstra
Where do these classic statements come from?
This is culled from The Mastery of Rust (2nd edition). What kind of book is this?
The book is about the Rust programming language, which enables you to build all kinds of software systems — from underlying embedded software to dynamic Web applications. Fast, reliable, and secure, Rust offers performance and security guarantees that exceed even C/C++, and is a popular programming language with a smooth learning curve. Through gradual improvement, combined with a positive and friendly community culture, the language has a bright future.
The book consists of 17 chapters that explain Rust from the basics down to the basics, It covers basic syntax, package manager, test tools, type system, memory management, exception handling, advanced types, concurrency model, macros, external function interfaces, network programming, HTTP, databases, WebAssembly, GTK+ framework and GDB debugging.
This book is for those who want to learn Rust programming, and hopefully have some familiarity with C, C++, or Python. Rich code examples and detailed explanations will help you quickly and efficiently master Rust programming.
Sample chapter: excerpted from part of chapter 3
3.1 Purpose of the test
“Anything is possible.”
– Ian Hickson
Software systems are like machines with gears and other parts. If any one of the gears fails, the whole machine may not work properly. In software, the gears are the functions, modules, or libraries that you use. Functional testing of each component of a software system is a way to ensure high quality, effective and practical code. It doesn’t verify that there are bugs in the code, but it helps build the developer’s confidence to deploy the code into production and keep it robust while the project is in long-term maintenance.
In addition, without unit testing, it is difficult to do large-scale refactoring in software. The benefits of judicious and balanced use of unit testing in software are long term. In the code implementation phase, well-written unit tests are an informal specification for software components. During the maintenance phase, existing unit tests can be used to prevent regression of the code base, thus encouraging the system to fix problems immediately. In a compiled language like Rust, the regression of unit tests is better because the compiler provides useful error diagnosis information with more “guidance” on the refactoring (if any) involved.
Another benefit of unit testing is that it encourages programmers to write modular code that relies primarily on input parameters, known as stateless functions. This allows programmers to avoid writing code that depends on globally mutable state. Tests that rely on globally mutable state are difficult to construct, but the mere act of thinking about writing tests for a piece of code can help programmers catch low-level errors during implementation. They are also great documentation for anyone new to trying to understand how different parts of a code base interact with each other.
It’s important to note that testing is integral to any software project. Now, let’s take a look at how to write tests in Rust, starting with how to structure the tests.
3.2 Organizing tests
When developing software, we typically write two kinds of tests: unit tests and integration tests. They serve different purposes and interact differently with the code under test. Unit tests are always lightweight, single-component tests that developers can run frequently to provide faster feedback loops; Integration testing is large and simulates real application scenarios based on the environment and specifications. Rust’s built-in testing framework gives us reasonable default parameters for writing and organizing these tests.
- Unit tests: Unit tests are usually written in the same module that contains the code being tested. As the number of these tests increases, they are organized into an entity in the form of nested modules. Typically, you create a submodule in the current module, name the test (for example, tests by convention), add the appropriate annotation attribute (#[CFG (test)]), and then put all the functions related to the test into it. This property simply tells the compiler to reference code in the test module, but only when the cargo test command is executed. More information about attributes will be provided later.
- Integration tests: Integration tests are written separately in the Tests/directory under the library root. They are constructed to behave themselves as users of the library under test. Any.rs file in the tests/ directory can add a use declaration to introduce any public API that needs to be tested.
To write any of these tests, we need to be familiar with some of the test-related primitives.
Test the primitive
Rust’s built-in testing framework is based on primitives composed of a series of primary properties and macros. Before we write any actual tests, it is important to be familiar with how to use them effectively.
attribute
Attributes in Rust code refer to comments on elements. Element items are top-level language constructs in the package (Crate), such as functions, modules, constructs, enumerations, and declared constants, as well as other contents defined in the package root. Properties are usually built into the compiler, but can also be created by the user through a compiler plug-in. They instruct the compiler to inject additional code or meaning into the element shown below, and apply the above rules to the module if it corresponds to it. We’ll cover this in more detail in Chapter 7. To simplify the topic discussed in this section, we will discuss two types of attributes.
- #[
] : This applies to each element and is usually shown above their definition. For example, test functions in Rust are annotated with the #[test] attribute. It means that the function will be treated as part of the test tool.
- #! [
] : This applies to each package. Note that it contains an extra “! “as opposed to #[
]. . It is usually located in the top part of the root directory of the user package.
Pay attention to
If you are creating a library project, the file in the project root will typically be a lib.rs file, whereas when creating a binary project, the file in the project root will be a main.rs file.
There are other forms of attributes, such as #[CFG (test)], used when writing tests in modules. This property is added to the test module to prompt the compiler to conditionally compile the module, but only in test mode.
Attributes are not limited to test code; they are widely used in Rust code. More on this later.
Assertion macros
In testing, when given a test case, we try to assert the expected behavior of a program component within a given input interval. Languages often provide functions called assertion functions to perform these assertions. Rust provides us with assertion functions implemented through macros that help us achieve the same functionality. The following sections describe some common assertion functions.
- assert! : This is the simplest assertion macro and is asserted by a Boolean value. If the value is false, the test fails and the line of code that produced the error is prompted. You can also add additional formatting strings, followed by a corresponding number of variables, to provide custom exception messages:
assert! (true); assert! (a == b, "{} was not equal to {}", a, b);Copy the code
- assert_eq! : This accepts two values and fails if they are not equal. It can also take a custom formatted string for exception information:
let a = 23; let b = 87; assert_eq! (a, b, "{} and {} are not equal", a, b);Copy the code
- assert_ne! : this and assert_eq! Similarly, because it needs to receive two values, but only asserts if the two values are not equal.
- debug_assert! : This is similar to Assert! . The DEBUG assertion macro can also be used in code other than test code. In other code, this is primarily used to assert any contracts or immutability that should be saved while the code is running. These assertions are valid only in the debug version and help catch assertion exceptions when running code in debug mode. When the code is compiled in optimized mode, these macro calls are ignored and optimized for no operations. It also has similar variations, such as debug_ASSERt_eq! And debug_assert_ne!!! They work like Assert! Macro.
In order to compare values in these assertion macros, Rust needs to rely on characteristics. For example, “asserts. The “==” in (a == b) “will actually turn into a method call, a.eq(&b), with the eq method coming from the feature PartialEq. Most of the built-in types in Rust implement the PartialEq and Eq characteristics, so they can be compared. The details of these features and the differences between PartialEq and Eq are discussed in Chapter 4.
However, for user-defined types, we need to implement these characteristics. Fortunately, Rust provides us with a handy macro called Derive that implements one or more features based on the name. You can use it by placing the #[derive(Eq, PartialEq)] annotation on any user-defined type, but note the feature names in parentheses. Derive is a procedure macro that simply generates code for the IMPL block of the type that implements it and implements the feature methods or any associated functions. We’ll discuss these macros in more detail in Chapter 9.
Next, let’s start writing some tests.
3.3 Unit Tests
Typically, a unit test is a function that instantiates a small part of the application and verifies its behavior independently of the rest of the code base. In Rust, unit tests are typically written in modules. Ideally, they should be used only to cover the functionality of a module and its interfaces.
3.3.1 First unit test
Here’s our first unit test:
// first_unit_test.rs #[test] fn basic_test() { assert! (true); }Copy the code
A unit test is constructed as a function and marked with the [test] property. There is nothing complicated about the previous basic_test function. One of the basic assertions is assert! , taking the true value as an argument. To better organize your code, you can also create a sub-module called Tests (by convention) and put all relevant tests into it.
3.3.2 Running tests
The way we run this test is by compiling the code in test mode. The compiler ignores the compilation of a function with a test flag unless it is told to run in test mode. This can be done by passing the –test flag argument to RUSTC when compiling the test code. After that, you simply execute the compiled binaries to run the tests. For the previous test, we will compile it by running the following command in test mode:
rustc --test first_unit_test.rs
Copy the code
By marking the –test parameter, RUSTC puts the main function together with some test tool code and calls all defined test functions in parallel as threads. By default, all tests are run in parallel, unless the following environment variable is set to “RUST_TEST_THREADS=1”. This means that if we want to run the previous test in single-threaded mode, we can do so with “RUST_TEST_THREADS=1”.
Cargo now supports running tests, all of which is usually done internally by calling the Cargo test command. This command compiles and runs the test marked function for us. In the following examples, we will mainly use Cargo to perform the tests.
3.3.3 Isolate the test code
As our tests become more complex, we may need to create additional helper methods that can only be used in the context of the code being tested. In this case, it is beneficial to separate the relevant test code from the actual code. We can do this by encapsulating all test-related code in a module and placing #[CFG (test)] annotation flags on it.
#[cfg(…)] CFG in the property is commonly used for conditional compilation, but is not limited to test code. It can reference or exclude certain code for different architectures or configurations. The configuration flag here is test. You may recall that this format was used in chapter 2’s tests. The advantage of this is that only when you run cargo test will the test code be compiled and included in the compiled binary, otherwise it will be ignored.
Suppose you want to generate test data programmatically, but you don’t have to include it in the official release. Let’s demonstrate this by running the cargo new unit_test –lib command. In lib.rs, we define some tests and functions:
// we want to test the function fn sum(a: i8, b: i8) -> i8 { a + b } #[cfg(test)] mod tests { fn sum_inputs_outputs() -> Vec<((i8, i8), i8)> { vec! [((1, 1), 2), ((0, 0), 0), ((2, -2), 0)] } #[test] fn test_sums() { for (input, output) in sum_inputs_outputs() { assert_eq! (crate::sum(input.0, input.1), output); }}}Copy the code
We can run these tests using the cargo test command. Let’s look at the code in detail. The sum_inputs_outputs function generates known input and output pairs. The #[test] attribute prevents the test_sums function from appearing in the official release of the compile. Sum_inputs_outputs, however, is not marked with #[test] and would have been included in the official build if it had been declared outside the Tests module. By pairing the #[CFG (test)] flag with a Mod Tests submodule and encapsulating all the test code and its related functions into this module, you can ensure that the code and the generated binaries are pure test code.
Our sum function is private without the pub keyword modifier preceding it, which means that the unit tests in the module also allow users to test private functions and methods. It would be very convenient.
3.3.4 Fault Test
There are also test cases where the user wants the API method to fail based on some input and wants the test framework to assert this failure. Rust provides an attribute called #[should_panic] for this purpose. Here is a test that uses this property:
// panic_test.rs #[test] #[should_panic] fn this_panics() { assert_eq! (1, 2); }Copy the code
The #[should_panic] attribute can be used with the #[test] attribute to indicate that running this_panics should cause an unrecoverable failure, and such exceptions are known as panic in Rust.
3.3.5 Ignoring tests
Another useful attribute when writing tests is #[ignore]. If your test code is very large, you can use the #[ignore] attribute tag to tell the testing tool to ignore such tests when executing cargo test. You can then run these tests independently by passing the –ignored parameter to the test tool or cargo test command. The following code contains an unwieldy loop that is ignored by default when cargo test is run:
// silly_loop.rs pub fn silly_loop() { for _ in 1.. 1 _000_000_000 {}; } #[cfg(test)] mod tests { #[test] #[ignore] pub fn test_silly_loop() { ::silly_loop(); }}Copy the code
Note the #[ignore] property at the top of test_silly_loop. Here is the output if the test is ignored:
Pay attention to
You can also run individual tests by giving Cargo a test function name, such as Cargo test some_test_func.
3.4 Integration Test
While unit testing can test a user’s software package and private interfaces inside a module, integration testing is somewhat similar to black box testing, designed to test end-to-end use of a software package’s public interfaces from a consumer perspective. In terms of writing code, there is not much difference between writing integration tests and unit tests; the only difference is that the directory structure and the projects in it need to be exposed, and the developers have exposed those projects according to the design principles of the package.
3.4.1 First integration test
As mentioned earlier, Rust expects all integration tests to be done under the Tests/directory. When we test the library, the files in the tests/ directory are compiled into a relatively separate binary package. In the following example, we will create a new library by running the cargo new Integration_test –lib command. It contains the same sum function as the previous unit tests, but we now add a Tests/directory that contains an integration test function defined as follows:
// integration_test/tests/sum.rs use integration_test::sum; #[test] fn sum_test() { assert_eq! (sum(6, 8), 14); }Copy the code
First, the sum function is scoped. Second, we use a sum_test function that calls the sum and assertion functions when it returns a value. When we try to run the cargo test command, we get the following error:
This seems a reasonable mistake. The sum function is expected to be called by the user of the library, but is defined as private by default in the library. So, after adding the pub modifier to the sum function and running cargo test again, the compilation passed without a problem:
Here is the directory tree view of our integration_test sample library:
. ├ ─ ─ Cargo. Lock ├ ─ ─ Cargo. Toml ├ ─ ─ the SRC │ └ ─ ─ lib. Rs └ ─ ─ tests └ ─ ─ sum. The rsCopy the code
As an example of integration testing, this is very simple. The key is that when we write integration tests, we can use the package under test just like any other user of the library.