Test Streaming Code

Su Suriya Ravichandran Updated 10 Nov 2026
5 min read ·Lesson 184 of 200

Testing Something That Arrives in Pieces

A streamed response (Lessons 51 to 60) is not a single value. It is a sequence of events that arrive over time: a start event, a series of text chunks, and a stop event. An application that displays the text as it arrives, or assembles it into a final answer, must handle every event, and it must handle a stream that stops early. The stop may come from a dropped connection, a timeout, or a server-side error, and any of them leaves the caller holding part of an answer.

Testing streaming code is therefore a matter of testing a sequence. The tests must feed the code a chosen sequence of events, including the unusual ones, and check what the code does at each step and at the end. Because a live stream is slow and unpredictable, the tests use a fake stream, built from a generator, that yields the events the test chooses. This lesson covers how to build such a fake, how to test the accumulation and the early-stop cases, and how to check that the stream is closed.


The Code Under Test

The function below consumes a stream of events, collects the text, and reports whether the stream completed. It is the streaming counterpart of the fallback in Lesson 183, with the same principle: the caller always receives a result, even when the stream fails:

"""stream_collect.py: gather text from a stream of events, and report whether it completed."""

from anthropic import APIConnectionError


def collect_text(events) -> tuple[str, bool]:
    parts: list[str] = []
    try:
        for event in events:
            if event.type == "text":
                parts.append(event.text)
            elif event.type == "stop":
                return "".join(parts), True
    except APIConnectionError:
        return "".join(parts), False
    return "".join(parts), False

The function returns a pair: the text received so far, and whether the stop event arrived. A stream that ends without a stop event is incomplete, even though no error was raised, and the function treats it that way.


A Fake Stream

A fake stream is a generator that yields events the test defines. Events are represented by simple objects with a type and the fields the code reads. A generator can also raise an exception partway through, which is how the test produces a dropped connection:

"""stream_demo.py: run the collector against fake streams, including ones that fail."""

from types import SimpleNamespace

import httpx
from anthropic import APIConnectionError

from stream_collect import collect_text

REQUEST = httpx.Request("POST", "https://api.anthropic.com/v1/messages")


def text(chunk):
    return SimpleNamespace(type="text", text=chunk)


STOP = SimpleNamespace(type="stop")


def normal_stream():
    yield text("Hel")
    yield text("lo")
    yield STOP


def dropped_stream():
    yield text("Hel")
    raise APIConnectionError(request=REQUEST)


def unfinished_stream():
    yield text("Hel")
    yield text("lo")


if __name__ == "__main__":
    print("normal:", collect_text(normal_stream()))
    print("dropped:", collect_text(dropped_stream()))
    print("no stop:", collect_text(unfinished_stream()))

Output:

normal: ('Hello', True)
dropped: ('Hel', False)
no stop: ('Hello', False)

How It Works

  1. The normal stream yields two chunks and a stop, and the collector joins the chunks into the full word. The completion flag is true because the stop event arrived.
  2. The dropped stream raises after one chunk. The collector keeps the chunk it received, which is the partial answer that Lesson 145 said should be preserved, and reports that the stream did not complete.
  3. The unfinished stream ends quietly, with no stop event and no error. This case is easy to miss, because nothing failed, yet the answer is still incomplete. The test ensures the flag is false.
  4. The three cases cover the three outcomes a stream can have: complete, interrupted, and silently truncated.

Writing the Tests

The three cases become tests that assert on the pair the collector returns. Each one names the behavior it protects, so a failure points directly to the broken rule:

"""test_stream_collect.py: the collector keeps partial text and reports completion accurately."""

from types import SimpleNamespace

import httpx
from anthropic import APIConnectionError

from stream_collect import collect_text

REQUEST = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
STOP = SimpleNamespace(type="stop")


def chunks(*texts):
    return [SimpleNamespace(type="text", text=t) for t in texts]


def test_complete_stream_is_reported_complete():
    assert collect_text(chunks("Hel", "lo") + [STOP]) == ("Hello", True)


def test_dropped_stream_keeps_partial_text():
    def events():
        yield from chunks("Hel")
        raise APIConnectionError(request=REQUEST)
    assert collect_text(events()) == ("Hel", False)


def test_stream_without_stop_is_incomplete():
    assert collect_text(chunks("Hel", "lo")) == ("Hello", False)

Running python -m pytest -q test_stream_collect.py reports three passed tests. The first test passes a list rather than a generator, which the collector accepts because it only iterates over its input.


Checking the Stream Is Closed

A real stream holds a connection open until it is closed. An application that exits early, because of an error or a limit, must close the stream, or the connection stays open until a timeout. The SDK's streaming interface is a context manager, which closes the stream when the block ends, even if an exception occurred. A test can check that behavior with a fake context manager that records whether it was closed:

class FakeStream:
    def __init__(self, events):
        self.events = events
        self.closed = False

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        self.closed = True
        return False

    def __iter__(self):
        return iter(self.events)


stream = FakeStream(chunks("Hel") + [])
with stream as open_stream:
    collect_text(open_stream)
print("closed:", stream.closed)

Output:

closed: True

The check confirms that leaving the block closes the stream, which the test can rely on because the context manager's __exit__ is always called. The fake records the closure in an attribute, so the test asserts on it directly.


Testing the Application Around the Stream

The collector is one piece. The application that displays text as it arrives, or saves the final answer, needs its own tests. The most useful ones check that displayed text matches the collected text, and that a partial answer is labeled as partial to the user. A test that feeds a dropped stream should confirm that the interface shows the partial text with a notice, not a finished-looking answer.


Summary

A stream is a sequence of events that can end in three ways: completed, interrupted by an error, or silently truncated. Tests feed the collector a chosen sequence, using generators that yield events and can raise partway through, and assert on the text and the completion flag. A fake context manager confirms that the stream is closed when the code leaves the block. The application around the collector needs its own tests, so that the user sees partial answers as partial. The next lesson turns to measuring the tokens an application uses, which is the foundation for controlling its cost.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Test Streaming Code and get answers drawn from it.

Signed-in readers only.