0%

Request Bodies and a JSON API · practice

Prove PUT Is Idempotent

In Chapter 2 you read two promises the make. PUT is : sending the same PUT twice leaves the server in the same state as sending it once. POST is not: a second identical POST adds a second book. A client relies on those promises when a reply is lost and it has to decide whether to send the request again. Your server now accepts both methods, and nothing yet shows that it keeps either promise. This lesson proves both with two tests of your own.

Compare states, not replies

You saw in Chapter 2 that a repeated DELETE is idempotent even though its two replies differ, 204 and then 404: the promise is about the state on the server, not about what comes back. A test of that promise has to look at the state.

Here that needs no server, because handle takes the state as an . Make a fresh state with reading_server.sample_state(), send a PUT, and keep what the state looks like; send the same PUT again, and compare:

state = reading_server.sample_state()
data = {"title": "Dune", "author": "Frank Herbert"}
reading_server.handle(make_request("PUT", "/books/2", data), state)
books_after_one = len(state["books"])

Arrange, act, assert, as in every test you have written: here the act is the second request, and the assertions compare. Two comparisons say what idempotent means for this server. The number of books after two PUTs equals the number after one. And the book the names, state["books"][2], is the same after both.

What would not prove it is two statuses. A server whose PUT quietly stored a new book every time would still answer 200 OK twice. A test that compares only the replies passes for that server too, and so proves nothing about the promise.

make_request(method, target, data) at the top of tests/test_server.py builds the request for you. With data, it encodes it as and adds content-type: application/json and a byte-counted content-length, as read_body would have left them.

Two POSTs, two books

The POST test checks the opposite promise: two identical POST /books requests create two books. A fresh sample state has two books, so after the two requests it should have four. The two replies say where each book went, in location, and those must differ: two books, two ids.

first = reading_server.handle(make_request("POST", "/books", data), state)

first["headers"]["location"] is the path of the first new book. A server whose POST handed out the same id twice would store the second book over the first, answer 201 Created both times, and send the same location twice. Counting books, or comparing the two locations, catches it.

Why the tests call reading_server.handle

Your tests are themselves checked. Check my work runs them against other versions of handle: one that behaves correctly, and broken ones that keep one promise but not the other. It puts each version in place by replacing reading_server.handle. A test that calls reading_server.handle(...) looks the up in the at the moment it runs, so it meets whichever version is in place. A file that says from reading_server import handle at the top, or handle = reading_server.handle, keeps the original function in a name of its own, and the swap never reaches it. That is why the file’s docstring tells you to call it through the module. A helper function of your own in the test file that calls reading_server.handle(...) is fine too: it also looks the function up each time it runs.

The correct version Check my work uses is its own, not yours, so its error messages may be worded differently from yours. Test statuses, headers, and the state, not the wording of an error.

Which assertion catches a PUT that adds a new book every time?

Your task

In tests/test_server.py, replace the two placeholders that say assert False, "not written yet":

  1. test_repeating_a_put_leaves_one_state: send the same PUT /books/2 twice to one fresh reading_server.sample_state(), and assert that the second left the number of books and book 2 exactly as the first did.

  2. test_repeating_a_post_creates_two_books: send the same POST /books twice to one fresh state, and assert that the state then holds four books and that the two location values differ.

Keep test_get_books_is_json, keep import reading_server, and send every request through reading_server.handle(...), inside each test or in a helper function of your own in the same file. Then run the tests from the Shell tab:

python -m pytest -q

It should report 3 passed. Then press Check my work. The project that ends this chapter adds one more feature to the reading and proves all of it.

Task

Prove with your own tests that PUT is and POST is not.

In tests/test_server.py, replace the two placeholders:

  1. test_repeating_a_put_leaves_one_state: send the same PUT /books/2 with the same body twice to one fresh reading_server.sample_state(). Assert that after the second request the number of books and book 2 are exactly what they were after the first.

  2. test_repeating_a_post_creates_two_books: send the same POST /books twice to one fresh state. Assert that the state then holds two more books than it started with, and that the two replies’ location values differ.

Build requests with the supplied make_request(method, target, data). Keep test_get_books_is_json and import reading_server, and send every request through reading_server.handle(...), inside each test or in a helper of your own in the same file; add no from reading_server import line and do not store handle in a name at the top of the file. Test statuses, headers, and the state, not the wording of error messages.

In the , python -m pytest -q runs the tests; with both written it reports 3 passed.

Then press Check my work. It runs your tests three times, each time with a different handle put in place through the : a correct one of its own, one whose PUT stores the body under a new id on every call, and one whose POST never moves on to the next id, so a second POST overwrites the first. Your tests must all pass against the first, and at least one must fail against each of the others. A run that takes longer than 3 seconds is stopped and reported. Nothing from your terminal is graded.