cache_selfie
helps you build fast deterministic tests even if they contain slow non-deterministic components. A generative AI example is available here.
To use expect_selfie
, you pass a value that you want to snapshot.
expect_selfie(customer.first_name).to_be("Fred")
To use cache_selfie
, you pass a function that returns a value to snapshot.
cache_selfie(lambda: customer.first_name).to_be("Fred")
When selfie is in read mode, it can ignore the function and just return the value within the to_be
call. When selfie is in write mode, it calls the function and sets the snapshot to that result.
You control whether a given value is being read or written using the _TODO
, //selfieonce
, //SELFIEWRITE
mechanism described in the quickstart.
In the examples above, we aren't doing anything with the return value, which is usually a mistake. The benefit of cache_selfie
is that we can take an expensive non-deterministic operation, and build a cheap deterministic test on top of the cached value.
The hazard is that the cached result is not testing the function call anymore. It is just a convenient way to generate sample data for testing other parts of the system.
brittle_assumption = cache_selfie(lambda: expensive_operation()).to_be("sand")
build_stuff_on(brittle_assumption)
Perhaps the to_be
snapshot was recorded a year ago, and the expensive_operation
has changed since then. Perhaps someone manually edited the recorded snapshot, and expensive_operation
has never returned a value anything like the snapshot. The function being cached is not being tested.
If you have a test with multiple cache_selfie
calls, avoid using _TODO
. You can have a situation where you recorded the ending of a test, and then later changed the beginning with _TODO
. The ending won't update itself automatically, so you might cache an inconsistent state. You can avoid this problem by only using #selfieonce
and #SELFIEWRITE
.
You have these choices for specifying the data in a snapshot:
cache_selfie(lambda: "string").to_be("string")
cache_selfie(lambda: "string").to_match_disk()
cache_selfie_binary(lambda: bytearray[3]).to_be_base64("AAAA")
cache_selfie_binary(lambda: bytearray[3]).to_be_file("pkg/someFile.ext")
cache_selfie_binary(lambda: bytearray[3]).to_match_disk()
The to_match_disk
method is nice because Selfie will garbage-collect the snapshot if it isn't being used, you can "set it and forget it". to_be
is nice to read inline with the code, and to_be_file
is nice because you can open the result in external programs.
Oftentimes you want to snapshot something besides just a string or binary. For that there is:
class Roundtrip(Generic[T, SerializedForm]):
def serialize(self, value: T) -> SerializedForm:
"""Serialize a value of type T to its SerializedForm."""
raise NotImplementedError
def parse(self, serialized: SerializedForm) -> T:
"""Parse the SerializedForm back to type T."""
raise NotImplementedError
But you don't have to implement Roundtrip
yourself. You can do cache_selfie_json
, and Roundtrip
will be implemented by json.dumps
and json.loads
.
And of course, you can also write your own Roundtrip
implementation, it's only two functions.
# Fetch the chat response with caching
chat = cache_selfie_json(lambda: openai.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "Expressive but brief language describing a robot creating a self portrait."}
]
).to_dict()).to_be("""{
"id": "chatcmpl-Af1Nf34netAfGW7ZIQArEHavfuYtg",
"choices": [
{
"finish_reason": "stop",
"index": 0,
"logprobs": null,
"message": {
"content": "A sleek robot, its mechanical fingers dancing with precision, deftly wields a brush against the canvas. Whirs and clicks echo softly as vibrant strokes emerge, each infused with an unexpected soulfulness. Metal meets art as synthetic imagination captures its own intricate reflection\\u2014a symphony of circuitry bathed in delicate hues.",
"refusal": null,
"role": "assistant"
}
}
],
"created": 1734340119,
"model": "gpt-4o-2024-08-06",
"object": "chat.completion",
"system_fingerprint": "fp_9faba9f038",
"usage": {
"completion_tokens": 62,
"prompt_tokens": 20,
"total_tokens": 82,
"completion_tokens_details": {
"accepted_prediction_tokens": 0,
"audio_tokens": 0,
"reasoning_tokens": 0,
"rejected_prediction_tokens": 0
},
"prompt_tokens_details": {
"audio_tokens": 0,
"cached_tokens": 0
}
}
}""")
image_url = cache_selfie_json(lambda: openai.images.generate(model="dall-e-3",prompt=chat['choices'][0]['message']['content']).to_dict()).to_be("""{
"created": 1734340142,
"data": [
{
"revised_prompt": "Visualize a sleek robot adorned in a metallic shell. Its highly precise mechanical digits engage rhythmically with a paintbrush, swirling it flawlessly over a robust canvas. The environment is immersed in resonating mechanical sounds blended with the aura of creativity unfurling. Strikingly vivid strokes of paint materialize from the robot's calculated artistry, each stroke conveying a depth and emotion unanticipated of a mechanical entity. This metallic artist exhibits its self-inspired art by meticulously crafting its own intricate reflection\\u2014an orchestra of electronics bathed in a palette of gentle colors.",
"url": "https://oaidalleapiprodscus.blob.core.windows.net/private/org-SUepmbCtftBix3RViJYKuYKY/user-KFRqcsnjZPSTulNaxrY5wjL3/img-JVxDCOAuLoIky3ucNNJWo7fG.png?st=2024-12-16T08%3A09%3A02Z&se=2024-12-16T10%3A09%3A02Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2024-12-16T00%3A47%3A43Z&ske=2024-12-17T00%3A47%3A43Z&sks=b&skv=2024-08-04&sig=nIiMMZBNnqPO2jblJ8pDvWS2AFTOaicAWAD6BDqP9jU%3D"
}
]
}""")
url = image_url["data"][0]["url"]
cache_selfie_binary(lambda: requests.get(url).content).to_be_file("self-portrait.png")
Since we used to_be_file
, we can open self-portrait.png
in Mac Preview / Windows Explorer.
Pull requests to improve the landing page and documentation are greatly appreciated, you can find the source code here.