Getting Started

The first step is to make sure Betamax is right for you. Let’s start by answering the following questions

  • Are you using Requests?

    If you’re not using Requests, Betamax is not for you. You should checkout VCRpy.

  • Are you using Sessions or are you using the functional API (e.g., requests.get)?

    If you’re using the functional API, and aren’t willing to use Sessions, Betamax is not yet for you.

So if you’re using Requests and you’re using Sessions, you’re in the right place.

Betamax officially supports py.test and unittest but it should integrate well with nose as well.

Installation

$ pip install betamax

Configuration

When starting with Betamax, you need to tell it where to store the cassettes that it creates. There’s two ways to do this:

  1. If you’re using Betamax or use_cassette you can pass the cassette_library_dir option. For example,

    import betamax
    import requests
    
    session = requests.Session()
    recorder = betamax.Betamax(session, cassette_library_dir='cassettes')
    with recorder.use_cassette('introduction'):
        # ...
    
  2. You can do it once, globally, for your test suite.

    import betamax
    
    with betamax.Betamax.configure() as config:
        config.cassette_library_dir = 'cassettes'
    

Note

If you don’t set a cassette directory, Betamax won’t save cassettes to disk

There are other configuration options that can be provided, but this is the only one that is required.

Recording Your First Cassette

Let’s make a file named our_first_recorded_session.py. Let’s add the following to our file:

import betamax
import requests

CASSETTE_LIBRARY_DIR = 'examples/cassettes/'


def main():
    session = requests.Session()
    recorder = betamax.Betamax(
        session, cassette_library_dir=CASSETTE_LIBRARY_DIR
    )

    with recorder.use_cassette('our-first-recorded-session'):
        session.get('https://httpbin.org/get')


if __name__ == '__main__':
    main()

If we then run our script, we’ll see that a new file is created in our specified cassette directory. It should look something like:

{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Connection": ["keep-alive"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "User-Agent": ["python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"]}, "method": "GET", "uri": "https://httpbin.org/get"}, "response": {"body": {"string": "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/get\"\n}\n", "encoding": null}, "headers": {"content-length": ["265"], "server": ["nginx"], "connection": ["keep-alive"], "access-control-allow-credentials": ["true"], "date": ["Fri, 19 Jun 2015 04:10:33 GMT"], "access-control-allow-origin": ["*"], "content-type": ["application/json"]}, "status": {"message": "OK", "code": 200}, "url": "https://httpbin.org/get"}, "recorded_at": "2015-06-19T04:10:33"}], "recorded_with": "betamax/0.4.1"}

Now, each subsequent time that we run that script, we will use the recorded interaction instead of talking to the internet over and over again.

Recording More Complex Cassettes

Most times we cannot isolate our tests to a single request at a time, so we’ll have cassettes that make multiple requests. Betamax can handle these with ease, let’s take a look at an example.

import betamax
from betamax_serializers import pretty_json
import requests

CASSETTE_LIBRARY_DIR = 'examples/cassettes/'


def main():
    session = requests.Session()
    betamax.Betamax.register_serializer(pretty_json.PrettyJSONSerializer)
    recorder = betamax.Betamax(
        session, cassette_library_dir=CASSETTE_LIBRARY_DIR
    )

    with recorder.use_cassette('more-complicated-cassettes',
                               serialize_with='prettyjson'):
        session.get('https://httpbin.org/get')
        session.post('https://httpbin.org/post',
                     params={'id': '20'},
                     json={'some-attribute': 'some-value'})
        session.get('https://httpbin.org/get', params={'id': '20'})


if __name__ == '__main__':
    main()

Before we run this example, we have to install a new package: betamax-serializers, e.g., pip install betamax-serializers.

If we now run our new example, we’ll see a new file appear in our examples/cassettes/ directory named more-complicated-cassettes.json. This cassette will be much larger as a result of making 3 requests and receiving 3 responses. You’ll also notice that we imported betamax_serializers.pretty_json and called register_serializer() with PrettyJSONSerializer. Then we added a keyword argument to our invocation of use_cassette(), serialize_with='prettyjson'. PrettyJSONSerializer is a class provided by the betamax-serializers package on PyPI that can serialize and deserialize cassette data into JSON while allowing it to be easily human readable and pretty. Let’s see the results:

{
  "http_interactions": [
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": ""
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "GET",
        "uri": "https://httpbin.org/get"
      },
      "response": {
        "body": {
          "encoding": null,
          "string": "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/get\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "265"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/get"
      }
    },
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": "{\"some-attribute\": \"some-value\"}"
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "Content-Length": [
            "32"
          ],
          "Content-Type": [
            "application/json"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "POST",
        "uri": "https://httpbin.org/post?id=20"
      },
      "response": {
        "body": {
          "encoding": null,
          "string": "{\n  \"args\": {\n    \"id\": \"20\"\n  }, \n  \"data\": \"{\\\"some-attribute\\\": \\\"some-value\\\"}\", \n  \"files\": {}, \n  \"form\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Content-Length\": \"32\", \n    \"Content-Type\": \"application/json\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"json\": {\n    \"some-attribute\": \"some-value\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/post?id=20\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "495"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/post?id=20"
      }
    },
    {
      "recorded_at": "2015-06-21T19:22:54",
      "request": {
        "body": {
          "encoding": "utf-8",
          "string": ""
        },
        "headers": {
          "Accept": [
            "*/*"
          ],
          "Accept-Encoding": [
            "gzip, deflate"
          ],
          "Connection": [
            "keep-alive"
          ],
          "User-Agent": [
            "python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0"
          ]
        },
        "method": "GET",
        "uri": "https://httpbin.org/get?id=20"
      },
      "response": {
        "body": {
          "encoding": null,
          "string": "{\n  \"args\": {\n    \"id\": \"20\"\n  }, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"gzip, deflate\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"python-requests/2.7.0 CPython/2.7.9 Darwin/14.1.0\"\n  }, \n  \"origin\": \"127.0.0.1\", \n  \"url\": \"https://httpbin.org/get?id=20\"\n}\n"
        },
        "headers": {
          "access-control-allow-credentials": [
            "true"
          ],
          "access-control-allow-origin": [
            "*"
          ],
          "connection": [
            "keep-alive"
          ],
          "content-length": [
            "289"
          ],
          "content-type": [
            "application/json"
          ],
          "date": [
            "Sun, 21 Jun 2015 19:22:54 GMT"
          ],
          "server": [
            "nginx"
          ]
        },
        "status": {
          "code": 200,
          "message": "OK"
        },
        "url": "https://httpbin.org/get?id=20"
      }
    }
  ],
  "recorded_with": "betamax/0.4.2"
}

This makes the cassette easy to read and helps us recognize that requests and responses are paired together. We’ll explore cassettes more a bit later.