Rust Web编程:第九章 测试我们的应用程序端点和组件
liuian 2025-01-03 17:19 41 浏览
我们的待办事项 Rust 应用程序现在可以正常运行了。 我们对第一个版本感到满意,因为它管理身份验证、不同的用户及其待办事项列表,并记录我们的流程以供检查。 然而,网络开发人员的工作永远没有完成。
虽然我们现在已经结束了向应用程序添加功能的过程,但我们知道旅程并不止于此。 在本书之外的未来迭代中,我们可能想要添加团队、新状态、每个用户的多个列表等等。 然而,当我们添加这些功能时,我们必须确保旧应用程序的行为保持不变,除非我们主动更改它。 这是通过构建测试来完成的。
在本章中,我们将构建测试来检查现有行为,设置陷阱,如果应用程序的行为在我们没有主动更改的情况下发生变化,则会抛出错误并向我们报告。 这可以防止我们在添加新功能或更改代码后破坏应用程序并将其推送到服务器。
在本章中,我们将讨论以下主题:
构建我们的单元测试
构建 JWT 单元测试
在 Postman 中编写功能 API 测试
使用 Newman 自动化 Postman 测试
构建完整的自动化测试管道
在本章结束时,我们将了解如何在 Rust 中构建单元测试,并使用一系列边缘情况详细检查我们的结构。 如果我们的结构体以我们不期望的方式运行,我们的单元测试会将其报告给我们。
技术要求
在本章中,我们将基于第 8 章“构建 RESTful 服务”中构建的代码进行构建。
安装和运行自动化 API 测试还需要 Node 和 NPM,可以在 https://nodejs.org/en/download/ 找到这些测试。
我们还将在 Python 中运行自动化测试管道的一部分。 Python 可以在 https://www.python.org/downloads/ 下载并安装。
构建我们的单元测试
在本节中,我们将探讨单元测试的概念以及如何构建包含测试作为函数的单元测试模块。 在这里,我们不会为我们的应用程序实现 100% 的单元测试覆盖率。 我们的应用程序中的某些地方可以通过功能测试来覆盖,例如 API 端点和 JSON 序列化。 然而,单元测试在我们应用程序的某些部分仍然很重要。
单元测试使我们能够更详细地查看一些流程。 正如我们在第 8 章“构建 RESTful 服务”中的日志记录中看到的那样,功能测试可能会按照我们希望的端到端方式工作,但可能会出现我们不想要的边缘情况和行为。 这在上一章中已经看到,我们看到我们的应用程序在一次足够的情况下进行了两次 GET 调用。
在我们的单元测试中,我们将一一分解流程,模拟某些参数,并测试结果。 这些测试是完全隔离的。 这样做的优点是我们可以快速测试一系列参数,而不必每次都运行完整的过程。 这还可以帮助我们准确查明应用程序发生故障的位置以及配置。 单元测试对于测试驱动开发也很有用,在测试驱动开发中,我们一点一点地构建功能的组件,运行单元测试并在测试结果需要时更改组件。
在大型、复杂的系统中,这可以节省大量时间,因为您不必启动应用程序并运行整个系统来发现拼写错误或无法解释边缘情况。
然而,在我们过于兴奋之前,我们必须承认单元测试是一种工具,而不是一种生活方式,并且使用它有一些回退。 测试的好坏取决于他们的模拟。 如果我们不模拟真实的交互,那么单元测试可能会通过,但应用程序可能会失败。 单元测试很重要,但也必须伴随着功能测试。
Rust 仍然是一门新语言,因此目前单元测试支持并不像 Python 或 Java 等其他语言那么先进。 例如,使用 Python,我们可以在测试中的任何时刻轻松模拟任何文件中的任何对象。 通过这些模拟,我们可以定义结果并监控交互。 虽然 Rust 没有那么容易获得这些模拟,但这并不意味着我们不能进行单元测试。
一个糟糕的工匠总是责怪他们的工具。 成功的单元测试背后的技巧是以这样一种方式构建我们的代码:各个代码片段不会相互依赖,从而使代码片段拥有尽可能多的自主权。 由于缺乏依赖性,可以轻松执行测试,而无需复杂的模拟系统。
首先,我们可以测试我们的待办事项结构。 您会记得,我们有 did 和 pending 结构,它们继承了一个基本结构。 我们可以从对没有依赖项的结构进行单元测试开始,然后向下移动到具有依赖项的其他结构。 在 src/to_do/structs/base.rs 文件中,我们可以使用以下代码在文件底部定义基本结构的单元测试:
#[cfg(test)]
mod base_tests {
use super::Base;
use super::TaskStatus;
#[test]
fn new() {
let expected_title = String::from("test title");
let expected_status = TaskStatus::DONE;
let new_base_struct = Base{
title: expected_title.clone(),
status: TaskStatus::DONE
};
assert_eq!(expected_title,
new_base_struct.title);
assert_eq!(expected_status,
new_base_struct.status);
}
}
在前面的代码中,我们仅创建一个结构体并评估该结构体的字段,确保它们符合我们的预期。 我们可以看到我们创建了测试模块,该模块用 #[cfg(test)] 属性进行注释。 #[cfg(test)] 属性是一个条件检查,其中代码仅在我们运行 Cargo test 时才处于活动状态。 如果我们不运行cargo test,则不会编译带有#[cfg(test)]注释的代码。
在模块内部,我们将从 base_tests 模块外部的文件导入 Base 结构,该模块仍在文件中。 在 Rust 世界中,通常使用 super 导入我们正在测试的内容。 有一个完善的标准,将测试代码放在同一文件中正在测试的代码的正下方。 然后,我们将通过用 #[test] 属性装饰我们的新函数来测试 Base::new 函数。
这是我们第一次讨论属性。 属性只是应用于模块和函数的元数据。 该元数据通过向编译器提供信息来帮助编译器。 在这种情况下,它告诉编译器该模块是一个测试模块并且该函数是一个单独的测试。
但是,如果我们运行前面的代码,它将不起作用。 这是因为 Eq 特征没有在 TaskStatus 枚举中实现,这意味着我们无法执行以下代码行:
assert_eq!(expected_status, new_base_struct.status);
这也意味着我们不能在两个 TaskStatus 枚举之间使用 == 运算符。 因此,在尝试运行测试之前,我们必须使用以下代码在 src/to_do/structs/enums.rs 文件中的 TaskStatus 枚举上实现 Eq 特征:
#[derive(Clone, Eq, Debug)]
pub enum TaskStatus {
DONE,
PENDING
}
我们可以看到我们已经实现了 Eq 和 Debug 特征,这是assert_eq! 所需要的! 宏。 但是,我们的测试仍然无法运行,因为我们还没有定义使两个 TaskStatus 枚举相等的规则。 我们可以通过简单地将 PartialEq 特征添加到派生注释中来实现 PartialEq 特征。 然而,我们应该探索如何编写我们自己的自定义逻辑。 为了定义相等规则,我们使用以下代码在 PartialEq 特征下实现 eq 函数:
impl PartialEq for TaskStatus {
fn eq(&self, other: &Self) -> bool {
match self {
TaskStatus::DONE => {
match other {
&TaskStatus::DONE => return true,
&TaskStatus::PENDING => false
}
},
TaskStatus::PENDING => {
match other {
&TaskStatus::DONE => return false,
&TaskStatus::PENDING => true
}
}
}
}
}
在这里,我们可以看到我们设法确认 TaskStatus 枚举是否等于使用两个 match 语句进行比较的其他 TaskStatus 枚举。 在 eq 函数中使用 == 运算符似乎更直观; 但是,使用 == 运算符调用 eq 函数会导致无限循环。 如果您在 eq 函数中使用 == 运算符,代码仍然可以编译,但如果运行它,您将收到以下无用的错误:
fatal runtime error: stack overflow
我们现在基本上创建了一个新的基本结构,然后检查字段是否符合我们的预期。 要运行它,请运行货物测试功能,将其指向我们要测试的文件,该文件由以下命令表示:
cargo test to_do::structs::base
我们将得到以下输出:
running 1 test
test to_do::structs::base::base_tests::new ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0
filtered out; finished in 0.00s
我们可以看到我们的测试已运行并且通过了。 现在,我们将继续为模块的其余部分编写测试,即 Done 和 Pending 结构。 现在是时候看看是否可以在 src/to_do/structs/done.rs 文件中编写基本的单元测试了。 如果您尝试在 src/to_do/structs/done.rs 文件中为 Done 结构编写单元测试,您的代码应类似于以下代码:
#[cfg(test)]
mod done_tests {
use super::Done;
use super::TaskStatus;
#[test]
fn new() {
let new_base_struct = Done::new("test title");
assert_eq!(String::from("test title"),
new_base_struct.super_struct.title);
assert_eq!(TaskStatus::DONE,
new_base_struct.super_struct.status);
}
}
我们可以使用以下命令运行这两个测试:
cargo test
这给出了以下输出:
running 2 tests
test to_do::structs::base::base_tests::new ... ok
test to_do::structs::done::done_tests::new ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0
measured; 0 filtered out; finished in 0.00s
运行 Cargo test 会运行所有 Rust 文件的所有测试。 我们可以看到所有测试现在都已运行并通过。
现在我们已经完成了一些基本测试,让我们看看我们可以测试的其他模块。 我们的 JSON 序列化和视图可以在我们的功能测试中使用 Postman 进行测试。 我们的数据库模型没有我们特意定义的任何高级功能。
构建 JWT 单元测试
我们所有的模型所做的就是读取和写入数据库。 这已被证明是有效的。 我们要进行单元测试的唯一模块是 auth 模块。 在这里,我们有一些基于输入产生多种结果的逻辑。 我们还必须进行一些模拟,因为某些函数接受 actix_web 结构,该结构具有某些字段和函数。 对我们来说幸运的是,actix_web 有一个测试模块,使我们能够模拟请求。
构建测试配置
在开始为 JWT 构建单元测试之前,我们必须记住,获取密钥依赖于配置文件。 单元测试必须是隔离的。 他们不需要将正确的参数传递给它们即可工作。 他们每次都应该隔离工作。 因此,我们必须在 src/config.rs 文件中为 Config 结构构建一个新函数。 编码测试的大纲将类似于以下代码:
impl Config {
// existing function reading from file
#[cfg(not(test))]
pub fn new() -> Config {
. . .
}
// new function for testing
#[cfg(test)]
pub fn new() -> Config {
. . .
}
}
从上面的概要可以看出,有两个新功能。 如果正在运行测试,我们的新函数就会被编译,如果服务器正常运行,旧的新函数就会被编译。 我们的测试新函数具有使用以下代码硬编码的标准值:
let mut map = HashMap::new();
map.insert(String::from("DB_URL"),
serde_yaml::from_str(
"postgres://username:password@localhost:5433/
to_do").unwrap());
map.insert(String::from("SECRET_KEY"),
serde_yaml::from_str("secret").unwrap());
map.insert(String::from("EXPIRE_MINUTES"),
serde_yaml::from_str("120").unwrap());
map.insert(String::from("REDIS_URL"),
serde_yaml::from_str("redis://127.0.0.1/")
.unwrap());
return Config {map}
这些默认的功能和我们开发的配置文件是一样的; 然而,我们知道这些变量将是一致的。 运行测试时我们不需要传递任何内容,也不会冒读取另一个文件的风险。 现在我们的测试已经配置完毕,我们可以定义需求,包括 JWT 测试的配置。
定义 JWT 测试的要求
现在我们已经保护了测试的 Config 结构,我们可以转到 src/jwt.rs 文件并使用以下代码定义测试的导入:
#[cfg(test)]
mod jwt_tests {
use std::str::FromStr;
use super::{JwToken, Config};
use actix_web::{HttpRequest, HttpResponse,
test::TestRequest, web, App};
use actix_web::http::header::{HeaderValue,
HeaderName, ContentType};
use actix_web::test::{init_service, call_service};
use actix_web;
use serde_json::json;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct ResponseFromTest {
pub user_id: i32,
pub exp_minutes: i32
}
. . .
}
通过前面的代码,我们可以导入一系列 actix_web 结构体和函数,使我们能够创建伪造的 HTTP 请求并将它们发送到伪造的应用程序,以测试 JwToken 结构体在 HTTP 请求过程中的工作方式。 我们还将定义一个 ResponseFromTest 结构,该结构可以与 JSON 进行处理,以从 HTTP 请求中提取用户 ID,因为 JwToken 结构包含用户 ID。 ResponseFromTest 结构是我们期望得到的 HTTP 响应,因此我们正在密切模拟响应对象。
现在我们已经导入了所需的所有内容,我们可以使用以下代码定义测试的大纲:
#[cfg(test)]
mod jwt_tests {
. . .
#[test]
fn get_key() {
. . .
}
#[test]
fn get_exp() {
. . .
}
#[test]
fn decode_incorrect_token() {
. . .
}
#[test]
fn encode_decode() {
. . .
}
async fn test_handler(token: JwToken,
_: HttpRequest) -> HttpResponse {
. . .
}
#[actix_web::test]
async fn test_no_token_request() {
. . .
}
#[actix_web::test]
async fn test_passing_token_request() {
. . .
}
#[actix_web::test]
async fn test_false_token_request() {
. . .
}
}
这里可以看到我们测试了key的获取以及token的编解码。 它们是 JwToken 结构的本机函数,根据我们之前介绍的内容,您应该能够自己编写它们。 其他函数用#[actix_web::test] 修饰。 这意味着我们将创建虚假的 HTTP 请求来测试 JwToken 如何实现 FromRequest 特征。 现在,没有什么可以阻止我们编写测试,我们将在下一节中介绍这些测试。
为 JWT 构建基本功能测试
我们将从最基本的测试开始,获取密钥,其形式如下:
#[test]
fn get_key() {
assert_eq!(String::from("secret"), JwToken::get_key());
}
我们必须记住,“secret”是在 Config::new 函数中定义的用于测试实现的硬编码密钥。 如果测试 Config::new 函数有效,则上述测试将有效。 获得有效期也很重要。 因为我们直接依赖从配置中提取的过期分钟数,所以以下测试将确保我们返回 120 分钟:
#[test]
fn get_exp() {
let config = Config::new();
let minutes = config.map.get("EXPIRE_MINUTES")
.unwrap().as_i64().unwrap();
assert_eq!(120, minutes);
}
我们现在可以继续通过以下测试来测试如何处理无效令牌:
#[test]
fn decode_incorrect_token() {
let encoded_token: String =
String::from("invalid_token");
match JwToken::from_token(encoded_token) {
Err(message) => assert_eq!("InvalidToken",
message),
_ => panic!(
"Incorrect token should not be able to be
encoded"
)
}
}
在这里,我们传入一个“invalid_token”字符串,该字符串应该会使解码过程失败,因为它显然不是有效的令牌。 然后我们将匹配结果。 如果结果是错误,我们将断言该消息是错误是由无效令牌导致的。 如果除了错误之外还有任何其他输出,那么我们会抛出一个测试失败的错误,因为我们预计解码会失败。
现在我们已经为 JwToken 结构函数编写了两个测试,现在是您尝试编写用于编码和解码令牌的测试的好时机。 如果您尝试编写编码和解码测试,它应该类似于以下代码:
#[test]
fn encode_decode() {
let test_token = JwToken::new(5);
let encoded_token = test_token.encode();
let new_token =
JwToken::from_token(encoded_token).unwrap();
assert_eq!(5, new_token.user_id);
}
前面的测试本质上归结为围绕令牌的登录和经过身份验证的请求过程。 我们使用用户 ID 创建一个新令牌,对令牌进行编码,然后对令牌进行解码测试,看看我们传入令牌的数据是否与解码时得到的数据相同。 如果我们不这样做,那么测试就会失败。
现在我们已经完成了 JwToken 结构体的功能测试,我们可以继续测试 JwToken 结构体如何实现 FromRequest 特征。 在此之前,我们必须定义一个基本视图函数,该函数仅处理 JwToken 的身份验证,然后使用以下代码从令牌返回用户 ID:
async fn test_handler(token: JwToken,
_: HttpRequest) -> HttpResponse {
return HttpResponse::Ok().json(json!({"user_id":
token.user_id,
"exp_minutes":
60}))
}
这并不是什么新鲜事,事实上,这个大纲也是我们在应用程序中定义视图的方式。 定义了基本测试后,我们可以继续构建 Web 请求的测试。
构建 Web 请求测试
我们现在可以使用以下代码测试我们的测试视图,看看它如何处理标头中没有令牌的请求:
#[actix_web::test]
async fn test_no_token_request() {
let app = init_service(App::new().route("/", web::get()
.to(test_handler))).await;
let req = TestRequest::default()
.insert_header(ContentType::plaintext())
.to_request();
let resp = call_service(&app, req).await;
assert_eq!("401", resp.status().as_str());
}
在前面的代码中,我们可以看到我们可以创建一个假服务器并将 test_handler 测试视图附加到它。 然后,我们可以创建一个标头中没有任何令牌的虚假请求。 然后,我们将使用虚假请求调用服务器,然后断言该请求的响应代码未经授权。 我们现在可以使用以下代码创建一个插入有效令牌的测试:
#[actix_web::test]
async fn test_passing_token_request() {
let test_token = JwToken::new(5);
let encoded_token = test_token.encode();
let app = init_service(App::new().route("/", web::get()
.to(test_handler))).await;
let mut req = TestRequest::default()
.insert_header(ContentType::plaintext())
.to_request();
let header_name = HeaderName::from_str("token")
.unwrap();
let header_value = HeaderValue::from_str(encoded_token
.as_str())
.unwrap();
req.headers_mut().insert(header_name, header_value);
let resp: ResponseFromTest = actix_web::test::
call_and_read_body_json(&app, req).await;
assert_eq!(5, resp.user_id);
}
在这里,我们可以看到我们创建了一个有效的令牌。 我们可以创建我们的假服务器并将我们的 test_handler 函数附加到该假服务器。 然后我们将创建一个可以改变的请求。 然后,我们将令牌插入标头,并使用 call_and_read_body_json 函数通过假请求调用假服务器。 需要注意的是,当我们调用call_and_read_body_json函数时,我们声明resp变量名下返回的类型为ResponseFromTest。 然后我们断言用户 ID 来自请求响应。
现在我们已经了解了如何创建带有标头的虚假 HTTP 请求,这是您尝试构建测试的好机会,该测试使用无法解码的虚假令牌发出请求。 如果您尝试过这样做,它应该类似于以下代码:
#[actix_web::test]
async fn test_false_token_request() {
let app = init_service(App::new().route("/", web::get()
.to(test_handler))).await;
let mut req = TestRequest::default()
.insert_header(ContentType::plaintext())
.to_request();
let header_name = HeaderName::from_str("token")
.unwrap();
let header_value = HeaderValue::from_str("test")
.unwrap();
req.headers_mut().insert(header_name, header_value);
let resp = call_service(&app, req).await;
assert_eq!("401", resp.status().as_str());
}
查看以下代码,我们可以看到,我们使用传递令牌请求测试中规定的方法在标头中插入了一个错误令牌,并在测试中使用了未经授权的断言,而没有提供令牌。 如果我们现在运行所有测试,我们应该得到以下打印输出:
running 9 tests
test to_do::structs::base::base_tests::new ... ok
test to_do::structs::done::done_tests::new ... ok
test to_do::structs::pending::pending_tests::new ... ok
test jwt::jwt_tests::get_key ... ok
test jwt::jwt_tests::decode_incorrect_token ... ok
test jwt::jwt_tests::encode_decode ... ok
test jwt::jwt_tests::test_no_token_request ... ok
test jwt::jwt_tests::test_false_token_request ... ok
test jwt::jwt_tests::test_passing_token_request ... ok
test result: ok. 9 passed; 0 failed; 0 ignored;
0 measured; 0 filtered out; finished in 0.00s
从前面的输出来看,我们的 jwt 和 to_do 模块现在已经过全面的单元测试。 考虑到 Rust 仍然是一门新语言,我们已经成功地对我们的代码进行了轻松的单元测试,因为我们以模块化的方式构建了我们的代码。
actix_web 提供的测试箱使我们能够快速轻松地测试边缘情况。 在本节中,我们测试了我们的函数如何处理带有丢失令牌、错误令牌和正确令牌的请求。 我们亲眼目睹了 Rust 如何使我们能够对代码运行单元测试。
一切都配置有货物。 我们不必设置路径、安装额外的模块或配置环境变量。 我们所要做的就是使用 test 属性定义模块并运行 Cargo test 命令。 但是,我们必须记住,我们的视图和 JSON 序列化代码没有经过单元测试。 这是我们切换到 Postman 来测试 API 端点的地方。
在 Postman 中编写测试
在本节中,我们将使用 Postman 实施功能集成测试来测试我们的 API 端点。 这将测试我们的 JSON 处理和数据库访问。 为此,我们将按照以下步骤操作:
我们必须为 Postman 测试创建一个测试用户。 我们可以使用如下所示的 JSON 主体来完成此操作:
{
"name": "maxwell",
"email": "maxwellflitton@gmail.com",
"password": "test"
}
我们需要向 http://127.0.0.1:8000/v1/user/create URL 添加 POST 请求。 完成此操作后,我们可以使用登录端点进行 Postman 测试。 现在我们已经创建了测试用户,我们必须从 POST 请求的响应标头中获取令牌到 http://127.0.0.1:8000/v1/auth/login URL 以及 JSON 请求正文:
{ "username": "maxwell", "password": "test"}
这为我们提供了以下 Postman 布局:
有了这个令牌,我们就拥有了创建 Postman 集合所需的所有信息。 Postman 是 API 请求的集合。 在此集合中,我们可以使用用户令牌作为身份验证将所有待办事项 API 调用集中在一起。 调用结果如下:
- 我们可以使用以下 Postman 按钮创建我们的集合,即 + New Collection:
- 单击此按钮后,我们必须确保为集合定义了用户令牌,因为所有待办事项 API 调用都需要该令牌。 这可以通过使用 API 调用的授权配置来完成,如以下屏幕截图所示:
我们可以看到,我们只是将令牌复制并粘贴到以令牌为键的值中,该值将被插入到请求的标头中。 现在应该将其传递到集合中的所有请求中。 该集合现在存储在左侧导航栏的“集合”选项卡下。
我们现在已经配置了我们的集合,现在可以通过单击此屏幕截图中显示的灰色“添加请求”按钮在集合下添加请求:
现在,我们必须考虑测试测试流程的方法,因为它必须是独立的。
编写测试的有序请求
我们的请求将按以下顺序进行:
创建:创建一个待办事项,然后检查返回是否正确存储。
创建:创建另一个待办事项,检查返回以查看前一个待办事项是否已存储以及该流程是否可以处理两项。
创建:创建另一个与其他项目具有相同标题的待办事项,检查响应以确保我们的应用程序不会存储具有相同标题的重复待办事项。
编辑:编辑项目,检查响应以查看编辑的项目是否已更改为完成以及是否存储在正确的列表中。
编辑:编辑第二项,查看编辑效果是否永久,以及完成列表是否支持这两项。
编辑:编辑应用程序中不存在的项目,以查看应用程序是否正确处理此问题。
删除:删除一项待办事项,查看响应是否不再返回已删除的待办事项,即不再存储在数据库中。
删除:删除最后一个待办事项,检查响应是否没有剩余项目,表明删除操作是永久性的。
我们需要运行前面的测试才能使它们正常工作,因为它们依赖于前面的操作是否正确。 当我们创建集合的请求时,我们必须清楚这个请求在做什么,在哪一步,以及它是什么类型的请求。 例如,创建我们的第一个创建测试将如下所示:
正如我们所看到的,该步骤通过下划线附加了类型。 然后,我们将列表中的测试描述放入请求描述(可选)字段中。 在定义请求时,您可能会发现 API 密钥不在请求的标头中。
这是因为它位于请求的隐藏自动生成标头中。 我们的第一个请求必须是带有 http://127.0.0.1:8000/v1/item/create/washing URL 的 POST 请求。
这将创建待办事项清洗。 但是,在单击“发送”按钮之前,我们必须移至 Postman 请求中的“测试”选项卡(位于“设置”选项卡左侧),以编写测试,如以下屏幕截图所示:
我们的测试必须用 JavaScript 编写。 但是,我们可以通过在测试脚本中输入 pm 来访问 Postman 的测试库。 首先,在测试脚本的顶部,我们需要处理请求,这是通过以下代码完成的:
var result = pm.response.json()
通过前面的行,我们可以在整个测试脚本中访问响应 JSON。 为了全面测试我们的请求,我们需要执行以下步骤:
首先,我们需要检查回复的基本内容。 我们的第一个测试是检查响应是否为 200。这可以通过以下代码完成:
pm.test("response is ok", function () {
pm.response.to.have.status(200);
});
在这里,我们定义测试描述。 然后,定义测试运行的函数。
然后,我们检查响应中数据的长度。 在前面的测试之后,我们将通过以下代码定义测试来检查待处理项的长度是否为 1:
pm.test("returns one pending item", function(){
if (result["pending_items"].length !== 1){
throw new Error(
"returns the wrong number of pending items");
}
})
在前面的代码中,我们对长度进行了简单的检查,如果长度不为 1,则抛出错误,因为我们只期望挂起的项目列表中有一个挂起的项目。
然后,我们在以下代码中检查待处理项目的标题和状态:
pm.test("Pending item has the correct title", function(){
if (result["pending_items"][0]["title"] !==
"washing"){
throw new Error(
"title of the pending item is not 'washing'");
}
})
pm.test("Pending item has the correct status",
function()
{
if (result["pending_items"][0]["status"] !==
"PENDING"){
throw new Error(
"status of the pending item is not
'pending'");
}
})
在前面的代码中,如果状态或标题与我们想要的不匹配,我们会抛出错误。 现在我们已经满足了对待处理项目的测试,我们可以继续对已完成项目进行测试。
鉴于我们完成的项目应该为零,测试具有以下定义:
pm.test("returns zero done items", function(){
if (result["done_items"].length !== 0){
throw new Error(
"returns the wrong number of done items");
}
})
在前面的代码中,我们只是确保 did_items 数组的长度为零。
现在,我们必须检查已完成和待处理项目的计数。 这是通过以下代码完成的:
pm.test("checking pending item count", function(){
if (result["pending_item_count"] !== 1){
throw new Error(
"pending_item_count needs to be one");
}
})
pm.test("checking done item count", function(){
if (result["done_item_count"] !== 0){
throw new Error(
"done_item_count needs to be zero");
}
})
现在我们的测试已经构建完成,我们可以通过单击 Postman 中的 SEND 按钮来发出请求,以获得以下测试输出:
Figure 9.8 – Postman tests output
我们可以看到我们的测试描述和测试状态被突出显示。 如果出现错误,状态将为红色并显示“失败”。 现在我们的第一个创建测试已经完成,我们可以创建第二个创建测试。
为 HTTP 请求创建测试
然后我们可以使用以下 URL 创建 2_create 测试:http://127.0.0.1:8000/v1/item/create/cooking。 这是一个很好的机会,可以尝试使用我们在上一步中探索的测试方法自行构建测试。 如果您尝试构建测试,它们应该类似于以下代码:
var result = pm.response.json()
pm.test("response is ok", function () {
pm.response.to.have.status(200);
});
pm.test("returns two pending item", function(){
if (result["pending_items"].length !== 2){
throw new Error(
"returns the wrong number of pending items");
}
})
pm.test("Pending item has the correct title", function(){
if (result["pending_items"][0]["title"] !== "washing"){
throw new Error(
"title of the pending item is not 'washing'");
}
})
pm.test("Pending item has the correct status", function(){
if (result["pending_items"][0]["status"] !==
"PENDING"){
throw new Error(
"status of the pending item is not 'pending'");
}
})
pm.test("Pending item has the correct title", function(){
if (result["pending_items"][1]["title"] !== "cooking"){
throw new Error(
"title of the pending item is not 'cooking'");
}
})
pm.test("Pending item has the correct status", function(){
if (result["pending_items"][1]["status"] !==
"PENDING"){
throw new Error(
"status of the pending item is not 'pending'");
}
})
pm.test("returns zero done items", function(){
if (result["done_items"].length !== 0){
throw new Error(
"returns the wrong number of done items");
}
})
pm.test("checking pending item count", function(){
if (result["pending_item_count"].length === 1){
throw new Error(
"pending_item_count needs to be one");
}
})
pm.test("checking done item count", function(){
if (result["done_item_count"].length === 0){
throw new Error(
"done_item_count needs to be zero");
}
})
我们可以看到我们在第二个待处理项目上添加了一些额外的测试。 前面的测试也直接适用于 3_create 测试,因为重复创建将与我们使用与 2_create 相同的 URL 相同。
前面的测试需要在这些测试中进行大量重复,稍微改变数组的长度、项目计数和这些数组中的属性。 这是练习基本 Postman 测试的好机会。 如果您需要将您的测试与我的测试交叉引用,您可以在以下 URL 的 JSON 文件中评估它们:https://github.com/PacktPublishing/Rust-Web-Programming-2nd-Edition/blob/main/chapter09 /building_test_pipeline/web_app/scripts/to_do_items.postman_collection.json。
在本节中,我们为 Postman 添加了一系列步骤来测试何时进行 API 调用。 这不仅对我们的应用程序有用。 Postman 可以访问它可以访问的互联网上的任何 API。 因此,您可以使用Postman测试来监控实时服务器和第三方API。
现在,如果每次都必须手动完成,运行所有这些测试可能会很困难。 我们可以使用 Newman 自动运行和检查该集合中的所有测试。 如果我们自动化这些收集,我们就可以在每天的特定时间在我们依赖的实时服务器和第三方 API 上运行测试,当我们的服务器或第三方 API 发生故障时向我们发出警报。
纽曼将为我们在这一领域的进一步发展奠定良好的基础。 在下一节中,我们将导出集合并使用 Newman 依次运行导出集合中的所有 API 测试。
使用 Newman 自动化 Postman 测试
为了自动化这一系列测试,在本节中,我们将以正确的顺序导出待办事项 Postman 集合。 但首先,我们必须将集合导出为 JSON 文件。 这可以通过单击左侧导航栏上的 Postman 中的集合并单击灰色的“导出”按钮来完成,如以下屏幕截图所示:
现在我们已经导出了集合,我们可以快速检查它以查看文件的结构。 以下代码定义了测试套件的标头:
"info": {
"_postman_id": "bab28260-c096-49b9-81e6-b56fc5f60e9d",
"name": "to_do_items",
"schema": "https://schema.getpostman.com
/json/collection/v2.1.0/collection.json",
"_exporter_id": "3356974"
},
前面的代码告诉 Postman 运行测试需要什么模式。 如果将代码导入 Postman,则 ID 和名称将可见。 然后该文件继续通过如下给出的代码定义各个测试:
"item": [
{
"name": "1_create",
"event": [
{
"listen": "test",
"script": {
"exec": [
"var result = pm.response.json()",
. . .
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "token",
"value": "eyJhbGciOiJIUzI1NiJ9
.eyJ1c2VyX2lkIjo2fQ.
uVo7u877IT2GEMpB_gxVtxhMAYAJD8
W_XiUoNvR7_iM",
"type": "text",
"disabled": true
}
],
"url": {
"raw": "http://127.0.0.1:8000/
v1/item/create/washing",
"protocol": "http",
"host": ["127", "0", "0", "1"],
"port": "8000",
"path": ["v1", "item", "create", "washing"]
},
"description": "create a to-do item,
and then check the
return to see if it is stored correctly "
},
"response": []
},
从前面的代码中,我们可以看到我们的测试、方法、URL、标头等都定义在一个数组中。 快速检查项目数组将显示测试将按照我们想要的顺序执行。
现在,我们可以简单地用 Newman 运行它。 我们可以使用以下命令安装 Newman:
npm install -g newman
笔记
必须注意的是,上述命令是全局安装,有时可能会出现问题。 为了避免这种情况,您可以设置一个包含以下内容的 package.json 文件:
{
"name": "newman testing",
"description": "",
"version": "0.1.0",
"scripts": {
"test": "newman run to_do_items.
postman_collection.json"
},
"dependencies": {
"newman": "5.3.2"
}
}
通过这个 package.json,我们定义了测试命令和 Newman 依赖项。 我们可以使用以下命令在本地安装依赖项:
npm install
然后,这将在 node_modules 目录下安装我们需要的所有内容。 我们可以使用 package.json 中定义的测试命令,而不是直接运行 Newman 测试命令,命令如下:
npm run test
现在我们已经安装了 Newman,我们可以使用以下命令针对导出的集合 JSON 文件运行测试集合:
newman run to_do_items.postman_collection.json
前面的命令运行所有测试并为我们提供状态报告。 每个描述都会被打印出来,并且状态也会由测试的侧面指示。 以下是正在评估的 API 测试的典型打印输出:
→ 1_create
POST http://127.0.0.1:8000/v1/item/create/washing
[200 OK, 226B, 115ms]
? response is ok
? returns one pending item
? Pending item has the correct title
? Pending item has the correct status
? returns zero done items
? checking pending item count
? checking done item count
前面的输出为我们提供了名称、方法、URL 和响应。 到了这里,所有人都过去了。 如果没有,那么测试描述将显示一个十字而不是勾号。 我们还得到以下总结:
我们可以看到我们所有的测试都通过了。 这样,我们就成功实现了功能测试的自动化,使我们能够以最小的努力测试完整的工作流程。 然而,我们所做的事情是不可维护的。 例如,我们的令牌将过期,这意味着如果我们在本月晚些时候运行测试,它们将会失败。 在下一节中,我们将构建一个完整的自动化管道,用于构建我们的服务器、更新我们的令牌并运行我们的测试。
构建完整的自动化测试管道
当涉及到开发和测试时,我们需要一个可以轻松拆除和重建的环境。 没有什么比在本地计算机上的数据库中构建数据以便能够使用该数据开发更多功能更糟糕的了。 但是,数据库容器可能会被意外删除,或者您可能编写一些损坏数据的代码。 然后,您必须花费大量时间重新创建数据,然后才能返回到原来的位置。 如果系统很复杂并且缺少文档,您可能会忘记重新创建数据所需的步骤。 如果您不愿意在开发和测试时破坏本地数据库并重新开始,那么肯定是出了问题,您被抓到只是时间问题。 在本节中,我们将创建一个执行以下操作的 Bash 脚本:
在后台启动数据库 Docker 容器。
编译 Rust 服务器。
运行单元测试。
开始运行 Rust 服务器。
运行对 Docker 中运行的数据库的迁移。
发出 HTTP 请求来创建用户。
发出 HTTP 请求以登录并获取令牌。
使用登录中的令牌更新 Newman JSON 文件。
运行纽曼测试。
删除整个过程中生成的文件。
停止 Rust 服务器运行。
停止并销毁整个进程中正在运行的 Docker 容器。
前面的列表列出了很多步骤。 浏览此列表,将我们将要探索的代码块分解为步骤似乎很直观; 然而,我们将在一个 Bash 脚本中运行几乎所有步骤。 上述许多步骤都可以通过一行 Bash 代码来实现。 将代码分解为步骤就太过分了。 现在我们已经完成了所需的所有步骤,我们可以设置测试基础设施了。 首先,我们需要在 web_app 根目录中的 src 目录旁边设置一个 script 目录。 在脚本目录中,我们需要一个 run_test_pipeline.sh 脚本来运行主要测试过程。 我们还需要将 Newman JSON 配置文件放入脚本目录中。
我们将使用bash来编排整个测试管道,这是编排测试任务的最佳工具。 在我们的 srcipts/run_test_pipeline.sh 脚本中,我们将从以下代码开始:
#!/bin/bash
# move to directory of the project
SCRIPTPATH="$( cd "$(dirname "$0")" ; pwd -P )"
cd $SCRIPTPATH
cd ..
在前面的代码中,我们告诉计算机该代码块是带有 #!/bin/bash shebang 行的 Bash 脚本。 Bash 脚本从调用 Bash 脚本的当前工作目录运行。 我们可以从多个目录调用脚本,因此我们需要确保获得脚本所在的目录,即脚本目录,将其分配给名为 SCRIPTPATH 的变量,移动到该目录,然后使用 cd.. 命令位于 Docker、config 和 Cargo 文件所在的主目录中。 然后,我们可以使用 -d 标志在后台启动 Docker 容器并循环,直到数据库接受以下代码的连接:
# spin up docker and hold script until accepting connections
docker-compose up -d
until pg_isready -h localhost -p 5433 -U username
do
echo "Waiting for postgres"
sleep 2;
done
现在我们的 Docker 容器正在运行,我们现在可以继续构建 Rust 服务器。 首先,我们可以编译 Rust 服务器并使用以下代码运行单元测试:
cargo build
cargo test
运行单元测试后,我们可以使用以下代码在后台运行我们的服务器:
# run server in background
cargo run config.yml &
SERVER_PID=$!
sleep 5
在命令末尾添加 & 后,cargo run config.yml 将在后台运行。 然后我们获取Cargo run config.yml命令的进程ID并将其分配给变量SERVER_PID。 然后我们休眠 5 秒钟,以确保服务器已准备好接受连接。 在对服务器进行任何 API 调用之前,我们必须使用以下代码运行对数据库的迁移:
diesel migration run
然后我们回到脚本目录并对服务器进行 API 调用以创建用户:
# create the user
curl --location --request POST 'http://localhost:8000/v1/user/create' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "maxwell",
"email": "maxwellflitton@gmail.com",
"password": "test"
}'
如果您想知道如何使用curl 在 Bash 中发出 HTTP 请求,您可以使用 Postman 工具自动生成它们。 在Postman工具的右侧,您可以看到一个Code按钮,如下图所示:
单击代码标签后,会出现一个下拉菜单,您可以在其中从多种语言中进行选择。 选择所需语言后,您的 API 调用将显示在所选语言的代码片段中,您可以复制并粘贴该代码片段。
现在我们已经创建了用户,我们可以使用以下代码登录并将令牌存储在 fresh_token.json 文件中; 不过需要注意的是,首先需要安装curl:
# login getting a fresh token
echo $(curl --location --request GET 'http://localhost:8000/v1/auth/login' \
--header 'Content-Type: application/json' \
--data-raw '{
"username": "maxwell",
"password": "test"
}') > ./fresh_token.json
这里发生的是,我们可以使用 $(...) 将 API 调用的结果包装到变量中。 然后,我们回显此内容并使用 echo $(...) > ./fresh_token.json 将其写入文件。 然后,我们可以将新令牌插入 Newman 数据中,并使用以下代码运行 Newman API 测试:
TOKEN=$(jq '.token' fresh_token.json)
jq '.auth.apikey[0].value = '"$TOKEN"''
to_do_items.postman_collection.json > test_newman.json
newman run test_newman.json
我们的测试现已完成。 我们可以使用以下代码清理运行测试时创建的文件、销毁 Docker 容器并停止服务器运行:
rm ./test_newman.json
rm ./fresh_token.json
# shut down rust server
kill $SERVER_PID
cd ..
docker-compose down
笔记
在我们运行 Bash 脚本之前,需要安装curl 和 jq。 如果您使用的是 Linux,则可能需要运行以下命令:
sudo chmod +x ./run_test_pipeline.sh
然后我们可以使用以下命令运行测试脚本:
sh run_test_pipeline.sh
显示整个打印输出只会不必要地填满这本书。 但是,我们可以在下面的屏幕截图中看到测试打印输出的结尾:
在这里,打印输出清楚地表明newmain测试已经运行并通过。 测试完成后,服务器被关闭,支持服务器的 Docker 容器被停止和删除。 如果你想将此日志写入txt文件,可以使用以下命令:
sh run_test_pipeline.sh > full_log.txt
你有它! 一个完全工作的测试管道,可以自动设置、测试和清理我们的服务器。 因为我们已经在简单的 Bash 测试管道中编写了它,所以我们可以将这些步骤集成到自动化管道中,例如 Travis、Jenkins 或 GitHub Actions。 当执行拉取请求和合并时,这些管道工具会自动触发。
概括
在本章中,我们详细介绍了应用程序的工作流程和组件,并对它们进行了分解,以便我们可以为正确的部分选择正确的工具。 我们使用单元测试,这样我们就可以快速检查几个边缘情况,以了解每个函数和结构如何与其他函数和结构交互。
我们还通过单元测试直接检查我们的自定义结构。 然后,我们使用 actix_web 测试结构来模拟请求,以查看使用该结构并处理请求的函数如何工作。 然而,当我们来到主 API 视图模块时,我们切换到 Postman。
这是因为我们的 API 端点很简单。 他们创建、编辑和删除待办事项。 我们可以通过调用 API 并检查响应来直接评估此过程。 我们设法开箱即用地评估接受和返回数据的 JSON 处理。 我们还能够通过这些 Postman 测试来评估数据库中数据的查询、写入和更新。
Postman 使我们能够快速有效地测试一系列流程。 我们甚至通过 Newman 实现自动化,从而加快了测试过程。 但必须指出的是,这种方法并不是一刀切的方法。 如果 API 视图功能变得更加复杂,具有更多移动部件,例如与另一个 API 或服务通信,那么就必须重新设计 Newman 方法。 必须考虑触发模拟此类过程的环境变量,以便我们可以快速测试一系列边缘情况。
如果系统随着结构的依赖关系的增长而增长,则需要模拟对象。 这是我们创建假结构或函数并定义测试输出的地方。 为此,我们需要一个外部包,例如mockall。 本章的进一步阅读部分介绍了有关此包的文档。
我们的应用程序现已完全运行并进行了一系列测试。 现在,我们剩下的就是在服务器上部署我们的应用程序。
在下一章中,我们将在 Amazon Web Services (AWS) 上设置服务器,利用 Docker 在服务器上部署我们的应用程序。 我们将介绍设置 AWS 配置、运行测试以及如果测试通过则在服务器上部署应用程序的过程。
问题
如果我们可以手动使用应用程序,为什么我们还要费心进行单元测试呢?
单元测试和功能测试有什么区别?
单元测试的优点是什么?
单元测试的缺点是什么?
功能测试有哪些优点?
功能测试有哪些缺点?
构建单元测试的合理方法是什么?
答案
当涉及到手动测试时,您可能会忘记运行某个程序。 运行测试标准化了我们的标准,并使我们能够将它们集成到持续集成工具中,以确保新代码不会破坏服务器,因为如果代码失败,持续集成可能会阻止新代码合并。
单元测试隔离各个组件,例如函数和结构。 然后使用一系列虚假输入来评估这些函数和结构,以评估组件如何与不同输入交互。 功能测试评估系统、访问 API 端点并检查响应。
单元测试是轻量级的,不需要运行整个系统。 他们可以快速测试一整套边缘情况。 单元测试还可以准确地隔离错误所在。
单元测试本质上是带有虚构输入的隔离测试。 如果系统中的输入类型发生了更改,但在单元测试中未更新,则该测试在应该失败时基本上会通过。 单元测试也不评估系统的运行方式。
功能测试可确保整个基础设施按其应有的方式协同工作。 例如,我们如何配置和连接数据库可能存在问题。 通过单元测试,可以忽略此类问题。 此外,尽管模拟可以确保隔离测试,但单元测试模拟可能已经过时。 这意味着模拟函数可能会返回更新版本不会返回的数据。 因此,单元测试将通过,但功能测试不会通过,因为它们测试了所有内容。
功能测试需要有像数据库一样运行的基础设施。 还必须有设置和拆卸功能。 例如,功能测试将影响数据库中存储的数据。 测试结束时,需要擦除数据库,然后才能再次运行测试。 这会增加复杂性,并且可能需要在不同操作之间使用“粘合”代码。
我们从测试没有任何依赖关系的结构和函数开始。 一旦这些经过测试,我们就知道我们对它们感到满意。 然后,我们继续讨论具有之前测试过的依赖项的函数和结构。 使用这种方法,我们知道我们正在编写的当前测试不会由于依赖关系而失败。
相关推荐
- 第7章 Linux磁盘管理—磁盘格式化和挂载
-
提醒:本文为合集文章,后续会持续更新!关注我,每日提升!7.3 格式化磁盘分区磁盘分区虽然分好区了,但暂时还不能用,我们还须对每一个分区进行格式化。所谓格式化,其实就是安装文件系统,Windows下的...
- Linux三剑客之sed命令详解,小白也能看得懂!
-
sed全称为StreamEDitor,行编辑器,同时也是一种流编辑器,它一次处理一行内容。处理时,把当前处理的行存储在临时缓冲区中,称为“模式空间”(patternspace),接着用sed命令处...
- Rust语言介绍,新崛起的编程语言
-
Rust是一门系统编程语言,由于其独特的特点和性能,近年来备受开发者关注,是近几年发展最迅猛的编程语言之一。据StackOverflow开发者调查显示,Rust连续第八年被评为最受喜爱的编程语言,...
- What does " 2>&1 " mean?
-
技术背景在Linux或Unix系统中,程序通常会将输出发送到两个地方:标准输出(stdout)和标准错误(stderr)。标准输出用于正常的程序输出,而标准错误则用于输出程序运行过程中产生的错误信息。...
- 玩转命令行:7 个高效 Linux 命令技巧,助你事半功倍!
-
日常的运维、开发、测试过程中,Linux命令行无疑是我们最常接触的界面之一。掌握一些不为人知但极具实用价值的命令技巧,不仅能大幅提升你的工作效率,更能在关键时刻帮你快速定位问题、批量处理任务、自动化...
- 作为测试人,如何优雅地查看Log日志?
-
作为一名测试工程师,测试工作中和Linux打交道的地方有很多。比如查看日志、定位Bug、修改文件、部署环境等。项目部署在Linux上,如果某个功能发生错误,就需要我们去排查出错的原因,所以熟练地掌握查...
- Linux新手必备:20个高效命令轻松掌握!
-
Linux基本命令使用指南在现代计算机操作系统中,Linux因其开放性、灵活性和强大的功能,广泛应用于服务器和开发环境中。作为技术人员,掌握Linux的基本命令是非常重要的。在本文中,我们将重点介绍2...
- 如何在 Linux 中有效使用 history 命令?
-
在Linux中,每当你在终端输入一条命令并按下回车,这条命令就会被默默记录下来。而history命令的作用,就是让你回顾这些操作的足迹。简单来说,它是一个“命令行日记本”,默认存储在用户主目录...
- Linux/Unix 系统中find命令用法
-
find是Linux/Unix系统中一个非常强大且灵活的命令,用于在目录层次结构中查找文件和目录。它允许你根据各种条件(如名称、类型、大小、权限、修改时间等)来搜索,并对找到的结果执行操作。基本...
- 阿里云国际站:如何通过日志分析排查故障?
-
本文由【云老大】TG@yunlaoda360撰写一、日志收集确定日志位置:应用程序日志:通常位于/var/log/或应用程序的安装目录下,例如Nginx的日志位于/var/log/ngi...
- Linux History命令:如何显示命令执行的日期和时间
-
在Linux系统中,history命令是一个简单却强大的工具,它允许用户查看和重用之前执行过的命令。然而,默认情况下,history命令的输出仅显示命令的序号和内容,并不包含命令执行的日期和时间。这对...
- 在R语言中使用正则表达式
-
有时候我们要处理的是非结构化的数据,例如网页或是电邮资料,那么就需要用R来抓取所需的字符串,整理为进一步处理的数据形式。R语言中有一整套可以用来处理字符的函数,在之前的博文中已经有所涉及。但真正的...
- 网络安全实战:记一次比较完整的靶机渗透
-
0x01信息搜集nmap-sC-sV-p--A10.10.10.123-T4-oAnmap_friendzone访问80端口的http服务只发现了一个域名。0x02DNS区域传输因...
- Java程序员必备的Linux命令
-
Java程序员必备的Linux命令作为一名Java开发者,在日常工作中难免会与Linux服务器打交道。熟练掌握一些常用的Linux命令,不仅能提高工作效率,还能让你在团队中显得更加专业。今天,我将带你...
- linux shell 笔记——1
-
shell的格式开头#!/bin/bash或者#!/bin/sh开头系统变量:HOME、HOME、HOME、PWD、SHELL、SHELL、SHELL、USER,PATH等等比方:echo$...
- 一周热门
-
-
Python实现人事自动打卡,再也不会被批评
-
Psutil + Flask + Pyecharts + Bootstrap 开发动态可视化系统监控
-
一个解决支持HTML/CSS/JS网页转PDF(高质量)的终极解决方案
-
【验证码逆向专栏】vaptcha 手势验证码逆向分析
-
再见Swagger UI 国人开源了一款超好用的 API 文档生成框架,真香
-
网页转成pdf文件的经验分享 网页转成pdf文件的经验分享怎么弄
-
C++ std::vector 简介
-
python使用fitz模块提取pdf中的图片
-
《人人译客》如何规划你的移动电商网站(2)
-
Jupyterhub安装教程 jupyter怎么安装包
-
- 最近发表
- 标签列表
-
- python判断字典是否为空 (50)
- crontab每周一执行 (48)
- aes和des区别 (43)
- bash脚本和shell脚本的区别 (35)
- canvas库 (33)
- dataframe筛选满足条件的行 (35)
- gitlab日志 (33)
- lua xpcall (36)
- blob转json (33)
- python判断是否在列表中 (34)
- python html转pdf (36)
- 安装指定版本npm (37)
- idea搜索jar包内容 (33)
- css鼠标悬停出现隐藏的文字 (34)
- linux nacos启动命令 (33)
- gitlab 日志 (36)
- adb pull (37)
- table.render (33)
- uniapp textarea (33)
- python判断元素在不在列表里 (34)
- python 字典删除元素 (34)
- react-admin (33)
- vscode切换git分支 (35)
- python bytes转16进制 (35)
- grep前后几行 (34)