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.


Benefits and hazards  

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.


Strings and binary  

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.


Roundtripping typed data  

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.


Example  

# 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.

Robot self portrait

Pull requests to improve the landing page and documentation are greatly appreciated, you can find the source code here.