这是我参与 11 月更文挑战的第 7 天,活动详情查看:2021最后一次更文挑战。
我收到过好几次这样的问题:为什么要用 PUT 请求而不是 POST 请求?
阅读这篇文章后你就会明白为什么了。你将能在 Python 中使用 FastAPI 实现 PUT 方法,并学习如何正确的测试它。
PUT 请求和 POST 请求的差异
让我们从解释 PUT 请求的关键特征开始:幂等性。
如果您多次调用 PUT 请求,则结果不变。 因此,假设你创建了一个资源:一本书。如果你调用该方法两次,结果仍是相同的。更准确地说,在数据库中一本书只存在一次。每本书都是唯一的。
一个 POST 请求有些不同。如果你创建了一本书,然后再创建同一本书,你将拥有两本书。换句话说,第二次请求时你将会得到不同的结果。
两种方法各有千秋,但 PUT 方法在防止副作用方面非常好用。
为了给让你有个更清晰的概念,让我描述一个场景:Juliette 想购买三明治,她点击一个按钮。因为网店的响应有点慢,她再次按下了同一个按钮。同一个请求两次到达服务器。因为你已经使用 PUT 请求实现了该方法,所以它在第二次访问服务器时没有造成任何的影响。Juliette 不必吃两顿饭,她也省下了一笔钱。很棒,对吧?
在许多情况下,PUT 请求非常适合用于防止不必要的更新。
备注:注意,即使你使用 PUT 方法,你仍有可能违反幂等性。如果你用和 POST 方法一样的方式来实现 PUT 方法,两者之间将没有任何区别。这违反了 PUT 的原则。作为一个 API 开发人员,你有责任创建好一个有效的 PUT 请求。
如何创建 PUT 请求
首先,搭建你的 Python 环境。为此,你需要一些库:FastAPI、uvicorn、pydantic 和 requests。你可以使用任意的工具进行安装 —— 我个人最喜欢的是使用虚拟环境。
在本文中,您将从头开始构建 PUT 请求。我们将从一个空书集开始,并创建一本本的新书。我们在本教程中使用的数据类型非常简单:
class Book:
isbn: int
title: str
我们尽量把事情变得简单,此时就不用数据库了。在更新数据方面,字典非常优秀,我们将使用它来代替数据库。
你的 PUT 请求将会创造一本新书。如果这本书不存在,它将在字典里创建一个新的。如果这本书存在,它将替换原有的书。 PUT 请求允许我们一次更新一整个对象。
在 FastAPI 中,代码结构长这样(这取决于你的底层的数据结构):
import fastapi
import uvicorn
from book import Book
app = fastapi.FastAPI()
books: dict[int, Book] = {}
@app.put("/books/")
def create_or_update_book(book: Book) -> Book:
books[book.isbn] = book
现在,让我们回顾一下这里发生了什么。如果书本已经存在,我们替代原有的书。如果书本不存在,执行后字典就会多出一本书。最后,这个方法将返回新增至字典中的 Book 对象。
很棒吧?
PUT 方法与 POST 方法的主要区别在于更新相同的资源时的处理方式。如果你想用 POST 方法做同样的更新,当该键已经存在于字典中时,你应该抛出一个异常。
如果你的数据结构允许重复的值,则 POST 方法不会抛出异常。每次调用 POST 方法时,你都会创建一本新书。这在某些情况下也是合理的,例如在卖书的时候。
测试 PUT 请求
FastAPI 让编写单元测试和集成测试的工作变得简单。开始一个测试总是很困难,尤其是对于较新的框架。但随着你对 FastAPI 越来越熟悉,你也可以完美地完成 TDD。
第一个测试往往是最重要的一个测试。当你创建一本不存在的书时,你将会在你的收藏中得到一本新书。代码如下:
@mock.patch('main.books', {})
def test_givenBook_whenCreateOrUpdateBook_thenBookIsAddedToCollection(self) -> None:
expected_book = book_clean_code
actual_book = create_or_update_book(book_clean_code)
self.assertEqual(expected_book, actual_book)
因为我们用的是字典,我们不能允许一个元素出现两次。这就意味着为这种情况写一个测试是没有意义的。这里还有一种情况需要考虑。如果您更新了一本原有的书,你最终应该得到一本新书,如下所示:
@mock.patch('main.books', {9780132350884: book_clean_code})
def test_givenUpdatedBook_whenCreateOrUpdateBook_thenBookIsUpdated(self) -> None:
expected_book = Book(isbn=9780132350884, title='Cleaner Code')
actual_book = create_or_update_book(expected_book)
self.assertEqual(expected_book, actual_book)
最后,我们也可以编写集成测试。集成测试最重要的部分是看看我们是否得到了正确的 200 OK 响应。此测试与使用 Postman 调用我们的 API 相同,但更好的是你能将此类测试自动化。代码如下:
class TestIntegrationMain(TestCase):
def setUp(self) -> None:
self.client = TestClient(app)
@mock.patch('main.books', {})
def test_integration_givenValidBook_whenCreateOrUpdateBook_thenReturnCreatedBook(self) -> None:
valid_book = {
'isbn': 9780132350884,
'title': 'Clean Code: A Handbook of Agile Software Craftsmanship'
}
response = self.client.put('/books/', json=valid_book)
self.assertEqual(200, response.status_code)
self.assertEqual(valid_book, response.json())
总结
阅读这篇文章后,你能看到 PUT 方法创建了一个稳定的 API。现在你应该明白什么是幂等性了,而且我相信你也能自己实现你的 PUT 方法。
FastAPI 让我们能简单地创建 PUT 请求,也让测试变得非常容易。从简单性这方面来说,这个框架很难被其他框架打败。什么?你还想继续学习 FastAPI?我之前关于 GET、POST 和 DELETE 方法的文章可能对你所帮助,去看看吧~
感谢你阅读这篇文章!