百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Rust Web编程:第九章 测试我们的应用程序端点和组件

mhr18 2025-05-15 19:09 3 浏览 0 评论

我们的待办事项 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 调用集中在一起。 调用结果如下:



  1. 我们可以使用以下 Postman 按钮创建我们的集合,即 + New Collection:



  1. 单击此按钮后,我们必须确保为集合定义了用户令牌,因为所有待办事项 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 端点并检查响应。

单元测试是轻量级的,不需要运行整个系统。 他们可以快速测试一整套边缘情况。 单元测试还可以准确地隔离错误所在。

单元测试本质上是带有虚构输入的隔离测试。 如果系统中的输入类型发生了更改,但在单元测试中未更新,则该测试在应该失败时基本上会通过。 单元测试也不评估系统的运行方式。

功能测试可确保整个基础设施按其应有的方式协同工作。 例如,我们如何配置和连接数据库可能存在问题。 通过单元测试,可以忽略此类问题。 此外,尽管模拟可以确保隔离测试,但单元测试模拟可能已经过时。 这意味着模拟函数可能会返回更新版本不会返回的数据。 因此,单元测试将通过,但功能测试不会通过,因为它们测试了所有内容。

功能测试需要有像数据库一样运行的基础设施。 还必须有设置和拆卸功能。 例如,功能测试将影响数据库中存储的数据。 测试结束时,需要擦除数据库,然后才能再次运行测试。 这会增加复杂性,并且可能需要在不同操作之间使用“粘合”代码。

我们从测试没有任何依赖关系的结构和函数开始。 一旦这些经过测试,我们就知道我们对它们感到满意。 然后,我们继续讨论具有之前测试过的依赖项的函数和结构。 使用这种方法,我们知道我们正在编写的当前测试不会由于依赖关系而失败。

相关推荐

京东大佬问我,每天新增100w订单数据的分库分表方案

京东大佬问我,每天新增100w订单数据的分库分表方案嗯,用户问的是高并发订单系统的分库分表方案,每天新增100万订单。首先,我得理解需求。每天100万订单,那每秒大概是多少呢?算一下,100万除以86...

MySQL 内存使用构成解析与优化实践

在为HULK平台的MySQL提供运维服务过程中,我们常常接到用户反馈:“MySQL内存使用率过高”。尤其在业务高峰期,监控中内存占用持续增长,即便数据库运行正常,仍让人怀疑是否存在异常,甚至...

阿里云国际站:怎样计算内存优化型需求?

本文由【云老大】TG@yunlaoda360撰写一、内存优化型实例的核心价值内存优化型ECS实例专为数据密集型场景设计,具有以下核心优势:高内存配比:内存与CPU比例可达1:8(如ecs.re6....

MySQL大数据量处理常用解决方案

1、读写分离读写分离,将数据库的读写操作分开,比如让性能比较好的服务器去做写操作,性能一般的服务器做读操作。写入或更新操作频繁可以借助MQ,进行顺序写入或更新。2、分库分表分库分表是最常规有效的一种大...

1024程序员节 花了三个小时调试 集合近50种常用小工具 开源项目

开篇1024是程序员节了,本来我说看个开源项目花半个小时调试之前看的一个不错的开源项目,一个日常开发常常使用的工具集,结果花了我三个小时,开源作者的开源项目中缺少一些文件,我一个个在网上找的,好多坑...

免费全开源,功能强大的多连接数据库管理工具!-DbGate

DBGate是一个强大且易于使用的开源数据库管理工具,它提供了一个统一的Web界面,让你能够轻松地访问和管理多种类型的数据库。无论你是开发者、数据分析师还是DBA,DBGate都能帮助你提升工作效率...

10个最佳的开源免费的酒店系统,接私活创业拿来改改
  • 10个最佳的开源免费的酒店系统,接私活创业拿来改改
  • 10个最佳的开源免费的酒店系统,接私活创业拿来改改
  • 10个最佳的开源免费的酒店系统,接私活创业拿来改改
  • 10个最佳的开源免费的酒店系统,接私活创业拿来改改
使用operator部署Prometheus

一、介绍Operator是CoreOS公司开发,用于扩展kubernetesAPI或特定应用程序的控制器,它用来创建、配置、管理复杂的有状态应用,例如数据库,监控系统。其中Prometheus-Op...

java学习总结

SpringBoot简介https://spring.io/guideshttp://www.spring4all.com/article/246http://www.spring4all.com/a...

Swoole难上手?从EasySwoole开始

前言有些童鞋感觉对Swoole不从下手,也不知在什么业务上使用它,看它这么火却学不会也是挺让人捉急的一件事情。Swoole:面向生产环境的PHP异步网络通信引擎啥是异步网络通信?10年架构师领你架...

一款商用品质的开源商城系统(Yii2+Vue2.0+uniapp)

一、项目简介这是一套很成熟的开源商城系统【开店星】,之前推过一次,后台感兴趣的还不少,今天再来详细介绍一下:基于Yii2+Vue2.0+uniapp框架研发,代码质量堪称商用品质,下载安装无门槛,UI...

Yii2中对Composer的使用

如何理解Composer?若使用Composer我们应该先知道这是一个什么东西,主要干什么用的,我们可以把Composer理解为PHP包的管理工具,管理我们用到的Yii2相关的插件。安装Compose...

SpringBoot实现OA自动化办公管理系统源码+代码讲解+开发文档

今天发布的是由【猿来入此】的优秀学员独立做的一个基于springboot脚手架的自动化OA办公管理系统,主要实现了日常办公的考勤签到等一些办公基本操作流程的全部功能,系统分普通员工、部门经理、管理员等...

7层架构解密:从UI到基础设施,打造真正可扩展的系统

"我们系统用户量暴增后完全崩溃了!"这是多少工程师的噩梦?选择正确的数据库只是冰山一角,真正的系统扩展性是一场全栈战役。客户端层:用户体验的第一道防线当用户点击你的应用时,0.1秒...

Win11系统下使用Django+Celery异步任务队列以及定时(周期)任务

首先明确一点,celery4.1+的官方文档已经详细说明,该版本之后不需要引入依赖django-celery这个库了,直接用celery本身就可以了,就在去年年初的一篇文章python3.7....

取消回复欢迎 发表评论: