HTTP 请求测试:开发者的生存工具

已发表: 2022-03-11

当测试套件不可行时该怎么办

有时我们——程序员和/或我们的客户——只有有限的资源来编写预期的可交付成果和该可交付成果的自动化测试。 当应用程序足够小时,您可以偷工减料并跳过测试,因为您(大部分)记得当您添加功能、修复错误或重构时代码中其他地方发生了什么。 也就是说,我们不会总是使用小型应用程序,此外,随着时间的推移,它们往往会变得更大、更复杂。 这使得手动测试变得困难并且超级烦人。

在我最近的几个项目中,我被迫在没有自动化测试的情况下工作,老实说,在代码推送后让客户给我发电子邮件说应用程序在我什至没有接触过代码的地方出现问题,这很尴尬。

因此,如果我的客户既没有预算也没有打算添加任何自动化测试框架,我开始测试整个网站的基本功能,方法是向每个单独的页面发送 HTTP 请求,解析响应标头并查找“200”回复。 这听起来简单明了,但是您可以做很多事情来确保保真度,而无需实际编写任何测试、单元、功能或集成。

自动化测试

在 Web 开发中,自动化测试包括三种主要的测试类型:单元测试、功能测试和集成测试。 我们经常将单元测试与功能和集成测试结合起来,以确保一切作为一个整体应用程序顺利运行。 当这些测试一致或按顺序运行时(最好使用单个命令或单击),我们开始称它们为自动化测试,无论是否为单元。

这些测试(至少在 web 开发中)的主要目的是确保所有应用程序页面都可以顺利呈现,没有致命(应用程序停止)错误或错误。

单元测试

单元测试是一个软件开发过程,其中代码的最小部分 - 单元 - 被独立测试以确保正确运行。 这是 Ruby 中的一个示例:

 test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end

功能测试

功能测试是一种用于检查系统或软件的特性和功能的技术,旨在涵盖所有用户交互场景,包括故障路径和边界情况。

注意:我们所有的例子都是用 Ruby 编写的。

 test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end

集成测试

一旦模块经过单元测试,它们就会被逐一集成,以检查组合行为,并验证需求是否正确实现。

 test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end

理想世界中的测试

测试在行业中被广泛接受并且是有意义的; 好的测试可以让你:

  • 质量保证您的整个应用程序以最少的人力投入
  • 更容易识别错误,因为您确切地知道您的代码在哪里因测试失败而中断
  • 为您的代码创建自动文档
  • 避免“编码便秘”,根据 Stack Overflow 上的一些家伙的说法,这是一种幽默的说法,“当你不知道接下来要写什么,或者你面前有一项艰巨的任务时,从小写开始。”

我可以继续谈论测试有多棒,以及它们如何改变世界和 yada yada yada,但你明白了。 从概念上讲,测试很棒。

相关:单元测试、如何编写可测试代码及其重要性

现实世界中的测试

虽然这三种类型的测试都有优点,但大多数项目都没有编写它们。 为什么? 好吧,让我分解一下:

时间/截止日期

每个人都有最后期限,编写新的测试可能会妨碍完成一个。 编写应用程序及其各自的测试可能需要时间半(或更多)时间。 现在,你们中的一些人不同意这一点,理由是最终节省了时间,但我认为情况并非如此,我将在“意见分歧”中解释原因。

客户问题

通常,客户并不真正了解测试是什么,或者为什么它对应用程序有价值。 客户往往更关心产品的快速交付,因此认为程序化测试会适得其反。

或者,它可能就像客户没有预算来支付实施这些测试所需的额外时间一样简单。

缺少知识

现实世界中有相当大的开发人员部落不知道测试的存在。 在每次会议、聚会、音乐会上(甚至在我的梦中),我都会遇到不知道如何编写测试、不知道要测试什么、不知道如何设置测试框架等的开发人员在。 学校并不完全教授测试,设置/学习框架以使其运行可能很麻烦。 所以是的,有一个明确的进入障碍。

'这是很多工作'

对于新手和有经验的程序员来说,编写测试可能会让人不知所措,即使对于那些改变世界的天才类型也是如此,而且最重要的是,编写测试并不令人兴奋。 有人可能会想,“当我可以实现一项主要功能并取得令我的客户印象深刻的结果时,我为什么还要从事平淡无奇的忙碌工作呢?” 这是一个艰难的论点。

最后但并非最不重要的一点是,编写测试很困难,而且计算机科学专业的学生也没有为此受过训练。

哦,使用单元测试进行重构并不好玩。

意见分歧

在我看来,单元测试对于算法逻辑是有意义的,但对于协调活代码则没有那么重要。

人们声称,即使您在编写测试之前投入了额外的时间,但在调试或更改代码时可以为您节省数小时。 我不敢苟同并提出一个问题:您的代码是静态的,还是不断变化的?

对于我们大多数人来说,它一直在变化。 如果您正在编写成功的软件,那么您总是在添加功能、更改现有功能、删除它们、吃掉它们,无论如何,为了适应这些变化,您必须不断更改您的测试,而更改您的测试需要时间。

但是,你需要某种测试

没有人会争辩说缺乏任何类型的测试是最糟糕的情况。 对代码进行更改后,您需要确认它确实有效。 很多程序员尝试手动测试基础:页面是否在浏览器中呈现? 是否正在提交表单? 显示的内容是否正确? 等等,但在我看来,这是野蛮的、低效的和劳动密集型的。

我用什么代替

测试 Web 应用程序(无论是手动还是自动)的目的是确认任何给定页面在用户浏览器中呈现时没有任何致命错误,并且它正确显示其内容。 实现此目的的一种方法(在大多数情况下是一种更简单的方法)是将 HTTP 请求发送到应用程序的端点并解析响应。 响应代码告诉您页面是否已成功传递。 通过解析 HTTP 请求的响应正文并搜索特定的文本字符串匹配项,可以很容易地测试内容,或者,您可以更进一步并使用诸如 nokogiri 之类的网络抓取库。

如果某些端点需要用户登录,您可以使用为自动化交互设计的库(在进行集成测试时非常理想),例如机械化登录或单击某些链接。 确实,在自动化测试的大局中,这看起来很像集成或功能测试(取决于您如何使用它们),但它的编写速度要快得多,并且可以包含在现有项目中,或添加到新项目中,比建立整个测试框架更省力。 发现!

相关:聘请前 3% 的自由 QA 工程师。

在处理具有广泛值的大型数据库时,边缘情况会带来另一个问题。 测试我们的应用程序是否在所有预期数据集上顺利运行可能会令人生畏。

解决它的一种方法是预测所有边缘情况(这不仅困难,而且通常是不可能的)并为每个情况编写测试。 这很容易变成数百行代码(想象一下恐怖)并且维护起来很麻烦。 然而,通过 HTTP 请求和一行代码,您可以直接在生产数据上测试这些边缘案例,这些数据可以在本地下载到您的开发机器或登台服务器上。

当然,现在这种测试技术不是灵丹妙药,并且有很多缺点,就像任何其他方法一样,但我发现这些类型的测试更快,更容易编写和修改。

实践:使用 HTTP 请求进行测试

由于我们已经确定在没有任何附带测试的情况下编写代码不是一个好主意,所以我对整个应用程序的最基本的测试是在本地向其所有页面发送 HTTP 请求并解析响应标头以获取200 (或所需)代码。

例如,如果我们用 HTTP 请求代替(在 Ruby 中)编写上述测试(寻找特定内容和致命错误的测试),它将是这样的:

 # testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end

curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }涵盖了很多测试用例; 任何在文章页面上引发错误的方法都会在此处捕获,因此它在一次测试中有效地涵盖了数百行代码。

第二部分专门捕获内容错误,可以多次使用以检查页面上的内容。 (可以使用mechanize处理更复杂的请求,但这超出了本博客的范围。)

现在,如果您想测试特定页面是否适用于大量不同的数据库值(例如,您的文章页面模板适用于生产数据库中的所有文章),您可以执行以下操作:

 ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids

这将返回数据库中所有未呈现的文章的 ID 数组,因此现在您可以手动转到特定文章页面并检查问题。

现在,我知道这种测试方式在某些情况下可能不起作用,例如测试独立脚本或发送电子邮件,并且不可否认它比单元测试慢,因为我们为每个测试直接调用端点,但是当您不能进行单元测试或功能测试,或两者兼而有之,这总比没有好。

您将如何构建这些测试? 对于小型、不复杂的项目,您可以将所有测试编写在一个文件中,并在每次提交更改之前运行该文件,但大多数项目都需要一套测试。

我通常为每个端点编写两到三个测试,具体取决于我正在测试的内容。 您也可以尝试测试单个内容(类似于单元测试),但我认为这将是多余且缓慢的,因为您将为每个单元进行 HTTP 调用。 但是,另一方面,它们会更干净,更容易理解。

我建议将您的测试放在您的常规测试文件夹中,每个主要端点都有自己的文件(例如,在 Rails 中,每个模型/控制器都有一个文件),这个文件可以根据我们的内容分为三个部分正在测试。 我经常至少有三个测试:

测试一

检查页面返回时是否没有任何致命错误。

测试一检查页面返回时没有任何致命错误。

请注意我是如何为Post列出所有端点的列表并对其进行迭代以检查每个页面是否在没有任何错误的情况下呈现。 假设一切顺利,并且所有页面都已渲染,您将在终端中看到如下内容: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []

如果任何页面未呈现,您将看到如下内容(在此示例中, posts/index page有错误,因此未呈现): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]

测试二

确认所有预期的内容都在那里:

测试二确认所有预期的内容都在那里。

如果在页面上找到了我们期望的所有内容,结果如下所示(在此示例中,我们确保posts/:id具有帖子标题、描述和状态): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []

如果在页面上找不到任何预期的内容(这里我们希望页面显示帖子的状态 - 如果帖子处于活动状态,则显示“活动”,如果帖子被禁用,则显示“禁用”)结果如下所示: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]

测试三

检查页面是否在所有数据集(如果有)中呈现:

测试 3 检查页面是否在所有数据集中呈现。

如果所有页面都渲染没有任何错误,我们将得到一个空列表: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []

如果某些记录的内容呈现问题(在此示例中,ID 为 2 和 5 的页面出现错误),结果如下所示: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]

如果你想摆弄上面的演示代码,这里是我的 github 项目。

那么哪个更好? 这取决于…

如果出现以下情况,HTTP 请求测试可能是您最好的选择:

  • 您正在使用 Web 应用程序
  • 你正处于时间紧缩状态,想快速写点东西
  • 您正在处理一个未编写测试的大型项目,预先存在的项目,但您仍需要某种方法来检查代码
  • 您的代码涉及简单的请求和响应
  • 您不想花费大部分时间来维护测试(我在某处读过单元测试 = 维护地狱,我部分同意他/她的观点)
  • 您想测试应用程序是否适用于现有数据库中的所有值

在以下情况下,传统测试是理想的:

  • 您正在处理的不是 Web 应用程序,例如脚本
  • 你正在编写复杂的算法代码
  • 你有时间和预算来写测试
  • 业务需要无错误或低错误率(财务、庞大的用户群)

感谢您阅读文章; 您现在应该有一种可以默认使用的测试方法,当您时间紧迫时可以依靠它。

有关的:
  • 性能和效率:使用 HTTP/3
  • 保持加密,保持安全:使用 ESNI、DoH 和 DoT