MockupDB

_images/mask.jpg

Mock server for testing MongoDB clients and creating MongoDB Wire Protocol servers.

The Tutorial is the primary documentation.

Contents:

Installation

Install MockupDB with pip:

$ python -m pip install mockupdb

Tutorial

This tutorial is the primary documentation for the MockupDB project.

I assume some familiarity with PyMongo and the MongoDB Wire Protocol.

Introduction

Begin by running a MockupDB and connecting to it with PyMongo’s MongoClient:

>>> from mockupdb import *
>>> server = MockupDB()
>>> port = server.run()  # Returns the TCP port number it listens on.
>>> from pymongo import MongoClient
>>> client = MongoClient(server.uri, connectTimeoutMS=999999)

When the client connects it calls the “ismaster” command, then blocks until the server responds. By default it throws an error if the server doesn’t respond in 10 seconds, so set a longer timeout.

MockupDB receives the “ismaster” command but does not respond until you tell it to:

>>> request = server.receives()
>>> request.command_name
'ismaster'

We respond:

>>> request.replies({'ok': 1, 'maxWireVersion': 6})
True

The receives call blocks until it receives a request from the client. Responding to each “ismaster” call is tiresome, so tell the client to send the default response to all ismaster calls:

>>> responder = server.autoresponds('ismaster', maxWireVersion=6)
>>> client.admin.command('ismaster') == {'ok': 1, 'maxWireVersion': 6}
True

A call to receives now blocks waiting for some request that does not match “ismaster”.

(Notice that replies returns True. This makes more advanced uses of autoresponds easier, see the reference document.)

Reply To Write Commands

If PyMongo sends an unacknowledged OP_INSERT it does not block waiting for you to call replies. However, for acknowledged operations it does block. Use go to defer PyMongo to a background thread so you can respond from the main thread:

>>> collection = client.db.coll
>>> from mockupdb import go
>>> # Default write concern is acknowledged.
>>> future = go(collection.insert_one, {'_id': 1})

Pass a method and its arguments to the go function, the same as to functools.partial. It launches insert_one on a thread and returns a handle to its future outcome. Meanwhile, wait for the client’s request to arrive on the main thread:

>>> cmd = server.receives()
>>> cmd
OpMsg({"insert": "coll", "ordered": true, "$db": "db", "$readPreference": {"mode": "primary"}, "documents": [{"_id": 1}]}, namespace="db")

(Note how MockupDB renders requests and replies as JSON, not Python. The chief differences are that “true” and “false” are lower-case, and the order of keys and values is faithfully shown, even in Python versions with unordered dicts.)

Respond thus:

>>> cmd.ok()
True

The server’s response unblocks the client, so its future contains the return value of insert_one, which is an InsertOneResult:

>>> write_result = future()
>>> write_result  # doctest: +ELLIPSIS
<pymongo.results.InsertOneResult object at ...>
>>> write_result.inserted_id
1

If you don’t need the future’s return value, you can express this more tersely with going:

>>> with going(collection.insert_one, {'_id': 1}):
...     server.receives().ok()
True

Simulate a command error:

>>> future = go(collection.insert_one, {'_id': 1})
>>> server.receives(insert='coll').command_err(11000, 'eek!')
True
>>> future()
Traceback (most recent call last):
  ...
DuplicateKeyError: eek!

Or a network error:

>>> future = go(collection.insert_one, {'_id': 1})
>>> server.receives(insert='coll').hangup()
True
>>> future()
Traceback (most recent call last):
  ...
AutoReconnect: connection closed

Pattern-Match Requests

MockupDB’s pattern-matching is useful for testing: you can tell the server to verify any aspect of the expected client request.

Pass a pattern to receives to test that the next request matches the pattern:

>>> future = go(client.db.command, 'commandFoo')
>>> request = server.receives('commandBar') # doctest: +NORMALIZE_WHITESPACE
Traceback (most recent call last):
  ...
AssertionError: expected to receive Command({"commandBar": 1}),
  got Command({"commandFoo": 1})

Even if the pattern does not match, the request is still popped from the queue.

If you do not know what order you need to accept requests, you can make a little loop:

>>> import traceback
>>> def loop():
...     try:
...         while server.running:
...             # Match queries most restrictive first.
...             if server.got(OpMsg('find', 'coll', filter={'a': {'$gt': 1}})):
...                 server.reply(cursor={'id': 0, 'firstBatch':[{'a': 2}]})
...             elif server.got('break'):
...                 server.ok()
...                 break
...             elif server.got(OpMsg('find', 'coll')):
...                 server.reply(
...                     cursor={'id': 0, 'firstBatch':[{'a': 1}, {'a': 2}]})
...             else:
...                 server.command_err(errmsg='unrecognized request')
...     except:
...         traceback.print_exc()
...         raise
...
>>> future = go(loop)
>>>
>>> list(client.db.coll.find())
[{'a': 1}, {'a': 2}]
>>> list(client.db.coll.find({'a': {'$gt': 1}}))
[{'a': 2}]
>>> client.db.command('break')
{'ok': 1}
>>> future()

You can even implement the “shutdown” command:

>>> def loop():
...     try:
...         while server.running:
...             if server.got('shutdown'):
...                 server.stop()  # Hangs up.
...             else:
...                 server.command_err('unrecognized request')
...     except:
...         traceback.print_exc()
...         raise
...
>>> future = go(loop)
>>> client.db.command('shutdown')
Traceback (most recent call last):
  ...
AutoReconnect: connection closed
>>> future()
>>> server.running
False
>>> client.close()

To show off a difficult test that MockupDB makes easy, assert that PyMongo sends a writeConcern argument if you specify w=1:

>>> server = MockupDB()
>>> responder = server.autoresponds('ismaster', maxWireVersion=6)
>>> port = server.run()
>>>
>>> # Specify w=1. This is distinct from the default write concern.
>>> client = MongoClient(server.uri, w=1)
>>> collection = client.db.coll
>>> future = go(collection.insert_one, {'_id': 4})
>>> server.receives({'writeConcern': {'w': 1}}).sends()
True
>>> client.close()

… but not by default:

>>> # Accept the default write concern.
>>> client = MongoClient(server.uri)
>>> collection = client.db.coll
>>> future = go(collection.insert_one, {'_id': 5})
>>> assert 'writeConcern' not in server.receives()
>>> client.close()

Message Specs

We’ve seen some examples of ways to specify messages to send, and examples of ways to assert that a reply matches an expected pattern. Both are “message specs”, a flexible syntax for describing wire protocol messages.

Matching a request

One of MockupDB’s most useful features for testing your application is that it can assert that your application’s requests match a particular pattern:

>>> client = MongoClient(server.uri)
>>> future = go(client.db.collection.insert, {'_id': 1})
>>> # Assert the command name is "insert" and its parameter is "collection".
>>> request = server.receives(OpMsg('insert', 'collection'))
>>> request.ok()
True
>>> assert future()

If the request did not match, MockupDB would raise an AssertionError.

The arguments to OpMsg above are an example of a message spec. The pattern-matching rules are implemented in Matcher. Here are some more examples.

The empty matcher matches anything:

>>> Matcher().matches({'a': 1})
True
>>> Matcher().matches({'a': 1}, {'a': 1})
True
>>> Matcher().matches('ismaster')
True

A matcher’s document matches if its key-value pairs are a subset of the request’s:

>>> Matcher({'a': 1}).matches({'a': 1})
True
>>> Matcher({'a': 2}).matches({'a': 1})
False
>>> Matcher({'a': 1}).matches({'a': 1, 'b': 1})
True

Prohibit a field:

>>> Matcher({'field': absent})
Matcher(Request({"field": {"absent": 1}}))
>>> Matcher({'field': absent}).matches({'field': 1})
False
>>> Matcher({'field': absent}).matches({'otherField': 1})
True

Order matters if you use an OrderedDict:

>>> doc0 = OrderedDict([('a', 1), ('b', 1)])
>>> doc1 = OrderedDict([('b', 1), ('a', 1)])
>>> Matcher(doc0).matches(doc0)
True
>>> Matcher(doc0).matches(doc1)
False

The matcher must have the same number of documents as the request:

>>> Matcher().matches()
True
>>> Matcher([]).matches([])
True
>>> Matcher({'a': 2}).matches({'a': 1}, {'a': 1})
False

By default, it matches any opcode:

>>> m = Matcher()
>>> m.matches(OpQuery)
True
>>> m.matches(OpInsert)
True

You can specify what request opcode to match:

>>> m = Matcher(OpQuery)
>>> m.matches(OpInsert, {'_id': 1})
False
>>> m.matches(OpQuery, {'_id': 1})
True

Commands in MongoDB 3.6 and later use the OP_MSG wire protocol message. The command name is matched case-insensitively:

>>> Matcher(OpMsg('ismaster')).matches(OpMsg('IsMaster'))
True

You can match properties specific to certain opcodes:

>>> m = Matcher(OpGetMore, num_to_return=3)
>>> m.matches(OpGetMore())
False
>>> m.matches(OpGetMore(num_to_return=2))
False
>>> m.matches(OpGetMore(num_to_return=3))
True
>>> m = Matcher(OpQuery(namespace='db.collection'))
>>> m.matches(OpQuery)
False
>>> m.matches(OpQuery(namespace='db.collection'))
True

It matches any wire protocol header bits you specify:

>>> m = Matcher(flags=QUERY_FLAGS['SlaveOkay'])
>>> m.matches(OpQuery({'_id': 1}))
False
>>> m.matches(OpQuery({'_id': 1}, flags=QUERY_FLAGS['SlaveOkay']))
True

If you match on flags, be careful to also match on opcode. For example, if you simply check that the flag in bit position 0 is set:

>>> m = Matcher(flags=INSERT_FLAGS['ContinueOnError'])

… you will match any request with that flag:

>>> m.matches(OpDelete, flags=DELETE_FLAGS['SingleRemove'])
True

So specify the opcode, too:

>>> m = Matcher(OpInsert, flags=INSERT_FLAGS['ContinueOnError'])
>>> m.matches(OpDelete, flags=DELETE_FLAGS['SingleRemove'])
False

Sending a reply

The default reply is {'ok': 1}:

>>> request = server.receives()
>>> request.ok()  # Send {'ok': 1}.

You can send additional information with the ok method:

>>> request.ok(field='value')  # Send {'ok': 1, 'field': 'value'}.

Simulate a server error with command_err:

>>> request.command_err(code=11000, errmsg='Duplicate key', field='value')

All methods for sending replies parse their arguments with the make_reply internal function. The function interprets its first argument as the “ok” field value if it is a number, otherwise interprets it as the first field of the reply document and assumes the value is 1:

>>> import mockupdb
>>> mockupdb.make_op_msg_reply()
OpMsgReply()
>>> mockupdb.make_op_msg_reply(0)
OpMsgReply({"ok": 0})
>>> mockupdb.make_op_msg_reply("foo")
OpMsgReply({"foo": 1})

You can pass a dict or OrderedDict of fields instead of using keyword arguments. This is best for fieldnames that are not valid Python identifiers:

>>> mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]))
OpMsgReply({"ok": 0, "$err": "bad"})

You can customize the OP_REPLY header flags with the “flags” keyword argument:

>>> r = mockupdb.make_op_msg_reply(OrderedDict([('ok', 0), ('$err', 'bad')]),
...                                flags=OP_MSG_FLAGS['checksumPresent'])
>>> repr(r)
'OpMsgReply({"ok": 0, "$err": "bad"}, flags=checksumPresent)'

Although these examples call make_op_msg_reply explicitly, this is only to illustrate how replies are specified. Your code will pass these arguments to a Request method like replies.

Wait For A Request Impatiently

If your test waits for PyMongo to send a request but receives none, it times out after 10 seconds by default. This way MockupDB ensures that even failing tests all take finite time.

To abbreviate the wait, pass a timeout in seconds to receives:

>>> try:
...     server.receives(timeout=0.1)
... except AssertionError as err:
...     print("Error: %s" % err)
Error: expected to receive Request(), got nothing

Test Cursor Behavior

Test what happens when a query fails:

>>> cursor = collection.find().batch_size(1)
>>> future = go(next, cursor)
>>> server.receives(OpMsg('find', 'coll')).command_err()
True
>>> future()
Traceback (most recent call last):
  ...
OperationFailure: database error: MockupDB command failure

You can simulate normal querying, too:

>>> cursor = collection.find().batch_size(2)
>>> future = go(list, cursor)
>>> documents = [{'_id': 1}, {'x': 2}, {'foo': 'bar'}, {'beauty': True}]
>>> request = server.receives(OpMsg('find', 'coll'))
>>> n = request['batchSize']
>>> request.replies(cursor={'id': 123, 'firstBatch': documents[:n]})
True
>>> while True:
...    getmore = server.receives(OpMsg('getMore', 123))
...    n = getmore['batchSize']
...    if documents:
...        cursor_id = 123
...    else:
...        cursor_id = 0
...    getmore.ok(cursor={'id': cursor_id, 'nextBatch': documents[:n]})
...    print('returned %d' % len(documents[:n]))
...    del documents[:n]
...    if cursor_id == 0:
...        break
True
returned 2
True
returned 2
True
returned 0

The loop receives three getMore commands and replies three times (True is printed each time we call getmore.ok), sending a cursor id of 0 on the last iteration to tell PyMongo that the cursor is finished. The cursor receives all documents:

>>> future()
[{'_id': 1}, {'x': 2}, {'_id': 1}, {'x': 2}, {'foo': 'bar'}, {'beauty': True}]

But this is just a parlor trick. Let us test something serious.

Test Server Discovery And Monitoring

To test PyMongo’s server monitor, make the server a secondary:

>>> hosts = [server.address_string]
>>> secondary_reply = OpReply({
...     'ismaster': False,
...     'secondary': True,
...     'setName': 'rs',
...     'hosts': hosts,
...     'maxWireVersion': 6})
>>> responder = server.autoresponds('ismaster', secondary_reply)

Connect to the replica set:

>>> client = MongoClient(server.uri, replicaSet='rs')
>>> from mockupdb import wait_until
>>> wait_until(lambda: server.address in client.secondaries,
...            'discover secondary')
True

Add a primary to the host list:

>>> primary = MockupDB()
>>> port = primary.run()
>>> hosts.append(primary.address_string)
>>> primary_reply = OpReply({
...     'ismaster': True,
...     'secondary': False,
...     'setName': 'rs',
...     'hosts': hosts,
...     'maxWireVersion': 6})
>>> responder = primary.autoresponds('ismaster', primary_reply)

Client discovers it quickly if there’s a pending operation:

>>> with going(client.db.command, 'buildinfo'):
...     wait_until(lambda: primary.address == client.primary,
...                'discovery primary')
...     primary.pop('buildinfo').ok()
True
True

API Reference

Simulate a MongoDB server, for use in unittests.

class mockupdb.MockupDB(port=None, verbose=False, request_timeout=10, auto_ismaster=None, ssl=False, min_wire_version=0, max_wire_version=6, uds_path=None)

A simulated mongod or mongos.

Call run to start the server, and always close it to avoid exceptions during interpreter shutdown.

See the tutorial for comprehensive examples.

Optional parameters:
 
  • port: listening port number. If not specified, choose some unused port and return the port number from run.
  • verbose: if True, print requests and replies to stdout.
  • request_timeout: seconds to wait for the next client request, or else assert. Default 10 seconds. Pass int(1e6) to disable.
  • auto_ismaster: pass True to autorespond {'ok': 1} to ismaster requests, or pass a dict or OpReply.
  • ssl: pass True to require SSL.
  • min_wire_version: the minWireVersion to include in ismaster responses if auto_ismaster is True, default 0.
  • max_wire_version: the maxWireVersion to include in ismaster responses if auto_ismaster is True, default 6.
  • uds_path: a Unix domain socket path. MockupDB will attempt to delete the path if it already exists.
address

The listening (host, port).

address_string

The listening “host:port”.

append_responder(*args, **kwargs)

Add a responder of last resort.

Like autoresponds, but instead of adding a responder to the top of the stack, add it to the bottom. This responder will be called if no others match.

autoresponds(*args, **kwargs)

Send a canned reply to all matching client requests.

matcher is a Matcher or a command name, or an instance of OpInsert, OpQuery, etc.

>>> s = MockupDB()
>>> port = s.run()
>>>
>>> from pymongo import MongoClient
>>> client = MongoClient(s.uri)
>>> responder = s.autoresponds('ismaster', maxWireVersion=6)
>>> client.admin.command('ismaster') == {'ok': 1, 'maxWireVersion': 6}
True

The remaining arguments are a message spec:

>>> # ok
>>> responder = s.autoresponds('bar', ok=0, errmsg='err')
>>> client.db.command('bar')
Traceback (most recent call last):
...
OperationFailure: command SON([('bar', 1)]) on namespace db.$cmd failed: err
>>> responder = s.autoresponds(OpMsg('find', 'collection'),
...                            {'cursor': {'id': 0, 'firstBatch': [{'_id': 1}, {'_id': 2}]}})
>>> # ok
>>> list(client.db.collection.find()) == [{'_id': 1}, {'_id': 2}]
True
>>> responder = s.autoresponds(OpMsg('find', 'collection'),
...                            {'cursor': {'id': 0, 'firstBatch': [{'a': 1}, {'a': 2}]}})
>>> # bad
>>> list(client.db.collection.find()) == [{'a': 1}, {'a': 2}]
True

Remove an autoresponder like:

>>> responder.cancel()

If the request currently at the head of the queue matches, it is popped and replied to. Future matching requests skip the queue.

>>> future = go(client.db.command, 'baz')
>>> # bad
>>> responder = s.autoresponds('baz', {'key': 'value'})
>>> future() == {'ok': 1, 'key': 'value'}
True

Responders are applied in order, most recently added first, until one matches:

>>> responder = s.autoresponds('baz')
>>> client.db.command('baz') == {'ok': 1}
True
>>> responder.cancel()
>>> # The previous responder takes over again.
>>> client.db.command('baz') == {'ok': 1, 'key': 'value'}
True

You can pass a request handler in place of the message spec. Return True if you handled the request:

>>> responder = s.autoresponds('baz', lambda r: r.ok(a=2))

The standard Request.ok, replies, fail, hangup and so on all return True to make them suitable as handler functions.

>>> client.db.command('baz') == {'ok': 1, 'a': 2}
True

If the request is not handled, it is checked against the remaining responders, or enqueued if none match.

You can pass the handler as the only argument so it receives all requests. For example you could log them, then return None to allow other handlers to run:

>>> def logger(request):
...     if not request.matches('ismaster'):
...         print('logging: %r' % request)
>>> responder = s.autoresponds(logger)
>>> client.db.command('baz') == {'ok': 1, 'a': 2}
logging: OpMsg({"baz": 1, "$db": "db", "$readPreference": {"mode": "primaryPreferred"}}, namespace="db")
True

The synonym subscribe better expresses your intent if your handler never returns True:

>>> subscriber = s.subscribe(logger)
cancel_responder(*args, **kwargs)

Cancel a responder that was registered with autoresponds.

command_err(*args, **kwargs)

Call command_err on the currently enqueued request.

fail(*args, **kwargs)

Call fail on the currently enqueued request.

gets(*args, **kwargs)

Synonym for receives.

got(*args, **kwargs)

Does request match the given message spec?

>>> s = MockupDB(auto_ismaster=True)
>>> port = s.run()
>>> s.got(timeout=0)  # No request enqueued.
False
>>> from pymongo import MongoClient
>>> client = MongoClient(s.uri)
>>> future = go(client.db.command, 'foo')
>>> s.got('foo')
True
>>> s.got(OpMsg('foo', namespace='db'))
True
>>> s.got(OpMsg('foo', key='value'))
False
>>> s.ok()
>>> future() == {'ok': 1}
True
>>> s.stop()
hangs_up()

Synonym for hangup.

hangup()

Call hangup on the currently enqueued request.

host

The listening hostname.

label

Label for logging, or None.

ok(*args, **kwargs)

Synonym for replies.

pop(*args, **kwargs)

Pop the next Request and assert it matches.

Returns None if the server is stopped.

Pass a Request or request pattern to specify what client request to expect. See the tutorial for examples. Pass timeout as a keyword argument to override this server’s request_timeout.

port

The listening port.

receive(*args, **kwargs)

Pop the next Request and assert it matches.

Returns None if the server is stopped.

Pass a Request or request pattern to specify what client request to expect. See the tutorial for examples. Pass timeout as a keyword argument to override this server’s request_timeout.

receives(*args, **kwargs)

Pop the next Request and assert it matches.

Returns None if the server is stopped.

Pass a Request or request pattern to specify what client request to expect. See the tutorial for examples. Pass timeout as a keyword argument to override this server’s request_timeout.

replies(*args, **kwargs)

Call reply on the currently enqueued request.

reply(*args, **kwargs)

Call reply on the currently enqueued request.

request

The currently enqueued Request, or None.

Warning

This property is useful to check what the current request is, but the pattern server.request.replies() is dangerous: you must follow it with server.pop() or the current request remains enqueued. Better to reply with server.pop().replies() than server.request.replies() or any variation on it.

requests_count

Number of requests this server has received.

Includes autoresponded requests.

run(*args, **kwargs)

Begin serving. Returns the bound port, or 0 for domain socket.

running

If this server is started and not stopped.

send(*args, **kwargs)

Call reply on the currently enqueued request.

sends(*args, **kwargs)

Call reply on the currently enqueued request.

stop(*args, **kwargs)

Stop serving. Always call this to clean up after yourself.

subscribe(*args, **kwargs)

Synonym for autoresponds.

uri

Connection string to pass to MongoClient.

verbose

If verbose logging is turned on.

wait(*args, **kwargs)

Synonym for got.

mockupdb.go(fn, *args, **kwargs)

Launch an operation on a thread and get a handle to its future result.

>>> from time import sleep
>>> def print_sleep_print(duration):
...     sleep(duration)
...     print('hello from background thread')
...     sleep(duration)
...     print('goodbye from background thread')
...     return 'return value'
...
>>> future = go(print_sleep_print, 0.1)
>>> sleep(0.15)
hello from background thread
>>> print('main thread')
main thread
>>> result = future()
goodbye from background thread
>>> result
'return value'
mockupdb.going(*args, **kwds)

Launch a thread and wait for its result before exiting the code block.

>>> with going(lambda: 'return value') as future:
...    pass
>>> future()  # Won't block, the future is ready by now.
'return value'

Or discard the result:

>>> with going(lambda: "don't care"):
...    pass

If an exception is raised within the context, the result is lost:

>>> with going(lambda: 'return value') as future:
...    assert 1 == 0
Traceback (most recent call last):
...
AssertionError
mockupdb.wait_until(predicate, success_description, timeout=10)

Wait up to 10 seconds (by default) for predicate to be true.

E.g.:

wait_until(lambda: client.primary == (‘a’, 1),
‘connect to the primary’)

If the lambda-expression isn’t true after 10 seconds, we raise AssertionError(“Didn’t ever connect to the primary”).

Returns the predicate’s first true value.

mockupdb.interactive_server(port=27017, verbose=True, all_ok=False, name='MockupDB', ssl=False, uds_path=None)

A MockupDB that the mongo shell can connect to.

Call run on the returned server, and clean it up with stop.

If all_ok is True, replies {ok: 1} to anything unmatched by a specific responder.

class mockupdb.Request(*args, **kwargs)

Base class for Command, OpMsg, and so on.

Some useful asserts you can do in tests:

>>> {'_id': 0} in OpInsert({'_id': 0})
True
>>> {'_id': 1} in OpInsert({'_id': 0})
False
>>> {'_id': 1} in OpInsert([{'_id': 0}, {'_id': 1}])
True
>>> {'_id': 1} == OpInsert([{'_id': 0}, {'_id': 1}])[1]
True
>>> 'field' in OpMsg(field=1)
True
>>> 'field' in OpMsg()
False
>>> 'field' in OpMsg('ismaster')
False
>>> OpMsg(ismaster=False)['ismaster'] is False
True
assert_matches(*args, **kwargs)

Assert this matches a message spec.

Returns self.

client_port

Client connection’s TCP port.

command_err(code=1, errmsg='MockupDB command failure', *args, **kwargs)

Error reply to a command.

Returns True so it is suitable as an autoresponds handler.

doc

The request document, if there is exactly one.

Use this for queries, commands, and legacy deletes. Legacy writes may have many documents, OP_GET_MORE and OP_KILL_CURSORS have none.

docs

The request documents, if any.

fail(err='MockupDB query failure', *args, **kwargs)

Reply to a query with the QueryFailure flag and an ‘$err’ key.

Returns True so it is suitable as an autoresponds handler.

flags

The request flags or None.

hangs_up()

Synonym for hangup.

hangup()

Close the connection.

Returns True so it is suitable as an autoresponds handler.

matches(*args, **kwargs)

True if this matches a message spec.

namespace

The operation namespace or None.

ok(*args, **kwargs)

Synonym for replies.

replies(*args, **kwargs)

Send an OpReply to the client.

The default reply to a command is {'ok': 1}, otherwise the default is empty (no documents).

Returns True so it is suitable as an autoresponds handler.

reply(*args, **kwargs)

Send an OpReply to the client.

The default reply to a command is {'ok': 1}, otherwise the default is empty (no documents).

Returns True so it is suitable as an autoresponds handler.

request_id

The request id or None.

send(*args, **kwargs)

Send an OpReply to the client.

The default reply to a command is {'ok': 1}, otherwise the default is empty (no documents).

Returns True so it is suitable as an autoresponds handler.

sends(*args, **kwargs)

Send an OpReply to the client.

The default reply to a command is {'ok': 1}, otherwise the default is empty (no documents).

Returns True so it is suitable as an autoresponds handler.

server

The MockupDB server.

slave_ok

True if the SlaveOkay wire protocol flag is set.

slave_okay

Synonym for slave_ok.

class mockupdb.Command(*args, **kwargs)

A command the client executes on the server.

replies_to_gle(**kwargs)

Send a getlasterror response.

Defaults to {ok: 1, err: null}. Add or override values by passing keyword arguments.

Returns True so it is suitable as an autoresponds handler.

class mockupdb.OpQuery(*args, **kwargs)

A query (besides a command) the client executes on the server.

>>> OpQuery({'i': {'$gt': 2}}, fields={'j': False})
OpQuery({"i": {"$gt": 2}}, fields={"j": false})
fields

Client query’s fields selector or None.

num_to_return

Client query’s numToReturn or None.

num_to_skip

Client query’s numToSkip or None.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpQuery or Command.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpGetMore(**kwargs)

An OP_GET_MORE the client executes on the server.

cursor_id

The client message’s cursorId field.

num_to_return

The client message’s numToReturn field.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpGetMore.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpKillCursors(**kwargs)

An OP_KILL_CURSORS the client executes on the server.

cursor_ids

List of cursor ids the client wants to kill.

classmethod unpack(msg, client, server, _)

Parse message and return an OpKillCursors.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpInsert(*args, **kwargs)

A legacy OP_INSERT the client executes on the server.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpInsert.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpUpdate(*args, **kwargs)

A legacy OP_UPDATE the client executes on the server.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpUpdate.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpDelete(*args, **kwargs)

A legacy OP_DELETE the client executes on the server.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpDelete.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.OpReply(*args, **kwargs)

An OP_REPLY reply from MockupDB to the client.

docs

The reply documents, if any.

reply_bytes(request)

Take a Request and return an OP_REPLY message as bytes.

update(*args, **kwargs)

Update the document. Same as dict().update().

>>> reply = OpReply({'ismaster': True})
>>> reply.update(maxWireVersion=3)
>>> reply.doc['maxWireVersion']
3
>>> reply.update({'maxWriteBatchSize': 10, 'msg': 'isdbgrid'})
class mockupdb.OpMsg(*args, **kwargs)

An OP_MSG request the client executes on the server.

checksum

The provided checksum, if set, else None.

command_name

The command name or None.

>>> OpMsg({'count': 'collection'}).command_name
'count'
>>> OpMsg('aggregate', 'collection', cursor=absent).command_name
'aggregate'
slave_ok

True if this OpMsg can read from a secondary.

slave_okay

Synonym for slave_ok.

classmethod unpack(msg, client, server, request_id)

Parse message and return an OpMsg.

Takes the client message as bytes, the client and server socket objects, and the client request id.

class mockupdb.Matcher(*args, **kwargs)

Matches a subset of Request objects.

Initialized with a message spec.

Used by receives to assert the client sent the expected request, and by got to test if it did and return True or False. Used by autoresponds to match requests with autoresponses.

matches(*args, **kwargs)

Test if a request matches a message spec.

Returns True or False.

prototype

The prototype Request used to match actual requests with.

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions

Report Bugs

Report bugs at https://github.com/ajdavis/mongo-mockup-db/issues.

If you are reporting a bug, please include:

  • Your operating system name and version.
  • Any details about your local setup that might be helpful in troubleshooting.
  • Detailed steps to reproduce the bug.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.

Implement Features

Look through the GitHub issues for features. Anything tagged with “feature” is open to whoever wants to implement it.

Write Documentation

MockupDB could always use more documentation, whether as part of the official MockupDB docs, in docstrings, or even on the web in blog posts, articles, and such.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/ajdavis/mongo-mockup-db/issues.

If you are proposing a feature:

  • Explain in detail how it would work.
  • Keep the scope as narrow as possible, to make it easier to implement.
  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Get Started!

Ready to contribute? Here’s how to set up MockupDB for local development.

  1. Fork the mongo-mockup-db repo on GitHub.

  2. Clone your fork locally:

    $ git clone git@github.com:your_name_here/mongo-mockup-db.git
    
  3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:

    $ mkvirtualenv mongo-mockup-db
    $ cd mongo-mockup-db/
    $ python setup.py develop
    
  4. Create a branch for local development:

    $ git checkout -b name-of-your-bugfix-or-feature
    

    Now you can make your changes locally.

  5. When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:

    $ flake8 mockupdb tests
    $ python setup.py test
    $ tox
    

    To get flake8 and tox, just pip install them into your virtualenv.

  6. Commit your changes and push your branch to GitHub:

    $ git add .
    $ git commit -m "Your detailed description of your changes."
    $ git push origin name-of-your-bugfix-or-feature
    
  7. Submit a pull request through the GitHub website.

Pull Request Guidelines

Before you submit a pull request, check that it meets these guidelines:

  1. The pull request should include tests.
  2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
  3. The pull request should work for Python 2.7 and 3.4+. Check that tests pass in all versions with tox.

Tips

To run a subset of tests:

$ python setup.py test -s tests.test_mockupdb

Credits

Development Lead

Contributors

  • Sean Purcell
  • George Wilson
  • Shane Harvey

Changelog

1.8.1 (2021-10-14)

Fix a bug where MockupDB did not recognize the OP_MSG exhaustAllowed flag.

1.8.0 (2020-09-26)

MockupDB supports Python 3.4 through 3.8; it no longer supports Python 2.6 or Python 3.3.

New method MockupDB.append_responder to add an autoresponder of last resort.

Fix a bug in interactive_server with all_ok=True. It had returned an empty isMaster response, causing drivers to throw errors like “Server at localhost:27017 reports wire version 0, but this version of PyMongo requires at least 2 (MongoDB 2.6).”

Stop logging “OSError: [WinError 10038] An operation was attempted on something that is not a socket” on Windows after a client disconnects.

Parse OP_MSGs with any number of sections in any order. This allows write commands from the mongo shell, which sends sections in the opposite order from drivers. Handle OP_MSGs with checksums, such as those sent by the mongo shell beginning in 4.2.

1.7.0 (2018-12-02)

Improve datetime support in match expressions. Python datetimes have microsecond precision but BSON only has milliseconds, so expressions like this always failed:

server.receives(Command('foo', when=datetime(2018, 12, 1, 6, 6, 6, 12345)))

Now, the matching logic has been rewritten to recurse through arrays and subdocuments, comparing them value by value. It compares datetime values with only millisecond precision.

1.6.0 (2018-11-16)

Remove vendored BSON library. Instead, require PyMongo and use its BSON library. This avoids surprising problems where a BSON type created with PyMongo does not appear equal to one created with MockupDB, and it avoids the occasional need to update the vendored code to support new BSON features.

1.5.0 (2018-11-02)

Support for Unix domain paths with uds_path parameter.

The interactive_server() function now prepares the server to autorespond to the getFreeMonitoringStatus command from the mongo shell.

1.4.1 (2018-06-30)

Fix an inadvertent dependency on PyMongo, which broke the docs build.

1.4.0 (2018-06-29)

Support, and expect, OP_MSG requests from clients. Thanks to Shane Harvey for the contribution.

Update vendored bson library from PyMongo. Support the Decimal128 BSON type. Fix Matcher so it equates BSON objects from PyMongo like ObjectId(...) with equivalent objects created from MockupDB’s vendored bson library.

1.3.0 (2018-02-19)

Support Windows. Log a traceback if a bad client request causes an assert. Fix SSL. Make errors less likely on shutdown. Enable testing on Travis and Appveyor. Fix doctests and interactive server for modern MongoDB protocol.

1.2.1 (2017-12-06)

Set minWireVersion to 0, not to 2. I had been wrong about MongoDB 3.6’s wire version range: it’s actually 0 to 6. MockupDB now reports the same wire version range as MongoDB 3.6 by default.

1.2.0 (2017-09-22)

Update for MongoDB 3.6: report minWireVersion 2 and maxWireVersion 6 by default.

1.1.3 (2017-04-23)

Avoid rare RuntimeError in close(), if a client thread shuts down a socket as MockupDB iterates its list of sockets.

1.1.2 (2016-08-23)

Properly detect closed sockets so MockupDB.stop() doesn’t take 10 seconds per connection. Thanks to Sean Purcell.

1.1.1 (2016-08-01)

Don’t use “client” as a keyword arg for Request, it conflicts with the actual “client” field in drivers’ new handshake protocol.

1.1.0 (2016-02-11)

Add cursor_id property to OpGetMore, and ssl parameter to interactive_server.

1.0.3 (2015-09-12)

MockupDB(auto_ismaster=True) had just responded {"ok": 1}, but this isn’t enough to convince PyMongo 3 it’s talking to a valid standalone, so auto-respond {"ok": 1, "ismaster": True}.

1.0.2 (2015-09-11)

Restore Request.assert_matches method, used in pymongo-mockup-tests.

1.0.1 (2015-09-11)

Allow co-installation with PyMongo.

1.0.0 (2015-09-10)

First release.

0.1.0 (2015-02-25)

Development begun.

Indices and tables

Image Credit: gnuckx