Improving Driver Documentation: The MongoDB Meta Driver
This is a guest post, written by Mario Alvarez, a MongoDB intern for Summer 2013
This summer, I worked on developing the Meta Driver project, an effort to re-work the drivers documentation, creating an implementable specification of driver functionality that could be applied to different drivers in different languages.
Part of MongoDB’s appeal to developers is its ease of use. One important way in which MongoDB provides a good user experience for developers is its ecosystem of drivers - bindings for interacting with the database in a variety of languages, with one for each (major) language. Whereas many other databases force developers to construct queries and database commands using a specific query language, MongoDB’s drivers allow for smooth, idiomatic interaction with the database. Drivers must balance conflicting goals: on the one hand, they should allow developers to write code naturally in the language of their choice; on the other, drivers should strive to provide a relatively consistent experience across languages, to minimize the difficulty of switching between MongoDB-oriented development in different languages.
Because of the language-dependence of providing a natural developer experience, as well as the drivers’ varying authorship (many originated as, or still are, community-supported projects) MongoDB’s drivers embody a wide variety of design decisions regarding these and other tradeoffs. This, coupled with the lack of a fully clear specification of driver functionality, makes it difficult to create or maintain drivers - in cases where the current spec falls short, it is hard to know which other driver to look to as an example.
The Meta Driver is part of a solution to this issue. It creates an implementable specification of driver functionality, as well as creating a reference driver implementation, following the principles of Behavior-Driven Development, to demonstrate a driver meeting the specification. While this project alone is not a complete answer to the problem of inconsistencies within the driver ecosystem, it provides a useful framework around which to begin the process of standardization, as well as the basis of a single, unified driver documentation. In order to achieve these goals, an emphasis on improving the quality and consistency of driver documentation and on more hands-on management of drivers by 10gen itself will be necessary, among other things.
Behavior-Driven Development (BDD)
Behavior-Driven Development is a development methodology that emphasizes the creation and maintenance of useful documentation. Its principles and techniques help make the Meta Driver possible.
BDD has its roots in the Agile community; particularly, in Test-Driven Development (TDD). Under TDD, a developer first writes tests describing the functionality she wants her code to implement, then writes the minimum amount of useful code that will make the tests pass. As a discipline, TDD can be a useful defense against over-architecting code at the beginning, and also leads to the creation of a comprehensive testing framework that (at least ideally) completely describes the code’s functionality, making the code much easier to maintain.
BDD builds on this methodology, taking it a step further. Rather than beginning with writing tests, the Behavior-Driven developer begins by writing a human-readable specification of the behaviors her code should implement. These specifications do not have arbitrary structure; they are written in a format (generally, a simple specification language called Gherkin) that computers can parse as well. Thus, the specifications form a common ground between human and machine understanding of the specified software. In fact, Gherkin’s syntax is so natural that it can be read by non-technical stakeholders in a project, enabling the creation of a common language shared by everyone involved, minimizing the possibility for misunderstanding or ambiguity. The specification is oriented around user-stories, describing various scenarios of interaction with the software, and the behavior the software should exhibit in each scenario.
Next, BDD translates the written specifications into tests run against the code. This is done by writing a series of step definitions that map each step of each scenario to code implementing the step. Once these are written, the developer proceeds just as with TDD, hacking until the tests pass. The difference is that, rather than just a series of tests, the developer has a human-readable specification for her code, and a guarantee that the specification is met. This helps to solve a difficult problem in software engineering: keeping documentation current as the documented codebase changes. If documentation has behavior specifications at its core, it will always be current. BDD serves to keep code and documentation “honest”: if the two do not match, the tests will not pass, and the developer will know where the problem is.
BDD in action: an example
The specifications for a project in BDD are contained in .feature files, which describe scenarios, each with a series of steps, as described above. Here is a simple example.
As someone who works with numbers
I want to be able to learn the total of my numbers
So I add my numbers together to get a result
<pre><code>Scenario: Adding two numbers
Given the number 5
And the other number 42
When I add the two numbers
Then the result should be 47</code></pre></p><p>These files are placed in the features directory (default location) at the root of the project being specified. The step definitions are placed in features/support. These are written in the language of the code the specifications are being run against (in this case, .rb files). Below are the step definitions implementing the steps making up the feature excerpt shown above.</p><p></p>
Given /^the number (-?\d+)$/ do |addend|
@addend1 = addend.to_i
<p>Given /^the other number (-?\d+)$/ do |addend|
@addend2 = addend.to_i
<p>When /^I add the two numbers$/ do
@sum = @addend1 + @addend2
<p>Then /^the result should be (-?\d+)$/ do |sum|
@sum.should == sum.to_i
Cucumber matches the provided regular expressions against lines in the .feature file, using matching groups (surrounded by parentheses) to extract arguments that are then passed in to the provided code blocks. In this way, steps can be made flexible, able to accept a variety of different arguments and thus able to model a wider range of use-cases.
To run these steps, one must first install Cucumber (this can be done via RubyGems: gem install cucumber). The cucumber executable can take a variety of options; cucumber —help will give a fairly detailed description. Without any options, cucumber looks in the default features directory and attempts to run all the tests described by the features and step definitions for all of them. For each step, Cucumber outputs whether the test succeeded or failed; and, if the step failed, Cucumber prints information about the specific assertion failure that caused the step to fail. For this example, the output should look like this:
As someone who works with numbers
I want to be able to learn the total of my numbers
So I add my numbers together to get a result
<pre><code>Scenario: Adding two numbers # features/adding.feature:6
Given the number 5 # features/step_definitions/step_definitions.rb:1
And the other number 42 # features/step_definitions/step_definitions.rb:5
When I add the two numbers # features/step_definitions/step_definitions.rb:9
Then the result should be 47 # features/step_definitions/step_definitions.rb:13
1 scenario (1 passed)
4 steps (4 passed)
BDD and Drivers: The Meta Driver
BDD can play a particularly useful role in helping to define and standardize driver behavior across languages. Since the Gherkin specifications of driver functionality are language-independent, they can serve as a common reference for all drivers, and be directly implemented as tests on each driver. In this way, drivers can be held to a common set of functionality.
Meta Driver Challenges
In practice, this approach can lead to some complications. The specifications must be written with an eye toward being independent of the special quirks and needs of each language. Otherwise, overly-complex step definitions would be required, in order to “hack around” the shorcomings of the specifications; this would obscure the direct relationship between the feature-files and the tests to which feature scenarios correspond, limiting the specification’s usefulness. As the specifications are linked to an ever-widening array of drivers and languages, they will likely have to be revised, to take into account any new language-dependencies we discover in the process. Slight differences between the specifications used in different languages may need to be introduced, if languages have differences that cannot be reconciled at the feature-spec level. We hope to keep these differences to a minimum, if they are needed at all; so far, they have not been.
So far, I have specified a new BSON implementation for Ruby (the code for which - but not the Cucumber spec - can be found here). Building on this implementation, I have implemented and specified the MongoDB wire protocol, allowing the reference driver to communicate with a real MongoDB server. Finally, I implemented the beginnings of a CRUD API, enabling a user to issue insert, update, delete, and find commands to the database.
I have also begun work on writing step definitions in Python to attach the specification to the PyMongo driver, as a first test-case for using the specification across programming-language lines, and on a pre-existing, production driver. So far I have implemented steps on PyMongo for BSON and the wire protocol. Though adjusting for differences in the driver APIs and the tools available for Ruby and Python caused some minor hitches. As we’d hoped, only small changes were needed in order to make the specifications work with PyMongo, and the changes can be back-ported to the Ruby driver, in order to keep the two consistent.
The full specification can be found on Github. Here is an example of some features and step implementations for BSON, in the same vein as the above example. This excerpt contains specifications and step implementations for deserializing objects containing individual BSON values. Here is part of a feature:
Feature: Deserialize Elements
As a user of MongoDB
In order to retrieve data from the database
The driver needs to deserialize BSON elements
<pre><code>Scenario Outline: Deserialize singleton BSON objects
Given an IO stream containing <hex_bytes>
When I deserialize the stream
Then the result should be the <type> value <value>
| hex_bytes | type | value |
| 10000000016b0026e4839ecd2a094000 | double | 3.1459 |
| 11000000026b0005000000746573740000 | string | test |
| 14000000076b0050d3409d82cb8a4fc700000100 | object_id | 50d3409d82cb8a4fc7000001 |
Here are the corresponding step definitions:
Given /^an IO stream containing ([0-9a-fA-F]+)$/ do |hex_bytes|
@io = StringIO.new([hex_bytes].pack('H*'))
<p>When /^I deserialize the stream$/ do
@document = Hash.from_bson(@io)
<p>Then /^the result should be the ((?:\S+) value (?:\S+))$/ do |value|
This example tests a few simple cases of the general problem of BSON deserialization: deserializing single objects, wrapped in a BSON document with one field (the key for the field is ‘k’). This excerpt is a good example of how feature files can be constructed, beginning with simple cases of functionality and building up toward larger, more advanced ones (later in the same file, deserialization of larger, more complex, heterogeneous BSON objects are tested). For more examples, here is the feature file the above code is taken from; and here are the step definitions.
Using the Specification with Production Drivers
The Meta Driver specification is ultimately designed to be integrated with a wide array of production drivers, in order to ensure that all conform to a common set of behaviors. In order to integrate the specifications with a new driver, step definitions in that driver’s language must be written that link the specification to calls against the driver code. While this is not an especially difficult task, it is nontrivial (differences between languages and between API designs for different drivers mean that the same steps must sometimes be implemented differently for different drivers). Ideally, the only difference between languages and drivers should be the step definitions; the feature files shared between them should be identical. In practice, some small differences may need to be tolerated, but they should be kept to an absolute minimum, since such differences reduce the usefulness of the Meta Driver by undermining its status as a single universal specification.
Binding to PyMongo
Though the vast majority of work in “porting” the Meta Driver specification to other, production drivers has yet to be done, I have begun work on binding the specification to PyMongo, the production MongoDB Python driver. I chose Python because it is largely similar to Ruby in terms of the paradigms it supports, but is different enough to create challenges and expose whatever subtle dependencies on Ruby or the Ruby implementation might exist in the Meta Driver features. Binding the specification to the production Ruby driver would be less likely to reveal such problems (though support for the production Ruby driver is an eventual goal).
So far, using the Behave implementation of Gherkin in Python, I have been able to get the BSON and wire-protocol features to run successfully on PyMongo, with only minimal changes to the specification (mostly, omitting some scenarios to compensate for functionality present in the reference driver’s API that are not supported by PyMongo; some of these can be worked around, and most of the ones that cannot are not very important).
There are frameworks for many other programming languages available. The Cucumber wiki has a partial listing, although there are many others, including (for some languages) multiple implementations, with distinct strengths and weaknesses. For example, I chose Behave over Lettuce and Freshen (two other Python Gherkin implementations) because it was better maintained and appeared to have a cleaner API for defining steps. Choosing between different Gherkin/Cucumber implementations in the target language is another important part of the process of making that language’s driver work with the Meta Driver specification.
Behavior-Driven Development is a useful way to create documentation for programs, encouraging the creation of docs that are both useful to humans and directly linked to the code they specify. Because of the polyglot nature of MongoDB drivers, and the importance of providing a consistent user experience for developers working with the drivers, BDD was an extremely good fit for the Meta Driver project. However, BDD has many other applications, across different products and different languages. Any project important enough to require accurate and up-to-date docs should consider incorporating BDD into its development methodology. While it is far from a “silver bullet” for documenting and testing code, it can streamline the process of doing both well, allowing developers to extend and maintain their code with confidence.
We believe that there are many other opportinities to deploy BDD, across the 10gen/MongoDB codebase, and hope that this is just the beginning of 10gen embracing this methodology.
A lot still remains to be done in achieving the goal of a general specification and testing across many drivers. The specification must be extended further, to more completely cover CRUD operations, as well as to support functionality for administrative commands, read preference, write concern, replica sets, and other more advanced but crucial features of MongoDB. At the same time, the specification will need to be applied to other drivers, in order to expand its reach and to learn more about the varying needs and characteristics of different drivers. The specifications will likely need to be revised during this process.
It will also be important to document the process of attaching the Meta Driver specification to a new driver. Since I have only done this once (and there is still more work to do on PyMongo), I won’t be able to document this process very completely by the time I leave; it will likely have to be put together over time. I will also try to compile some of the best-practices for writing Cucumber specifications I discovered while working on the Meta Driver itself, to help out non-driver projects that want to make use of BDD.
For information about the Cucumber platform, cukes.info, the Cucumber website, is an excellent resource, as is the Cucumber wiki. The wiki contains a listing of Cucumber/Gherkin implementations in various languages, though it is not a complete catalogue. The wiki also has a useful page on the Gherkin specification language.
This project is completely open-source, and can be found in MongoDB’s github, here. The readme file in that repository contains some useful practical information for getting started using the Meta Driver, not covered here.
I’d like to thank Mike Friedman, my mentor, as well as Gary Murakami, Tyler Brock, and the rest of the drivers team at 10gen for their help and support on this project. I’d also like to thank 10gen as a whole, for a wonderful summer internship!