Refactor the test suite

This moves the test suite to using unittest classes so that we can
control the database creation more directly. This also creates randomly
named databases so that we have fewer race conditions during test runs.

BugzId: 33294
diff --git a/test/01-index-crud-test.py b/test/01-index-crud-test.py
index be13bdd..459566b 100644
--- a/test/01-index-crud-test.py
+++ b/test/01-index-crud-test.py
@@ -11,218 +11,184 @@
 # the License.
 
 import random
-import time
 
 import mango
 
 
-def mkdb():
-    return mango.Database("127.0.0.1", "5984", "mango_test")
+class IndexCrudTests(mango.DbPerClass):
+    def test_bad_fields(self):
+        bad_fields = [
+            None,
+            True,
+            False,
+            "bing",
+            2.0,
+            {"foo": "bar"},
+            [{"foo": 2}],
+            [{"foo": "asc", "bar": "desc"}],
+            [{"foo": "asc"}, {"bar": "desc"}]
+        ]
+        for fields in bad_fields:
+            try:
+                self.db.create_index(fields)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad create index")
 
+    def test_bad_types(self):
+        bad_types = [
+            None,
+            True,
+            False,
+            1.5,
+            "foo", # Future support
+            "geo", # Future support
+            {"foo": "bar"},
+            ["baz", 3.0]
+        ]
+        for bt in bad_types:
+            try:
+                self.db.create_index(["foo"], idx_type=bt)
+            except Exception, e:
+                assert e.response.status_code == 400, (bt, e.response.status_code)
+            else:
+                raise AssertionError("bad create index")
 
-def setup():
-    db = mkdb()
-    db.recreate()
-    time.sleep(1)
+    def test_bad_names(self):
+        bad_names = [
+            True,
+            False,
+            1.5,
+            {"foo": "bar"},
+            [None, False]
+        ]
+        for bn in bad_names:
+            try:
+                self.db.create_index(["foo"], name=bn)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad create index")
+            try:
+                self.db.create_index(["foo"], ddoc=bn)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad create index")
 
-
-def test_bad_fields():
-    db = mkdb()
-    bad_fields = [
-        None,
-        True,
-        False,
-        "bing",
-        2.0,
-        {"foo": "bar"},
-        [{"foo": 2}],
-        [{"foo": "asc", "bar": "desc"}],
-        [{"foo": "asc"}, {"bar": "desc"}]
-    ]
-    for fields in bad_fields:
-        try:
-            db.create_index(fields)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad create index")
-
-
-def test_bad_types():
-    db = mkdb()
-    bad_types = [
-        None,
-        True,
-        False,
-        1.5,
-        "foo",
-        "text", # Future support
-        "geo", # Future support
-        {"foo": "bar"},
-        ["baz", 3.0]
-    ]
-    for bt in bad_types:
-        try:
-            db.create_index(["foo"], idx_type=bt)
-        except Exception, e:
-            assert e.response.status_code == 400, (bt, e.response.status_code)
-        else:
-            raise AssertionError("bad create index")
-
-
-def test_bad_names():
-    db = mkdb()
-    bad_names = [
-        True,
-        False,
-        1.5,
-        {"foo": "bar"},
-        [None, False]
-    ]
-    for bn in bad_names:
-        try:
-            db.create_index(["foo"], name=bn)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad create index")
-        try:
-            db.create_index(["foo"], ddoc=bn)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad create index")
-
-
-def test_create_idx_01():
-    db = mkdb()
-    fields = ["foo", "bar"]
-    ret = db.create_index(fields, name="idx_01")
-    assert ret is True
-    for idx in db.list_indexes():
-        if idx["name"] != "idx_01":
-            continue
-        assert idx["def"]["fields"] == [{"foo": "asc"}, {"bar": "asc"}]
-        return
-    raise AssertionError("index not created")
-
-
-def test_create_idx_01_exists():
-    db = mkdb()
-    fields = ["foo", "bar"]
-    ret = db.create_index(fields, name="idx_01")
-    assert ret is False
-
-
-def test_create_idx_02():
-    db = mkdb()
-    fields = ["baz", "foo"]
-    ret = db.create_index(fields, name="idx_02")
-    assert ret is True
-    for idx in db.list_indexes():
-        if idx["name"] != "idx_02":
-            continue
-        assert idx["def"]["fields"] == [{"baz": "asc"}, {"foo": "asc"}]
-        return
-    raise AssertionError("index not created")
-
-
-def test_read_idx_doc():
-    db = mkdb()
-    for idx in db.list_indexes():
-        if idx["type"] == "special":
-            continue
-        ddocid = idx["ddoc"]
-        doc = db.open_doc(ddocid)
-        assert doc["_id"] == ddocid
-        info = db.ddoc_info(ddocid)
-        assert info["name"] == ddocid
-
-
-def test_delete_idx_escaped():
-    db = mkdb()
-    pre_indexes = db.list_indexes()
-    ret = db.create_index(["bing"], name="idx_del_1")
-    assert ret is True
-    for idx in db.list_indexes():
-        if idx["name"] != "idx_del_1":
-            continue
-        assert idx["def"]["fields"] == [{"bing": "asc"}]
-        db.delete_index(idx["ddoc"].replace("/", "%2F"), idx["name"])
-    post_indexes = db.list_indexes()
-    assert pre_indexes == post_indexes
-
-
-def test_delete_idx_unescaped():
-    db = mkdb()
-    pre_indexes = db.list_indexes()
-    ret = db.create_index(["bing"], name="idx_del_2")
-    assert ret is True
-    for idx in db.list_indexes():
-        if idx["name"] != "idx_del_2":
-            continue
-        assert idx["def"]["fields"] == [{"bing": "asc"}]
-        db.delete_index(idx["ddoc"], idx["name"])
-    post_indexes = db.list_indexes()
-    assert pre_indexes == post_indexes
-
-
-def test_delete_idx_no_design():
-    db = mkdb()
-    pre_indexes = db.list_indexes()
-    ret = db.create_index(["bing"], name="idx_del_3")
-    assert ret is True
-    for idx in db.list_indexes():
-        if idx["name"] != "idx_del_3":
-            continue
-        assert idx["def"]["fields"] == [{"bing": "asc"}]
-        db.delete_index(idx["ddoc"].split("/")[-1], idx["name"])
-    post_indexes = db.list_indexes()
-    assert pre_indexes == post_indexes
-
-
-def test_recreate_index():
-    db = mkdb()
-    pre_indexes = db.list_indexes()
-    for i in range(5):
-        ret = db.create_index(["bing"], name="idx_recreate")
+    def test_create_idx_01(self):
+        fields = ["foo", "bar"]
+        ret = self.db.create_index(fields, name="idx_01")
         assert ret is True
-        for idx in db.list_indexes():
-            if idx["name"] != "idx_recreate":
+        for idx in self.db.list_indexes():
+            if idx["name"] != "idx_01":
+                continue
+            assert idx["def"]["fields"] == [{"foo": "asc"}, {"bar": "asc"}]
+            return
+        raise AssertionError("index not created")
+
+    def test_create_idx_01_exists(self):
+        fields = ["foo", "bar"]
+        ret = self.db.create_index(fields, name="idx_01")
+        assert ret is False
+
+    def test_create_idx_02(self):
+        fields = ["baz", "foo"]
+        ret = self.db.create_index(fields, name="idx_02")
+        assert ret is True
+        for idx in self.db.list_indexes():
+            if idx["name"] != "idx_02":
+                continue
+            assert idx["def"]["fields"] == [{"baz": "asc"}, {"foo": "asc"}]
+            return
+        raise AssertionError("index not created")
+
+    def test_read_idx_doc(self):
+        for idx in self.db.list_indexes():
+            if idx["type"] == "special":
+                continue
+            ddocid = idx["ddoc"]
+            doc = self.db.open_doc(ddocid)
+            assert doc["_id"] == ddocid
+            info = self.db.ddoc_info(ddocid)
+            assert info["name"] == ddocid
+
+    def test_delete_idx_escaped(self):
+        pre_indexes = self.db.list_indexes()
+        ret = self.db.create_index(["bing"], name="idx_del_1")
+        assert ret is True
+        for idx in self.db.list_indexes():
+            if idx["name"] != "idx_del_1":
                 continue
             assert idx["def"]["fields"] == [{"bing": "asc"}]
-            db.delete_index(idx["ddoc"], idx["name"])
-            break
-        post_indexes = db.list_indexes()
+            self.db.delete_index(idx["ddoc"].replace("/", "%2F"), idx["name"])
+        post_indexes = self.db.list_indexes()
         assert pre_indexes == post_indexes
 
+    def test_delete_idx_unescaped(self):
+        pre_indexes = self.db.list_indexes()
+        ret = self.db.create_index(["bing"], name="idx_del_2")
+        assert ret is True
+        for idx in self.db.list_indexes():
+            if idx["name"] != "idx_del_2":
+                continue
+            assert idx["def"]["fields"] == [{"bing": "asc"}]
+            self.db.delete_index(idx["ddoc"], idx["name"])
+        post_indexes = self.db.list_indexes()
+        assert pre_indexes == post_indexes
 
-def test_delete_missing():
-    db = mkdb()
+    def test_delete_idx_no_design(self):
+        pre_indexes = self.db.list_indexes()
+        ret = self.db.create_index(["bing"], name="idx_del_3")
+        assert ret is True
+        for idx in self.db.list_indexes():
+            if idx["name"] != "idx_del_3":
+                continue
+            assert idx["def"]["fields"] == [{"bing": "asc"}]
+            self.db.delete_index(idx["ddoc"].split("/")[-1], idx["name"])
+        post_indexes = self.db.list_indexes()
+        assert pre_indexes == post_indexes
 
-    # Missing design doc
-    try:
-        db.delete_index("this_is_not_a_design_doc_id", "foo")
-    except Exception, e:
-        assert e.response.status_code == 404
-    else:
-        raise AssertionError("bad index delete")
+    def test_recreate_index(self):
+        pre_indexes = self.db.list_indexes()
+        for i in range(5):
+            ret = self.db.create_index(["bing"], name="idx_recreate")
+            assert ret is True
+            for idx in self.db.list_indexes():
+                if idx["name"] != "idx_recreate":
+                    continue
+                assert idx["def"]["fields"] == [{"bing": "asc"}]
+                self.db.delete_index(idx["ddoc"], idx["name"])
+                break
+            post_indexes = self.db.list_indexes()
+            assert pre_indexes == post_indexes
 
-    # Missing view name
-    indexes = db.list_indexes()
-    idx = random.choice([idx for idx in indexes if idx["type"] != "special"])
-    ddocid = idx["ddoc"].split("/")[-1]
-    try:
-        db.delete_index(ddocid, "this_is_not_an_index_name")
-    except Exception, e:
-        assert e.response.status_code == 404
-    else:
-        raise AssertionError("bad index delete")
+    def test_delete_misisng(self):
+        # Missing design doc
+        try:
+            self.db.delete_index("this_is_not_a_design_doc_id", "foo")
+        except Exception, e:
+            assert e.response.status_code == 404
+        else:
+            raise AssertionError("bad index delete")
 
-    # Bad view type
-    try:
-        db.delete_index(ddocid, idx["name"], idx_type="not_a_real_type")
-    except Exception, e:
-        assert e.response.status_code == 404
-    else:
-        raise AssertionError("bad index delete")
+        # Missing view name
+        indexes = self.db.list_indexes()
+        not_special = [idx for idx in indexes if idx["type"] != "special"]
+        idx = random.choice(not_special)
+        ddocid = idx["ddoc"].split("/")[-1]
+        try:
+            self.db.delete_index(ddocid, "this_is_not_an_index_name")
+        except Exception, e:
+            assert e.response.status_code == 404
+        else:
+            raise AssertionError("bad index delete")
+
+        # Bad view type
+        try:
+            self.db.delete_index(ddocid, idx["name"], idx_type="not_a_real_type")
+        except Exception, e:
+            assert e.response.status_code == 404
+        else:
+            raise AssertionError("bad index delete")
diff --git a/test/02-basic-find-test.py b/test/02-basic-find-test.py
index f6b0610..58029ed 100644
--- a/test/02-basic-find-test.py
+++ b/test/02-basic-find-test.py
@@ -11,286 +11,275 @@
 # the License.
 
 # -*- coding: latin-1 -*-
-import user_docs
 
-def setup():
-    user_docs.create_db_and_indexes()
+import mango
 
 
-def test_bad_selector():
-    db = user_docs.mkdb()
-    bad_selectors = [
-        None,
-        True,
-        False,
-        1.0,
-        "foobarbaz",
-        {"foo":{"$not_an_op": 2}},
-        {"$gt":2},
-        [None, "bing"]
-    ]
-    for bs in bad_selectors:
-        try:
-            db.find(bs)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+class BasicFindTests(mango.UserDocsTests):
 
+    def test_bad_selector(self):
+        bad_selectors = [
+            None,
+            True,
+            False,
+            1.0,
+            "foobarbaz",
+            {"foo":{"$not_an_op": 2}},
+            {"$gt":2},
+            [None, "bing"]
+        ]
+        for bs in bad_selectors:
+            try:
+                self.db.find(bs)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
-def test_bad_limit():
-    db = user_docs.mkdb()
-    bad_limits = [
-        None,
-        True,
-        False,
-        -1,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2]
-    ],
-    for bl in bad_limits:
-        try:
-            db.find({"int":{"$gt":2}}, limit=bl)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+    def test_bad_limit(self):
+        bad_limits = [
+            None,
+            True,
+            False,
+            -1,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2]
+        ],
+        for bl in bad_limits:
+            try:
+                self.db.find({"int":{"$gt":2}}, limit=bl)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
+    def test_bad_skip(self):
+        bad_skips = [
+            None,
+            True,
+            False,
+            -3,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2]
+        ],
+        for bs in bad_skips:
+            try:
+                self.db.find({"int":{"$gt":2}}, skip=bs)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
-def test_bad_skip():
-    db = user_docs.mkdb()
-    bad_skips = [
-        None,
-        True,
-        False,
-        -3,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2]
-    ],
-    for bs in bad_skips:
-        try:
-            db.find({"int":{"$gt":2}}, skip=bs)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+    def test_bad_sort(self):
+        bad_sorts = [
+            None,
+            True,
+            False,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2],
+            [{"foo":"asc", "bar": "asc"}],
+            [{"foo":"asc"}, {"bar":"desc"}],
+        ],
+        for bs in bad_sorts:
+            try:
+                self.db.find({"int":{"$gt":2}}, sort=bs)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
+    def test_bad_fields(self):
+        bad_fields = [
+            None,
+            True,
+            False,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2],
+            [[]],
+            ["foo", 2.0],
+        ],
+        for bf in bad_fields:
+            try:
+                self.db.find({"int":{"$gt":2}}, fields=bf)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
-def test_bad_sort():
-    db = user_docs.mkdb()
-    bad_sorts = [
-        None,
-        True,
-        False,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2],
-        [{"foo":"asc", "bar": "asc"}],
-        [{"foo":"asc"}, {"bar":"desc"}],
-    ],
-    for bs in bad_sorts:
-        try:
-            db.find({"int":{"$gt":2}}, sort=bs)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+    def test_bad_r(self):
+        bad_rs = [
+            None,
+            True,
+            False,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2],
+        ],
+        for br in bad_rs:
+            try:
+                self.db.find({"int":{"$gt":2}}, r=br)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
+    def test_bad_conflicts(self):
+        bad_conflicts = [
+            None,
+            1.2,
+            "no limit!",
+            {"foo": "bar"},
+            [2],
+        ],
+        for bc in bad_conflicts:
+            try:
+                self.db.find({"int":{"$gt":2}}, conflicts=bc)
+            except Exception, e:
+                assert e.response.status_code == 400
+            else:
+                raise AssertionError("bad find")
 
-def test_bad_fields():
-    db = user_docs.mkdb()
-    bad_fields = [
-        None,
-        True,
-        False,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2],
-        [[]],
-        ["foo", 2.0],
-    ],
-    for bf in bad_fields:
-        try:
-            db.find({"int":{"$gt":2}}, fields=bf)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+    def test_simple_find(self):
+        docs = self.db.find({"age": {"$lt": 35}})
+        assert len(docs) == 3
+        assert docs[0]["user_id"] == 9
+        assert docs[1]["user_id"] == 1
+        assert docs[2]["user_id"] == 7
 
+    def test_multi_cond_and(self):
+        docs = self.db.find({"manager": True, "location.city": "Longbranch"})
+        assert len(docs) == 1
+        assert docs[0]["user_id"] == 7
 
-def test_bad_r():
-    db = user_docs.mkdb()
-    bad_rs = [
-        None,
-        True,
-        False,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2],
-    ],
-    for br in bad_rs:
-        try:
-            db.find({"int":{"$gt":2}}, r=br)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
+    def test_multi_cond_or(self):
+        docs = self.db.find({
+                "$and":[
+                    {"age":{"$gte": 75}},
+                    {"$or": [
+                        {"name.first": "Mathis"},
+                        {"name.first": "Whitley"}
+                    ]}
+                ]
+            })
+        assert len(docs) == 2
+        assert docs[0]["user_id"] == 11
+        assert docs[1]["user_id"] == 13
 
-
-def test_bad_conflicts():
-    db = user_docs.mkdb()
-    bad_conflicts = [
-        None,
-        1.2,
-        "no limit!",
-        {"foo": "bar"},
-        [2],
-    ],
-    for bc in bad_conflicts:
-        try:
-            db.find({"int":{"$gt":2}}, conflicts=bc)
-        except Exception, e:
-            assert e.response.status_code == 400
-        else:
-            raise AssertionError("bad find")
-
-
-def test_simple_find():
-    db = user_docs.mkdb()
-    docs = db.find({"age": {"$lt": 35}})
-    assert len(docs) == 3
-    assert docs[0]["user_id"] == 9
-    assert docs[1]["user_id"] == 1
-    assert docs[2]["user_id"] == 7
-
-
-def test_multi_cond_and():
-    db = user_docs.mkdb()
-    docs = db.find({"manager": True, "location.city": "Longbranch"})
-    assert len(docs) == 1
-    assert docs[0]["user_id"] == 7
-
-
-def test_multi_cond_or():
-    db = user_docs.mkdb()
-    docs = db.find({
-            "$and":[
-                {"age":{"$gte": 75}},
-                {"$or": [
-                    {"name.first": "Mathis"},
-                    {"name.first": "Whitley"}
-                ]}
-            ]
+    def test_multi_col_idx(self):
+        docs = self.db.find({
+            "location.state": {"$and": [
+                {"$gt": "Hawaii"},
+                {"$lt": "Maine"}
+            ]},
+            "location.city": {"$lt": "Longbranch"}
         })
-    assert len(docs) == 2
-    assert docs[0]["user_id"] == 11
-    assert docs[1]["user_id"] == 13
+        assert len(docs) == 1
+        assert docs[0]["user_id"] == 6
 
+    def test_missing_not_indexed(self):
+        docs = self.db.find({"favorites.3": "C"})
+        assert len(docs) == 1
+        assert docs[0]["user_id"] == 6
 
-def test_multi_col_idx():
-    db = user_docs.mkdb()
-    docs = db.find({
-        "location.state": {"$and": [
-            {"$gt": "Hawaii"},
-            {"$lt": "Maine"}
-        ]},
-        "location.city": {"$lt": "Longbranch"}
-    })
-    assert len(docs) == 1
-    assert docs[0]["user_id"] == 6
+        docs = self.db.find({"favorites.3": None})
+        assert len(docs) == 0
 
+        docs = self.db.find({"twitter": {"$gt": None}})
+        assert len(docs) == 4
+        assert docs[0]["user_id"] == 1
+        assert docs[1]["user_id"] == 4
+        assert docs[2]["user_id"] == 0
+        assert docs[3]["user_id"] == 13
 
-def test_missing_not_indexed():
-    db = user_docs.mkdb()
-    docs = db.find({"favorites.3": "C"})
-    assert len(docs) == 2
-    assert docs[0]["user_id"] == 8
-    assert docs[1]["user_id"] == 6
-
-    docs = db.find({"favorites.3": None})
-    assert len(docs) == 0
-
-    docs = db.find({"twitter": {"$gt": None}})
-    assert len(docs) == 4
-    assert docs[0]["user_id"] == 1
-    assert docs[1]["user_id"] == 4
-    assert docs[2]["user_id"] == 0
-    assert docs[3]["user_id"] == 13
-
-def test_dot_key():
-    db = user_docs.mkdb()
-    fields = ["title", "dot\\.key", "none.dot"]
-    docs = db.find({"type": "complex_key"}, fields = fields)
-    assert len(docs) == 4
-    assert docs[1].has_key("dot.key")
-    assert docs[1]["dot.key"] == "dot's value"
-    assert docs[1].has_key("none")
-    assert docs[1]["none"]["dot"] == "none dot's value"
-
-def test_peso_key():
-    db = user_docs.mkdb()
-    fields = ["title", "$key", "deep.$key"]
-    docs = db.find({"type": "complex_key"}, fields = fields)
-    assert len(docs) == 4
-    assert docs[2].has_key("$key")
-    assert docs[2]["$key"] == "peso"
-    assert docs[2].has_key("deep")
-    assert docs[2]["deep"]["$key"] == "deep peso"
-
-def test_unicode_key():
-    db = user_docs.mkdb()
-    docs = db.find({"type": "complex_key"}, fields = ["title", ""])
-    assert len(docs) == 4
-    # note:  == \uf8ff
-    assert docs[3].has_key(u'\uf8ff')
-    assert docs[3][u'\uf8ff'] == "apple"
-
-def test_limit():
-    db = user_docs.mkdb()
-    docs = db.find({"age": {"$gt": 0}})
-    assert len(docs) == 15
-    for l in [0, 1, 5, 14]:
-        docs = db.find({"age": {"$gt": 0}}, limit=l)
-        assert len(docs) == l
-
-def test_skip():
-    db = user_docs.mkdb()
-    docs = db.find({"age": {"$gt": 0}})
-    assert len(docs) == 15
-    for s in [0, 1, 5, 14]:
-        docs = db.find({"age": {"$gt": 0}}, skip=s)
-        assert len(docs) == (15 - s)
-
-
-def test_sort():
-    db = user_docs.mkdb()
-
-    docs1 = db.find({"age": {"$gt": 0}}, sort=[{"age":"asc"}])
-    docs2 = list(sorted(docs1, key=lambda d: d["age"]))
-    assert docs1 is not docs2 and docs1 == docs2
-
-    docs1 = db.find({"age": {"$gt": 0}}, sort=[{"age":"desc"}])
-    docs2 = list(reversed(sorted(docs1, key=lambda d: d["age"])))
-    assert docs1 is not docs2 and docs1 == docs2
-
-
-def test_fields():
-    db = user_docs.mkdb()
-    docs = db.find({"age": {"$gt": 0}}, fields=["user_id", "location.address"])
-    for d in docs:
-        assert sorted(d.keys()) == ["location", "user_id"]
-        assert sorted(d["location"].keys()) == ["address"]
-
-
-def test_r():
-    db = user_docs.mkdb()
-    for r in [1, 2, 3]:
-        docs = db.find({"age": {"$gt": 0}}, r=r)
+    def test_limit(self):
+        docs = self.db.find({"age": {"$gt": 0}})
         assert len(docs) == 15
+        for l in [0, 1, 5, 14]:
+            docs = self.db.find({"age": {"$gt": 0}}, limit=l)
+            assert len(docs) == l
+
+    def test_skip(self):
+        docs = self.db.find({"age": {"$gt": 0}})
+        assert len(docs) == 15
+        for s in [0, 1, 5, 14]:
+            docs = self.db.find({"age": {"$gt": 0}}, skip=s)
+            assert len(docs) == (15 - s)
+
+    def test_sort(self):
+        docs1 = self.db.find({"age": {"$gt": 0}}, sort=[{"age":"asc"}])
+        docs2 = list(sorted(docs1, key=lambda d: d["age"]))
+        assert docs1 is not docs2 and docs1 == docs2
+
+        docs1 = self.db.find({"age": {"$gt": 0}}, sort=[{"age":"desc"}])
+        docs2 = list(reversed(sorted(docs1, key=lambda d: d["age"])))
+        assert docs1 is not docs2 and docs1 == docs2
+
+    def test_fields(self):
+        selector = {"age": {"$gt": 0}}
+        docs = self.db.find(selector, fields=["user_id", "location.address"])
+        for d in docs:
+            assert sorted(d.keys()) == ["location", "user_id"]
+            assert sorted(d["location"].keys()) == ["address"]
+
+    def test_r(self):
+        for r in [1, 2, 3]:
+            docs = self.db.find({"age": {"$gt": 0}}, r=r)
+            assert len(docs) == 15
+
+    def test_dot_key(self):
+        fields = ["title", "dot\\.key", "none.dot"]
+        docs = self.db.find({"type": "complex_key"}, fields = fields)
+        assert len(docs) == 4
+        assert docs[1].has_key("dot.key")
+        assert docs[1]["dot.key"] == "dot's value"
+        assert docs[1].has_key("none")
+        assert docs[1]["none"]["dot"] == "none dot's value"
+
+    def test_peso_key(self):
+        fields = ["title", "$key", "deep.$key"]
+        docs = self.db.find({"type": "complex_key"}, fields = fields)
+        assert len(docs) == 4
+        assert docs[2].has_key("$key")
+        assert docs[2]["$key"] == "peso"
+        assert docs[2].has_key("deep")
+        assert docs[2]["deep"]["$key"] == "deep peso"
+
+    def test_unicode_key(self):
+        docs = self.db.find({"type": "complex_key"}, fields = ["title", ""])
+        assert len(docs) == 4
+        # note:  == \uf8ff
+        assert docs[3].has_key(u'\uf8ff')
+        assert docs[3][u'\uf8ff'] == "apple"
+
+    def test_empty(self):
+        try:
+            self.db.find({})
+        except Exception, e:
+            assert e.response.status_code == 400
+        else:
+            raise AssertionError("bad find")
+
+    def test_empty_subsel(self):
+        docs = self.db.find({
+                "_id": {"$gt": None},
+                "location": {}
+            })
+        assert len(docs) == 0
+
+    def test_empty_subsel_match(self):
+        self.db.save_docs([{"user_id": "eo", "empty_obj": {}}])
+        docs = self.db.find({
+                "_id": {"$gt": None},
+                "empty_obj": {}
+            })
+        assert len(docs) == 1
+        assert docs[0]["user_id"] == "eo"
diff --git a/test/03-operator-test.py b/test/03-operator-test.py
index bd0500f..27418a6 100644
--- a/test/03-operator-test.py
+++ b/test/03-operator-test.py
@@ -10,83 +10,73 @@
 # License for the specific language governing permissions and limitations under
 # the License.
 
-import user_docs
+import mango
 
 
-def setup():
-    user_docs.create_db_and_indexes()
+class OperatorTests(mango.UserDocsTests):
 
+    def test_all(self):
+        docs = self.db.find({
+                "manager": True,
+                "favorites": {"$all": ["Lisp", "Python"]}
+            })
+        print docs
+        assert len(docs) == 4
+        assert docs[0]["user_id"] == 2
+        assert docs[1]["user_id"] == 12
+        assert docs[2]["user_id"] == 9
+        assert docs[3]["user_id"] == 14
 
-def test_all():
-    db = user_docs.mkdb()
-    docs = db.find({
-            "manager": True,
-            "favorites": {"$all": ["Lisp", "Python"]}
-        })
-    assert len(docs) == 3
-    assert docs[0]["user_id"] == 2
-    assert docs[1]["user_id"] == 12
-    assert docs[2]["user_id"] == 9
+    def test_all_non_array(self):
+        docs = self.db.find({
+                "manager": True,
+                "location": {"$all": ["Ohai"]}
+            })
+        assert len(docs) == 0
 
-
-def test_all_non_array():
-    db = user_docs.mkdb()
-    docs = db.find({
-            "manager": True,
-            "location": {"$all": ["Ohai"]}
-        })
-    assert len(docs) == 0
-
-
-def test_elem_match():
-    db = user_docs.mkdb()
-    emdocs = [
-        {
-            "user_id": "a",
-            "bang": [{
-                "foo": 1,
-                "bar": 2
-            }]
-        },
-        {
-            "user_id": "b",
-            "bang": [{
-                "foo": 2,
+    def test_elem_match(self):
+        emdocs = [
+            {
+                "user_id": "a",
+                "bang": [{
+                    "foo": 1,
+                    "bar": 2
+                }]
+            },
+            {
+                "user_id": "b",
+                "bang": [{
+                    "foo": 2,
+                    "bam": True
+                }]
+            }
+        ]
+        self.db.save_docs(emdocs, w=3)
+        docs = self.db.find({
+            "_id": {"$gt": None},
+            "bang": {"$elemMatch": {
+                "foo": {"$gte": 1},
                 "bam": True
-            }]
-        }
-    ]
-    db.save_docs(emdocs)
-    docs = db.find({
-        "_id": {"$gt": None},
-        "bang": {"$elemMatch": {
-            "foo": {"$gte": 1},
-            "bam": True
-        }}
-    })
-    assert len(docs) == 1
-    assert docs[0]["user_id"] == "b"
-
-
-def test_regex():
-    db = user_docs.mkdb()
-
-    docs = db.find({
-            "age": {"$gt": 40},
-            "location.state": {"$regex": "(?i)new.*"}
+            }}
         })
-    assert len(docs) == 2
-    assert docs[0]["user_id"] == 2
-    assert docs[1]["user_id"] == 10
+        print docs
+        assert len(docs) == 1
+        assert docs[0]["user_id"] == "b"
 
+    def test_in_operator_array(self):
+        docs = self.db.find({
+                "manager": True,
+                "favorites": {"$in": ["Ruby", "Python"]}
+            })
+        assert len(docs) == 7
+        assert docs[0]["user_id"] == 2
+        assert docs[1]["user_id"] == 12
 
-def test_in_operator_array():
-    db = user_docs.mkdb()
-
-    docs = db.find({
-            "manager": True,
-            "favorites": {"$in": ["Ruby", "Python"]}
-        })
-    assert len(docs) == 7
-    assert docs[0]["user_id"] == 2
-    assert docs[1]["user_id"] == 12
+    def test_regex(self):
+        docs = self.db.find({
+                "age": {"$gt": 40},
+                "location.state": {"$regex": "(?i)new.*"}
+            })
+        assert len(docs) == 2
+        assert docs[0]["user_id"] == 2
+        assert docs[1]["user_id"] == 10
diff --git a/test/04-empty-selectors-test.py b/test/04-empty-selectors-test.py
deleted file mode 100644
index 08bb03e..0000000
--- a/test/04-empty-selectors-test.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Licensed under the Apache License, Version 2.0 (the "License"); you may not
-# use this file except in compliance with the License. You may obtain a copy of
-# the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations under
-# the License.
-
-import user_docs
-
-
-def setup():
-    user_docs.create_db_and_indexes()
-
-
-def test_empty():
-    db = user_docs.mkdb()
-    try:
-        db.find({})
-    except Exception, e:
-        assert e.response.status_code == 400
-    else:
-        raise AssertionError("bad find")
-
-
-def test_empty_subsel():
-    db = user_docs.mkdb()
-    docs = db.find({
-            "_id": {"$gt": None},
-            "location": {}
-        })
-    assert len(docs) == 0
-
-
-def test_empty_subsel_match():
-    db = user_docs.mkdb()
-    db.save_docs([{"user_id": "eo", "empty_obj": {}}])
-    docs = db.find({
-            "_id": {"$gt": None},
-            "empty_obj": {}
-        })
-    assert len(docs) == 1
-    assert docs[0]["user_id"] == "eo"
diff --git a/test/mango.py b/test/mango.py
index 3163cf5..c28f280 100644
--- a/test/mango.py
+++ b/test/mango.py
@@ -12,9 +12,17 @@
 
 import json
 import time
+import unittest
+import uuid
 
 import requests
 
+import user_docs
+
+
+def random_db_name():
+    return "mango_test_" + uuid.uuid4().hex
+
 
 class Database(object):
     def __init__(self, host, port, dbname, auth=None):
@@ -35,10 +43,10 @@
             parts = [parts]
         return "/".join([self.url] + parts)
 
-    def create(self):
+    def create(self, q=1, n=3):
         r = self.sess.get(self.url)
         if r.status_code == 404:
-            r = self.sess.put(self.url)
+            r = self.sess.put(self.url, params={"q":q, "n": n})
             r.raise_for_status()
 
     def delete(self):
@@ -53,9 +61,9 @@
     def save_doc(self, doc):
         self.save_docs([doc])
 
-    def save_docs(self, docs):
+    def save_docs(self, docs, **kwargs):
         body = json.dumps({"docs": docs})
-        r = self.sess.post(self.path("_bulk_docs"), data=body)
+        r = self.sess.post(self.path("_bulk_docs"), data=body, params=kwargs)
         r.raise_for_status()
         for doc, result in zip(docs, r.json()):
             doc["_id"] = result["id"]
@@ -124,3 +132,46 @@
             return results[0]
         else:
             return None
+
+
+class DbPerClass(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(klass):
+        klass.db = Database("127.0.0.1", "5984", random_db_name())
+        klass.db.create(q=1, n=3)
+
+    def setUp(self):
+        self.db = self.__class__.db
+
+
+class UserDocsTests(DbPerClass):
+
+    @classmethod
+    def setUpClass(klass):
+        super(UserDocsTests, klass).setUpClass()
+        user_docs.setup(klass.db)
+
+
+class UserDocsTextTests(DbPerClass):
+
+    DEFAULT_FIELD = None
+    FIELDS = None
+
+    @classmethod
+    def setUpClass(klass):
+        super(UserDocsTextTests, klass).setUpClass()
+        user_docs.setup(
+                klass.db,
+                index_type="text",
+                default_field=klass.DEFAULT_FIELD,
+                fields=klass.FIELDS
+            )
+
+
+class FriendDocsTextTests(DbPerClass):
+
+    @classmethod
+    def setUpClass(klass):
+        super(FriendDocsTextTests, klass).setUpClass()
+        friend_docs.setup(klass.db, index_type="text")
diff --git a/test/user_docs.py b/test/user_docs.py
index 1cccb58..465370a 100644
--- a/test/user_docs.py
+++ b/test/user_docs.py
@@ -9,8 +9,8 @@
 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 # License for the specific language governing permissions and limitations under
 # the License.
-
-# -*- coding: latin-1 -*-
+#
+# -*- coding: utf-8 -*-
 """
 Generated with http://www.json-generator.com/
 
@@ -50,20 +50,20 @@
 ]
 """
 
+
 import copy
-import time
-
-import mango
 
 
-def mkdb():
-    return mango.Database("127.0.0.1", "5984", "mango_test")
-
-
-def create_db_and_indexes():
-    db = mkdb()
+def setup(db, index_type="view", **kwargs):
     db.recreate()
     db.save_docs(copy.deepcopy(DOCS))
+    if index_type == "view":
+        add_view_indexes(db, kwargs)
+    elif index_type == "text":
+        add_text_indexes(db, kwargs)
+
+
+def add_view_indexes(db, kwargs):
     indexes = [
         ["user_id"],
         ["name.last", "name.first"],
@@ -85,6 +85,10 @@
         assert db.create_index(idx) is True
 
 
+def add_text_indexes(db, kwargs):
+    db.create_text_index(**kwargs)
+
+
 DOCS = [
     {
         "_id": "71562648-6acb-42bc-a182-df6b1f005b09",
@@ -110,7 +114,8 @@
             "Ruby",
             "C",
             "Python"
-        ]
+        ],
+        "test" : [{"a":1}, {"b":2}]
     },
     {
         "_id": "12a2800c-4fe2-45a8-8d78-c084f4e242a9",
@@ -136,8 +141,9 @@
             "Ruby",
             "Python",
             "C",
-            "Python"
-        ]
+            {"Versions": {"Alpha": "Beta"}}
+        ],
+        "test" : [{"a":1, "b":2}]
     },
     {
         "_id": "48ca0455-8bd0-473f-9ae2-459e42e3edd1",
@@ -162,7 +168,8 @@
             "Lisp",
             "Python",
             "Erlang"
-        ]
+        ],
+        "test_in": {"val1" : 1, "val2": "val2"}
     },
     {
         "_id": "0461444c-e60a-457d-a4bb-b8d811853f21",
@@ -183,7 +190,11 @@
         "company": "Tasmania",
         "email": "madelynsoto@tasmania.com",
         "manager": True,
-        "favorites": [
+        "favorites": [[
+                "Lisp",
+                "Erlang",
+                "Python"
+            ],
             "Erlang",
             "C",
             "Erlang"
@@ -291,7 +302,9 @@
             "Ruby",
             "Ruby",
             "Erlang"
-        ]
+        ],
+        "exists_field" : "should_exist1"
+
     },
     {
         "_id": "6c0afcf1-e57e-421d-a03d-0c0717ebf843",
@@ -312,12 +325,8 @@
         "company": "Globoil",
         "email": "jamesmcdaniel@globoil.com",
         "manager": True,
-        "favorites": [
-            "Lisp",
-            "C",
-            "Ruby",
-            "C"
-        ]
+        "favorites": None,
+        "exists_field" : "should_exist2"
     },
     {
         "_id": "954272af-d5ed-4039-a5eb-8ed57e9def01",
@@ -342,7 +351,8 @@
             "Lisp",
             "Erlang",
             "Python"
-        ]
+        ],
+        "exists_array" : ["should", "exist", "array1"]
     },
     {
         "_id": "e900001d-bc48-48a6-9b1a-ac9a1f5d1a03",
@@ -366,7 +376,8 @@
         "favorites": [
             "Erlang",
             "Erlang"
-        ]
+        ],
+        "exists_array" : ["should", "exist", "array2"]
     },
     {
         "_id": "b06aadcf-cd0f-4ca6-9f7e-2c993e48d4c4",
@@ -393,7 +404,8 @@
             "C",
             "C++",
             "C++"
-        ]
+        ],
+        "exists_object" : {"should": "object"}
     },
     {
         "_id": "5b61abc1-a3d3-4092-b9d7-ced90e675536",
@@ -418,7 +430,8 @@
             "C",
             "Python",
             "Lisp"
-        ]
+        ],
+        "exists_object" : {"another": "object"}
     },
     {
         "_id": "b1e70402-8add-4068-af8f-b4f3d0feb049",
@@ -436,7 +449,7 @@
                 "number": 8766
             }
         },
-        "company": "Fangold",
+        "company": None,
         "email": "whitleyharvey@fangold.com",
         "manager": False,
         "twitter": "@whitleyharvey",
@@ -444,11 +457,16 @@
             "C",
             "Ruby",
             "Ruby"
-        ]
+        ],
+        "utf8-1[]:string" : "string",
+        "utf8-2[]:boolean[]" : True,
+        "utf8-3[]:number" : 9,
+        "utf8-3[]:null" : None
     },
     {
         "_id": "c78c529f-0b07-4947-90a6-d6b7ca81da62",
         "user_id": 14,
+        "«ταБЬℓσ»" : "utf-8",
         "name": {
             "first": "Faith",
             "last": "Hess"
@@ -466,7 +484,8 @@
         "email": "faithhess@pharmex.com",
         "manager": True,
         "favorites": [
-            "Lisp",
+            "Erlang",
+            "Python",
             "Lisp"
         ]
     },