Repository: cloudant/python-cloudant

Issue:
Document fetch fails when uses its default json decoder and module simplejson is present
## Bug Description
`Document.fetch()` fails when it uses its default json decoder and `simplejson` is in the environment. 

### Steps to reproduce and code sample to demonstrate the issue
To reproduce, just run a simple document fetch like this having `simplejson` package in the environment:
```
from cloudant import cloudant
from cloudant.database import CloudantDatabase
from cloudant.document import Document

def fetch_by_id(usr, pwd,url,db_name,id):
  with cloudant(user=usr, passwd=pwd, url=url) as client:
    db = CloudantDatabase(client, db_name)
    doc = Document(db, id)
    if doc.exists():
        doc.fetch()
```

### Expected behavior
The full document should be retrieved by `fetch` function, instead when `simplejson` is reachable in the environment, it will raise the following error:
```
File \"/usr/local/lib/python3.6/site-packages/simplejson/__init__.py\", line 535, in loads
return cls(encoding=encoding, **kw).decode(s)
TypeError: __init__() got an unexpected keyword argument 'encoding'
```

### What actually happened
Starting in [this line](https://github.com/cloudant/python-cloudant/blob/2.10.0/src/cloudant/document.py#L170): `self.update(resp.json(cls=self.decoder))`:
1. `json()` function from `requests.models` is called with argument `cls` and value `json.JSONDecoder`, as `self.decoder` [was initialized](https://github.com/cloudant/python-cloudant/blob/2.10.0/src/cloudant/document.py#L69) by default when no decoder is passed to `Document`
2. a JSON decoder [is imported](https://github.com/requests/requests/blob/v2.20.0/requests/models.py#L42) selectively in `requests.compat` module: [when `simplejson` is present](https://github.com/requests/requests/blob/v2.20.0/requests/compat.py#L29), then the json decoder implementation comes from that package
3. the JSON decoder from `simplejson` package is used [to load the contents of the response](https://github.com/requests/requests/blob/v2.20.0/requests/models.py#L888)
4. now, argument `cls` is a `json.JSONDecoder` (see point 1), and it does not expect [an argument called `encoding`](https://github.com/simplejson/simplejson/blob/v3.16.0/simplejson/__init__.py#L535), thus a `TypeError` is raised. 

## Environment details
Version affected: `2.10.0`
Note that in version `2.9.0`, `cls=self.decoder` [was not passed to](https://github.com/cloudant/python-cloudant/commit/82017411beb12717780d036f7308086d8fe37ef5#diff-396eae97315a21001968a65d1ed17e61R171) `resp.json()`.

Discussion:
ricellis: @smithsz @emlaver see bcaf416 for adding an axis that runs tests with `simplejson` present.

Patch:
diff --git a/CHANGES.md b/CHANGES.md
index e1ddefd..47e621f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,3 +1,9 @@
+# Unreleased
+
+- [FIXED] Unexpected keyword argument errors when using the library with the
+ `simplejson` module present in the environment caused by `requests` preferentially
+  loading it over the system `json` module.
+
 # 2.10.0 (2018-09-19)
 
 - [NEW] Add custom JSON encoder/decoder option to `Document` constructor.
diff --git a/Jenkinsfile b/Jenkinsfile
index a806771..1ab03a6 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -10,12 +10,13 @@ def getEnvForSuite(suiteName) {
     case 'basic':
       envVars.add("RUN_BASIC_AUTH_TESTS=1")
       break
-    case 'cookie':
-      break
     case 'iam':
       // Setting IAM_API_KEY forces tests to run using an IAM enabled client.
       envVars.add("IAM_API_KEY=$DB_IAM_API_KEY")
       break
+    case 'cookie':
+    case 'simplejson':
+      break
     default:
       error("Unknown test suite environment ${suiteName}")
   }
@@ -36,6 +37,7 @@ def setupPythonAndTest(pythonVersion, testSuite) {
             . ./tmp/bin/activate
             pip install -r requirements.txt
             pip install -r test-requirements.txt
+            ${'simplejson'.equals(testSuite) ? 'pip install simplejson' : ''}
             pylint ./src/cloudant
             nosetests -A 'not db or (db is "cloudant" or "cloudant" in db)' -w ./tests/unit --with-xunit
           """
@@ -58,12 +60,15 @@ stage('Checkout'){
 }
 
 stage('Test'){
-  axes = [:]
-  ['2.7.12','3.5.2'].each { version ->
+  def py2 = '2.7.12'
+  def py3 = '3.5.2'
+  def axes = [:]
+  [py2, py3].each { version ->
     ['basic','cookie','iam'].each { auth ->
        axes.put("Python${version}-${auth}", {setupPythonAndTest(version, auth)})
     }
   }
+  axes.put("Python${py3}-simplejson", {setupPythonAndTest(py3, 'simplejson')})
   parallel(axes)
 }
 
diff --git a/src/cloudant/_client_session.py b/src/cloudant/_client_session.py
index 0e8fd38..820bc53 100644
--- a/src/cloudant/_client_session.py
+++ b/src/cloudant/_client_session.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015, 2018 IBM Corp. All rights reserved.
+# Copyright (C) 2015, 2018 IBM Corp. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ import os
 from requests import RequestException, Session
 
 from ._2to3 import bytes_, unicode_, url_join
+from ._common_util import response_to_json_dict
 from .error import CloudantException
 
 
@@ -75,7 +76,7 @@ class ClientSession(Session):
 
         resp = self.get(self._session_url)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def set_credentials(self, username, password):
         """
@@ -170,7 +171,7 @@ class CookieSession(ClientSession):
 
         is_expired = any((
             resp.status_code == 403 and
-            resp.json().get('error') == 'credentials_expired',
+            response_to_json_dict(resp).get('error') == 'credentials_expired',
             resp.status_code == 401
         ))
 
@@ -282,10 +283,10 @@ class IAMSession(ClientSession):
                     'apikey': self._api_key
                 }
             )
-            err = resp.json().get('errorMessage', err)
+            err = response_to_json_dict(resp).get('errorMessage', err)
             resp.raise_for_status()
 
-            return resp.json()['access_token']
+            return response_to_json_dict(resp)['access_token']
 
         except KeyError:
             raise CloudantException('Invalid response from IAM token service')
diff --git a/src/cloudant/_common_util.py b/src/cloudant/_common_util.py
index 1904a0a..7a37381 100644
--- a/src/cloudant/_common_util.py
+++ b/src/cloudant/_common_util.py
@@ -270,7 +270,7 @@ def append_response_error_content(response, **kwargs):
     """
     if response.status_code >= 400:
         try:
-            resp_dict = response.json()
+            resp_dict = response_to_json_dict(response)
             error = resp_dict.get('error', '')
             reason = resp_dict.get('reason', '')
             # Append to the existing response's reason
@@ -279,6 +279,17 @@ def append_response_error_content(response, **kwargs):
             pass
     return response
 
+def response_to_json_dict(response, **kwargs):
+    """
+    Standard place to convert responses to JSON.
+
+    :param response: requests response object
+    :param **kwargs: arguments accepted by json.loads
+
+    :returns: dict of JSON response
+    """
+    return json.loads(response.text, **kwargs)
+
 # Classes
 
 
diff --git a/src/cloudant/client.py b/src/cloudant/client.py
index 06a6092..9b22d0d 100755
--- a/src/cloudant/client.py
+++ b/src/cloudant/client.py
@@ -35,6 +35,7 @@ from ._common_util import (
     USER_AGENT,
     append_response_error_content,
     CloudFoundryService,
+    response_to_json_dict,
     )
 
 
@@ -256,7 +257,7 @@ class CouchDB(dict):
         url = '/'.join((self.server_url, '_all_dbs'))
         resp = self.r_session.get(url)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def create_database(self, dbname, **kwargs):
         """
@@ -345,7 +346,7 @@ class CouchDB(dict):
         """
         resp = self.r_session.get(self.server_url)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def keys(self, remote=False):
         """
@@ -622,7 +623,7 @@ class Cloudant(CouchDB):
             raise CloudantArgumentError(101, year, month)
         else:
             resp.raise_for_status()
-            return resp.json()
+            return response_to_json_dict(resp)
 
     def bill(self, year=None, month=None):
         """
@@ -687,7 +688,7 @@ class Cloudant(CouchDB):
             self.server_url, '_api', 'v2', 'user', 'shared_databases'))
         resp = self.r_session.get(endpoint)
         resp.raise_for_status()
-        data = resp.json()
+        data = response_to_json_dict(resp)
         return data.get('shared_databases', [])
 
     def generate_api_key(self):
@@ -699,7 +700,7 @@ class Cloudant(CouchDB):
         endpoint = '/'.join((self.server_url, '_api', 'v2', 'api_keys'))
         resp = self.r_session.post(endpoint)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def cors_configuration(self):
         """
@@ -712,7 +713,7 @@ class Cloudant(CouchDB):
         resp = self.r_session.get(endpoint)
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def disable_cors(self):
         """
@@ -807,7 +808,7 @@ class Cloudant(CouchDB):
         )
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     @classmethod
     def bluemix(cls, vcap_services, instance_name=None, service_name=None, **kwargs):
diff --git a/src/cloudant/database.py b/src/cloudant/database.py
index ec6c8b7..51ff243 100644
--- a/src/cloudant/database.py
+++ b/src/cloudant/database.py
@@ -26,7 +26,9 @@ from ._common_util import (
     SEARCH_INDEX_ARGS,
     SPECIAL_INDEX_TYPE,
     TEXT_INDEX_TYPE,
-    get_docs)
+    get_docs,
+    response_to_json_dict,
+    )
 from .document import Document
 from .design_document import DesignDocument
 from .security_document import SecurityDocument
@@ -123,7 +125,7 @@ class CouchDatabase(dict):
         """
         resp = self.r_session.get(self.database_url)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def doc_count(self):
         """
@@ -192,7 +194,7 @@ class CouchDatabase(dict):
         query = "startkey=\"_design\"&endkey=\"_design0\"&include_docs=true"
         resp = self.r_session.get(url, params=query)
         resp.raise_for_status()
-        data = resp.json()
+        data = response_to_json_dict(resp)
         return data['rows']
 
     def list_design_documents(self):
@@ -206,7 +208,7 @@ class CouchDatabase(dict):
         query = "startkey=\"_design\"&endkey=\"_design0\""
         resp = self.r_session.get(url, params=query)
         resp.raise_for_status()
-        data = resp.json()
+        data = response_to_json_dict(resp)
         return [x.get('key') for x in data.get('rows', [])]
 
     def get_design_document(self, ddoc_id):
@@ -403,7 +405,7 @@ class CouchDatabase(dict):
                         '/'.join([self.database_url, '_all_docs']),
                         self.client.encoder,
                         **kwargs)
-        return resp.json()
+        return response_to_json_dict(resp)
 
     @contextlib.contextmanager
     def custom_result(self, **options):
@@ -718,7 +720,7 @@ class CouchDatabase(dict):
             headers=headers
         )
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def missing_revisions(self, doc_id, *revisions):
         """
@@ -742,7 +744,7 @@ class CouchDatabase(dict):
         )
         resp.raise_for_status()
 
-        resp_json = resp.json()
+        resp_json = response_to_json_dict(resp)
         missing_revs = resp_json['missing_revs'].get(doc_id)
         if missing_revs is None:
             missing_revs = []
@@ -771,7 +773,7 @@ class CouchDatabase(dict):
         )
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def get_revision_limit(self):
         """
@@ -787,7 +789,7 @@ class CouchDatabase(dict):
         try:
             ret = int(resp.text)
         except ValueError:
-            raise CloudantDatabaseException(400, resp.json())
+            raise CloudantDatabaseException(400, response_to_json_dict(resp))
 
         return ret
 
@@ -806,7 +808,7 @@ class CouchDatabase(dict):
         resp = self.r_session.put(url, data=json.dumps(limit, cls=self.client.encoder))
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def view_cleanup(self):
         """
@@ -822,7 +824,7 @@ class CouchDatabase(dict):
         )
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def get_list_function_result(self, ddoc_id, list_name, view_name, **kwargs):
         """
@@ -974,10 +976,10 @@ class CouchDatabase(dict):
         resp.raise_for_status()
 
         if raw_result:
-            return resp.json()
+            return response_to_json_dict(resp)
 
         indexes = []
-        for data in resp.json().get('indexes', []):
+        for data in response_to_json_dict(resp).get('indexes', []):
             if data.get('type') == JSON_INDEX_TYPE:
                 indexes.append(Index(
                     self,
@@ -1239,7 +1241,7 @@ class CloudantDatabase(CouchDatabase):
             headers={'Content-Type': 'application/json'}
         )
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def unshare_database(self, username):
         """
@@ -1264,7 +1266,7 @@ class CloudantDatabase(CouchDatabase):
             headers={'Content-Type': 'application/json'}
         )
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def shards(self):
         """
@@ -1276,7 +1278,7 @@ class CloudantDatabase(CouchDatabase):
         resp = self.r_session.get(url)
         resp.raise_for_status()
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def get_search_result(self, ddoc_id, index_name, **query_params):
         """
@@ -1399,4 +1401,4 @@ class CloudantDatabase(CouchDatabase):
             data=json.dumps(query_params, cls=self.client.encoder)
         )
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
diff --git a/src/cloudant/design_document.py b/src/cloudant/design_document.py
index b32c5e3..66a9789 100644
--- a/src/cloudant/design_document.py
+++ b/src/cloudant/design_document.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (C) 2015, 2018 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -16,7 +16,7 @@
 API module/class for interacting with a design document in a database.
 """
 from ._2to3 import iteritems_, STRTYPE
-from ._common_util import QUERY_LANGUAGE, codify
+from ._common_util import QUERY_LANGUAGE, codify, response_to_json_dict
 from .document import Document
 from .view import View, QueryIndexView
 from .error import CloudantArgumentError, CloudantDesignDocumentException
@@ -686,7 +686,7 @@ class DesignDocument(Document):
         ddoc_info = self.r_session.get(
             '/'.join([self.document_url, '_info']))
         ddoc_info.raise_for_status()
-        return ddoc_info.json()
+        return response_to_json_dict(ddoc_info)
 
     def search_info(self, search_index):
         """
@@ -698,7 +698,7 @@ class DesignDocument(Document):
         ddoc_search_info = self.r_session.get(
             '/'.join([self.document_url, '_search_info', search_index]))
         ddoc_search_info.raise_for_status()
-        return ddoc_search_info.json()
+        return response_to_json_dict(ddoc_search_info)
 
     def search_disk_size(self, search_index):
         """
@@ -710,4 +710,4 @@ class DesignDocument(Document):
         ddoc_search_disk_size = self.r_session.get(
             '/'.join([self.document_url, '_search_disk_size', search_index]))
         ddoc_search_disk_size.raise_for_status()
-        return ddoc_search_disk_size.json()
+        return response_to_json_dict(ddoc_search_disk_size)
diff --git a/src/cloudant/document.py b/src/cloudant/document.py
index ac08ee6..426b065 100644
--- a/src/cloudant/document.py
+++ b/src/cloudant/document.py
@@ -20,6 +20,7 @@ import requests
 from requests.exceptions import HTTPError
 
 from ._2to3 import url_quote, url_quote_plus
+from ._common_util import response_to_json_dict
 from .error import CloudantDocumentException
 
 
@@ -53,8 +54,8 @@ class Document(dict):
     :param database: A database instance used by the Document.  Can be
         either a ``CouchDatabase`` or ``CloudantDatabase`` instance.
     :param str document_id: Optional document id used to identify the document.
-    :param str encoder: Optional JSON encoder object.
-    :param str decoder: Optional JSON decoder object.
+    :param str encoder: Optional JSON encoder object (extending json.JSONEncoder).
+    :param str decoder: Optional JSON decoder object (extending json.JSONDecoder).
     """
     def __init__(self, database, document_id=None, **kwargs):
         super(Document, self).__init__()
@@ -150,7 +151,7 @@ class Document(dict):
             data=json.dumps(doc, cls=self.encoder)
         )
         resp.raise_for_status()
-        data = resp.json()
+        data = response_to_json_dict(resp)
         self._document_id = data['id']
         super(Document, self).__setitem__('_id', data['id'])
         super(Document, self).__setitem__('_rev', data['rev'])
@@ -167,7 +168,7 @@ class Document(dict):
         resp = self.r_session.get(self.document_url)
         resp.raise_for_status()
         self.clear()
-        self.update(resp.json(cls=self.decoder))
+        self.update(response_to_json_dict(resp, cls=self.decoder))
 
     def save(self):
         """
@@ -189,7 +190,7 @@ class Document(dict):
             headers=headers
         )
         put_resp.raise_for_status()
-        data = put_resp.json()
+        data = response_to_json_dict(put_resp)
         super(Document, self).__setitem__('_rev', data['rev'])
         return
 
@@ -418,7 +419,7 @@ class Document(dict):
         if attachment_type == 'text':
             return resp.text
         if attachment_type == 'json':
-            return resp.json()
+            return response_to_json_dict(resp)
 
         return resp.content
 
@@ -447,7 +448,7 @@ class Document(dict):
             headers=headers
         )
         resp.raise_for_status()
-        super(Document, self).__setitem__('_rev', resp.json()['rev'])
+        super(Document, self).__setitem__('_rev', response_to_json_dict(resp)['rev'])
         # Execute logic only if attachment metadata exists locally
         if self.get('_attachments'):
             # Remove the attachment metadata for the specified attachment
@@ -457,7 +458,7 @@ class Document(dict):
             if not self['_attachments']:
                 super(Document, self).__delitem__('_attachments')
 
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def put_attachment(self, attachment, content_type, data, headers=None):
         """
@@ -494,4 +495,4 @@ class Document(dict):
         )
         resp.raise_for_status()
         self.fetch()
-        return resp.json()
+        return response_to_json_dict(resp)
diff --git a/src/cloudant/index.py b/src/cloudant/index.py
index 3d0d49c..40274ce 100644
--- a/src/cloudant/index.py
+++ b/src/cloudant/index.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (C) 2015, 2018 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ from ._common_util import JSON_INDEX_TYPE
 from ._common_util import TEXT_INDEX_TYPE
 from ._common_util import SPECIAL_INDEX_TYPE
 from ._common_util import TEXT_INDEX_ARGS
+from ._common_util import response_to_json_dict
 from .error import CloudantArgumentError, CloudantIndexException
 
 class Index(object):
@@ -143,8 +144,8 @@ class Index(object):
             headers=headers
         )
         resp.raise_for_status()
-        self._ddoc_id = resp.json()['id']
-        self._name = resp.json()['name']
+        self._ddoc_id = response_to_json_dict(resp)['id']
+        self._name = response_to_json_dict(resp)['name']
 
     def _def_check(self):
         """
diff --git a/src/cloudant/query.py b/src/cloudant/query.py
index b5bd2b5..ed9ec35 100644
--- a/src/cloudant/query.py
+++ b/src/cloudant/query.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (C) 2015, 2018 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ from ._2to3 import iteritems_
 from .result import QueryResult
 from .error import CloudantArgumentError
 from ._common_util import QUERY_ARG_TYPES
+from ._common_util import response_to_json_dict
 
 class Query(dict):
     """
@@ -172,7 +173,7 @@ class Query(dict):
             data=json.dumps(data, cls=self._encoder)
         )
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     @contextlib.contextmanager
     def custom_result(self, **options):
diff --git a/src/cloudant/scheduler.py b/src/cloudant/scheduler.py
index 723635b..012272f 100644
--- a/src/cloudant/scheduler.py
+++ b/src/cloudant/scheduler.py
@@ -16,6 +16,8 @@
 API module for interacting with scheduler endpoints
 """
 
+from ._common_util import response_to_json_dict
+
 class Scheduler(object):
     """
     API for retrieving scheduler jobs and documents.
@@ -47,7 +49,7 @@ class Scheduler(object):
             params["skip"] = skip
         resp = self._r_session.get('/'.join([self._scheduler, 'docs']), params=params)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
     def get_doc(self, doc_id):
         """
@@ -55,7 +57,7 @@ class Scheduler(object):
         """
         resp = self._r_session.get('/'.join([self._scheduler, 'docs', '_replicator', doc_id]))
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
 
 
     def list_jobs(self, limit=None, skip=None):
@@ -78,4 +80,4 @@ class Scheduler(object):
             params["skip"] = skip
         resp = self._r_session.get('/'.join([self._scheduler, 'jobs']), params=params)
         resp.raise_for_status()
-        return resp.json()
+        return response_to_json_dict(resp)
diff --git a/src/cloudant/security_document.py b/src/cloudant/security_document.py
index 22b0f79..87b77ca 100644
--- a/src/cloudant/security_document.py
+++ b/src/cloudant/security_document.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2016 IBM. All rights reserved.
+# Copyright (C) 2016, 2018 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ API module/class for interacting with a security document in a database.
 import json
 
 from ._2to3 import url_quote_plus
+from ._common_util import response_to_json_dict
 
 class SecurityDocument(dict):
     """
@@ -101,7 +102,7 @@ class SecurityDocument(dict):
         resp = self.r_session.get(self.document_url)
         resp.raise_for_status()
         self.clear()
-        self.update(resp.json())
+        self.update(response_to_json_dict(resp))
 
     def save(self):
         """
diff --git a/src/cloudant/view.py b/src/cloudant/view.py
index 685600e..3a0e63f 100644
--- a/src/cloudant/view.py
+++ b/src/cloudant/view.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright (c) 2015 IBM. All rights reserved.
+# Copyright (C) 2015, 2018 IBM. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@ API module for interacting with a view in a design document.
 import contextlib
 
 from ._2to3 import STRTYPE
-from ._common_util import codify, get_docs
+from ._common_util import codify, get_docs, response_to_json_dict
 from .result import Result
 from .error import CloudantArgumentError, CloudantViewException
 
@@ -227,7 +227,7 @@ class View(dict):
                         self.url,
                         self.design_doc.encoder,
                         **kwargs)
-        return resp.json()
+        return response_to_json_dict(resp)
 
     @contextlib.contextmanager
     def custom_result(self, **options):


Test patch:
diff --git a/tests/unit/client_tests.py b/tests/unit/client_tests.py
index e110e5f..0a544a1 100644
--- a/tests/unit/client_tests.py
+++ b/tests/unit/client_tests.py
@@ -247,7 +247,7 @@ class ClientTests(UnitTestDbBase):
         """
         m_response_ok = mock.MagicMock()
         type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
-        m_response_ok.json.return_value = ['animaldb']
+        type(m_response_ok).text = mock.PropertyMock(return_value='["animaldb"]')
         m_req.return_value = m_response_ok
 
         client = Cloudant('foo', 'bar', url=self.url, use_basic_auth=True)
@@ -298,8 +298,7 @@ class ClientTests(UnitTestDbBase):
         """
         # mock 200
         m_response_ok = mock.MagicMock()
-        m_response_ok.json.return_value = ['animaldb']
-
+        type(m_response_ok).text = mock.PropertyMock(return_value='["animaldb"]')
         # mock 401
         m_response_bad = mock.MagicMock()
         m_response_bad.raise_for_status.side_effect = HTTPError('401 Unauthorized')
diff --git a/tests/unit/database_tests.py b/tests/unit/database_tests.py
index 5881a92..f4c0e9a 100644
--- a/tests/unit/database_tests.py
+++ b/tests/unit/database_tests.py
@@ -29,6 +29,7 @@ import uuid
 import mock
 import requests
 from cloudant._2to3 import UNICHR
+from cloudant._common_util import response_to_json_dict
 from cloudant.design_document import DesignDocument
 from cloudant.document import Document
 from cloudant.error import CloudantArgumentError, CloudantDatabaseException
@@ -237,7 +238,7 @@ class DatabaseTests(UnitTestDbBase):
         """
         resp = self.db.r_session.get(
             '/'.join((self.client.server_url, self.test_dbname)))
-        expected = resp.json()
+        expected = response_to_json_dict(resp)
         actual = self.db.metadata()
         self.assertListEqual(list(actual.keys()), list(expected.keys()))
 
diff --git a/tests/unit/design_document_tests.py b/tests/unit/design_document_tests.py
index 4371472..c986e76 100644
--- a/tests/unit/design_document_tests.py
+++ b/tests/unit/design_document_tests.py
@@ -27,6 +27,7 @@ import unittest
 
 import mock
 import requests
+from cloudant._common_util import response_to_json_dict
 from cloudant.design_document import DesignDocument
 from cloudant.document import Document
 from cloudant.error import CloudantArgumentError, CloudantDesignDocumentException
@@ -693,7 +694,7 @@ class DesignDocumentTests(UnitTestDbBase):
         # Ensure that remotely saved design document does not
         # include a views sub-document.
         resp = self.client.r_session.get(ddoc.document_url)
-        raw_ddoc = resp.json()
+        raw_ddoc = response_to_json_dict(resp)
         self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
         self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
         self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1182,7 +1183,7 @@ class DesignDocumentTests(UnitTestDbBase):
         # Ensure that remotely saved design document does not
         # include a search indexes sub-document.
         resp = self.client.r_session.get(ddoc.document_url)
-        raw_ddoc = resp.json()
+        raw_ddoc = response_to_json_dict(resp)
         self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
         self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
         self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1263,7 +1264,7 @@ class DesignDocumentTests(UnitTestDbBase):
         doc.save()
         resp = self.client.r_session.get('/'.join([ddoc.document_url, '_rewrite']))
         self.assertEquals(
-            resp.json(),
+            response_to_json_dict(resp),
             {
                 '_id': 'rewrite_doc',
                 '_rev': doc['_rev']
@@ -1451,7 +1452,7 @@ class DesignDocumentTests(UnitTestDbBase):
         # Ensure that remotely saved design document does not
         # include a lists sub-document.
         resp = self.client.r_session.get(ddoc.document_url)
-        raw_ddoc = resp.json()
+        raw_ddoc = response_to_json_dict(resp)
         self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
         self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
         self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1752,7 +1753,7 @@ class DesignDocumentTests(UnitTestDbBase):
         # Ensure that remotely saved design document does not
         # include a shows sub-document.
         resp = self.client.r_session.get(ddoc.document_url)
-        raw_ddoc = resp.json()
+        raw_ddoc = response_to_json_dict(resp)
         self.assertEqual(set(raw_ddoc.keys()), {'_id', '_rev'})
         self.assertEqual(raw_ddoc['_id'], ddoc['_id'])
         self.assertEqual(raw_ddoc['_rev'], ddoc['_rev'])
@@ -1834,7 +1835,7 @@ class DesignDocumentTests(UnitTestDbBase):
             data=json.dumps({'_id': 'test001'})
         )
         self.assertEqual(
-            resp.json(),
+            response_to_json_dict(resp),
             {'reason': 'Document must have an address.', 'error': 'forbidden'}
         )
 
diff --git a/tests/unit/iam_auth_tests.py b/tests/unit/iam_auth_tests.py
index 6e0c767..ec4fbd9 100644
--- a/tests/unit/iam_auth_tests.py
+++ b/tests/unit/iam_auth_tests.py
@@ -43,24 +43,22 @@ MOCK_ACCESS_TOKEN = ('eyJraWQiOiIyMDE3MDQwMi0wMDowMDowMCIsImFsZyI6IlJTMjU2In0.e'
                      '2PTo4Exa17V-R_73Nq8VPCwpOvZcwKRA2sPTVgTMzU34max8b5kpTzVGJ'
                      '6SXSItTVOUdAygZBng')
 
-MOCK_IAM_TOKEN_RESPONSE = {
-    'access_token':  MOCK_ACCESS_TOKEN,
-    'refresh_token': ('MO61FKNvVRWkSa4vmBZqYv_Jt1kkGMUc-XzTcNnR-GnIhVKXHUWxJVV3'
-                      'RddE8Kqh3X_TZRmyK8UySIWKxoJ2t6obUSUalPm90SBpTdoXtaljpNyo'
-                      'rmqCCYPROnk6JBym72ikSJqKHHEZVQkT0B5ggZCwPMnKagFj0ufs-VIh'
-                      'CF97xhDxDKcIPMWG02xxPuESaSTJJug7e_dUDoak_ZXm9xxBmOTRKwOx'
-                      'n5sTKthNyvVpEYPE7jIHeiRdVDOWhN5LomgCn3TqFCLpMErnqwgNYbyC'
-                      'Bd9rNm-alYKDb6Jle4njuIBpXxQPb4euDwLd1osApaSME3nEarFWqRBz'
-                      'hjoqCe1Kv564s_rY7qzD1nHGvKOdpSa0ZkMcfJ0LbXSQPs7gBTSVrBFZ'
-                      'qwlg-2F-U3Cto62-9qRR_cEu_K9ZyVwL4jWgOlngKmxV6Ku4L5mHp4Kg'
-                      'EJSnY_78_V2nm64E--i2ZA1FhiKwIVHDOivVNhggE9oabxg54vd63glp'
-                      '4GfpNnmZsMOUYG9blJJpH4fDX4Ifjbw-iNBD7S2LRpP8b8vG9pb4WioG'
-                      'zN43lE5CysveKYWrQEZpThznxXlw1snDu_A48JiL3Lrvo1LobLhF3zFV'
-                      '-kQ='),
-    'token_type': 'Bearer',
-    'expires_in': 3600,  # 60mins
-    'expiration': 1500470702  # Wed Jul 19 14:25:02 2017
-}
+MOCK_IAM_TOKEN_RESPONSE = '{"access_token": "%s",\
+    "refresh_token": "MO61FKNvVRWkSa4vmBZqYv_Jt1kkGMUc-XzTcNnR-GnIhVKXHUWxJVV3\
+                      RddE8Kqh3X_TZRmyK8UySIWKxoJ2t6obUSUalPm90SBpTdoXtaljpNyo\
+                      rmqCCYPROnk6JBym72ikSJqKHHEZVQkT0B5ggZCwPMnKagFj0ufs-VIh\
+                      CF97xhDxDKcIPMWG02xxPuESaSTJJug7e_dUDoak_ZXm9xxBmOTRKwOx\
+                      n5sTKthNyvVpEYPE7jIHeiRdVDOWhN5LomgCn3TqFCLpMErnqwgNYbyC\
+                      Bd9rNm-alYKDb6Jle4njuIBpXxQPb4euDwLd1osApaSME3nEarFWqRBz\
+                      hjoqCe1Kv564s_rY7qzD1nHGvKOdpSa0ZkMcfJ0LbXSQPs7gBTSVrBFZ\
+                      qwlg-2F-U3Cto62-9qRR_cEu_K9ZyVwL4jWgOlngKmxV6Ku4L5mHp4Kg\
+                      EJSnY_78_V2nm64E--i2ZA1FhiKwIVHDOivVNhggE9oabxg54vd63glp\
+                      4GfpNnmZsMOUYG9blJJpH4fDX4Ifjbw-iNBD7S2LRpP8b8vG9pb4WioG\
+                      zN43lE5CysveKYWrQEZpThznxXlw1snDu_A48JiL3Lrvo1LobLhF3zFV\
+                      -kQ=",\
+    "token_type": "Bearer",\
+    "expires_in": 3600,\
+    "expiration": 1500470702}'%(MOCK_ACCESS_TOKEN)
 
 
 class IAMAuthTests(unittest.TestCase):
@@ -100,7 +98,8 @@ class IAMAuthTests(unittest.TestCase):
     @mock.patch('cloudant._client_session.ClientSession.request')
     def test_iam_get_access_token(self, m_req):
         m_response = mock.MagicMock()
-        m_response.json.return_value = MOCK_IAM_TOKEN_RESPONSE
+        mock_token_response_text = mock.PropertyMock(return_value=MOCK_IAM_TOKEN_RESPONSE)
+        type(m_response).text = mock_token_response_text
         m_req.return_value = m_response
 
         iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
@@ -120,7 +119,7 @@ class IAMAuthTests(unittest.TestCase):
 
         self.assertEqual(access_token, MOCK_ACCESS_TOKEN)
         self.assertTrue(m_response.raise_for_status.called)
-        self.assertTrue(m_response.json.called)
+        mock_token_response_text.assert_called_with()
 
     @mock.patch('cloudant._client_session.ClientSession.request')
     @mock.patch('cloudant._client_session.IAMSession._get_access_token')
@@ -152,10 +151,10 @@ class IAMAuthTests(unittest.TestCase):
 
     @mock.patch('cloudant._client_session.ClientSession.get')
     def test_iam_get_session_info(self, m_get):
-        m_info = {'ok': True, 'info': {'authentication_db': '_users'}}
+        m_info = '{"ok": true, "info": {"authentication_db": "_users"}}'
 
         m_response = mock.MagicMock()
-        m_response.json.return_value = m_info
+        type(m_response).text = mock.PropertyMock(return_value=m_info)
         m_get.return_value = m_response
 
         iam = IAMSession(MOCK_API_KEY, 'http://127.0.0.1:5984')
@@ -163,7 +162,7 @@ class IAMAuthTests(unittest.TestCase):
 
         m_get.assert_called_once_with(iam._session_url)
 
-        self.assertEqual(info, m_info)
+        self.assertEqual(info, json.loads(m_info))
         self.assertTrue(m_response.raise_for_status.called)
 
     @mock.patch('cloudant._client_session.IAMSession.login')
@@ -172,7 +171,7 @@ class IAMAuthTests(unittest.TestCase):
         # mock 200
         m_response_ok = mock.MagicMock()
         type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
-        m_response_ok.json.return_value = {'ok': True}
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"ok": true}')
 
         m_req.return_value = m_response_ok
 
@@ -197,7 +196,7 @@ class IAMAuthTests(unittest.TestCase):
         # mock 200
         m_response_ok = mock.MagicMock()
         type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
-        m_response_ok.json.return_value = {'ok': True}
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"ok": true}')
 
         m_req.return_value = m_response_ok
 
@@ -219,7 +218,7 @@ class IAMAuthTests(unittest.TestCase):
         # mock 200
         m_response_ok = mock.MagicMock()
         type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
-        m_response_ok.json.return_value = {'ok': True}
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"ok": true}')
         # mock 401
         m_response_bad = mock.MagicMock()
         type(m_response_bad).status_code = mock.PropertyMock(return_value=401)
@@ -298,7 +297,7 @@ class IAMAuthTests(unittest.TestCase):
         # mock 200
         m_response_ok = mock.MagicMock()
         type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
-        m_response_ok.json.return_value = ['animaldb']
+        type(m_response_ok).text = mock.PropertyMock(return_value='["animaldb"]')
 
         m_req.return_value = m_response_ok
 
diff --git a/tests/unit/scheduler_tests.py b/tests/unit/scheduler_tests.py
index 97b5046..5ef8cab 100644
--- a/tests/unit/scheduler_tests.py
+++ b/tests/unit/scheduler_tests.py
@@ -46,107 +46,105 @@ class SchedulerTests(UnitTestDbBase):
         Test scheduler docs
         """
         # set up mock response using a real captured response
-        m_response_ok = requests.Response()
-        m_response_ok.status_code = 200
-        m_response_ok.json = mock.Mock()
-        m_response_ok.json.return_value = {"total_rows":6,"offset":0,"docs":[
-            {"database":"tomblench/_replicator",
-             "doc_id":"296e48244e003eba8764b2156b3bf302",
-             "id":None,
-             "source":"https://tomblench.cloudant.com/animaldb/",
-             "target":"https://tomblench.cloudant.com/animaldb_copy/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":15,
-                     "missing_revisions_found":2,
-                     "docs_read":2,
-                     "docs_written":2,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"19-g1AAAAGjeJyVz10KwjAMB_BoJ4KX8AZF2tWPJ3eVpqnO0XUg27PeTG9Wa_VhwmT6kkDIPz_iACArGcGS0DRnWxDmHE9HdJ3lxjUdad9yb1sXF6cacB9CqEqmZ3UczKUh2uGhHxeD8U9i_Z3AIla8vJVJUlBIZYTqX5A_KMM7SfFZrHCNLUK3p7RIkl5tSRD-K6kx6f6S0k8sScpYJTb5uFQ9AI9Ch9c"},
-             "start_time":None,
-             "last_updated":"2017-04-13T14:53:50+00:00"},
-            {"database":"tomblench/_replicator",
-             "doc_id":"3b749f320867d703550b0f758a4000ae",
-             "id":None,
-             "source":"https://examples.cloudant.com/animaldb/",
-             "target":"https://tomblench.cloudant.com/animaldb/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":15,
-                     "missing_revisions_found":15,
-                     "docs_read":15,
-                     "docs_written":15,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"56-g1AAAAGveJzLYWBgYMlgTmFQSElKzi9KdUhJstDLTS3KLElMT9VLzskvTUnMK9HLSy3JAapkSmRIsv___39WBnMiby5QgN04JS3FLDUJWb8Jdv0gSxThigyN8diS5AAkk-qhFvFALEo2MTEwMSXGDDSbTPHYlMcCJBkagBTQsv0g28TBtpkbGCQapaF4C4cxJFt2AGIZ2GscYMuMDEzMUizMkC0zw25MFgBKoovi"},
-             "start_time":None,
-             "last_updated":"2017-04-27T12:28:44+00:00"},
-            {"database":"tomblench/_replicator",
-             "doc_id":"ad8f7896480b8081c8f0a2267ffd1859",
-             "id":None,
-             "source":"https://tortytherlediffecareette:*****@mikerhodestesty008.cloudant.com/moviesdb/",
-             "target":"https://tomblench.cloudant.com/moviesdb_rep/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":5997,
-                     "missing_revisions_found":5997,
-                     "docs_read":5997,
-                     "docs_written":5997,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"5997-g1AAAANreJy10UEKwjAQAMBgBcVP2BeUpEm1PdmfaDYJSKkVtB486U_0J_oBTz5AHyAI3jxIjUml1x7ayy67LDssmyKE-nNHIleCWK5ULIF6uVrnW4xDT6TLjeRZ7mUqT_VkhyMYFkWRzB3Q1XOhez3iczKKghor6jvg6giTiroYiuNQYYqbpeIfNa2oh72KhQGosFlq9qN2FfUyFPgUCKONoneXR7TXSWuHkvsYjjEWjQVvgTta7lRyV_szKgmRbVx3ttzNcs7AcEoKCHAb3N1y_9-9DYeBYzEiNTYlX3EcE0s"},
-             "start_time":None,
-             "last_updated":"2016-08-23T13:11:26+00:00"},
-            {"database":"tomblench/_replicator",
-             "doc_id":"b63c053ecd95a4047b55ed8847b046f1",
-             "id":None,
-             "source":"https://tomblench.cloudant.com/atestdb2/",
-             "target":"https://tomblench.cloudant.com/atestdb1/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":1,
-                     "missing_revisions_found":1,
-                     "docs_read":1,
-                     "docs_written":1,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"2-g1AAAAFHeJyNjkEOgjAQRSdAYjyFN2jSFCtdyVU6nSKQWhJC13ozvVktsoEF0c2fTPL_-98BQNHmBCdCM4y2JuQMuxu6YJlxQyDtJ-bt5JIx04DXGGOvYRsR-xGsk-JjTrW5hnv6Dg0XplRngmPwZJvOW9ry5D7PF0nhmU5CvmZm9mVKVVacLr8pfy9fmt5L02q9qEhJbtbr-w-AQmfD"},
-             "start_time":None,
-             "last_updated":"2017-05-16T16:25:22+00:00"},
-            {"database":"tomblench/_replicator",
-             "doc_id":"c71c9e69e30a182dc91d8938277bc85e",
-             "id":None,
-             "source":"https://tomblench.cloudant.com/animaldb/",
-             "target":"https://tomblench.cloudant.com/animaldb_copy/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":15,
-                     "missing_revisions_found":15,
-                     "docs_read":15,
-                     "docs_written":15,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"14-g1AAAAEueJzLYWBgYMlgTmGQSUlKzi9KdUhJMtTLTU1M0UvOyS9NScwr0ctLLckBqmJKZEiy____f1YGUyJrLlCAPdHEPCktJZk43UkOQDKpHmoAI9gAw2STxCTzJOIMyGMBkgwNQApoxv6sDGaoK0yN04wsk80IGEGKHQcgdoAdygxxaIplklFaWhYAu2FdOA"},
-             "start_time":None,
-             "last_updated":"2015-05-12T11:47:33+00:00"},
-            {"database":"tomblench/_replicator",
-             "doc_id":"e6242d1e9ce059b0388fc75af3116a39",
-             "id":None,
-             "source":"https://tomblench.cloudant.com/atestdb1/",
-             "target":"https://tomblench.cloudant.com/atestdb2/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":1,
-                     "missing_revisions_found":1,
-                     "docs_read":1,
-                     "docs_written":1,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"1-g1AAAAFheJyFzkEOgjAQBdBRSIyn8AZNgEJgJVeZ6bQCqSUhdK0305th1Q1dEDYzyWTy_rcAkHYJw4VJjZNumQpB_Y2s10LZ0TO6WTg92_B4RKDrsixDlyDcw-FUVUiFahjO3rE2vdMcY9k2Rm2Y9Ig8bWqspdz25Lbn0jDhGVYgX1_z8DMblnlp8n0lTir3kt7_pFV7NE2WYbluP3wATr5vQA"},
-             "start_time":None,
-             "last_updated":"2017-05-16T16:24:02+00:00"}
-        ]}
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"total_rows":6,"offset":0,"docs":[\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"296e48244e003eba8764b2156b3bf302",\
+             "id":null,\
+             "source":"https://tomblench.cloudant.com/animaldb/",\
+             "target":"https://tomblench.cloudant.com/animaldb_copy/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":15,\
+                     "missing_revisions_found":2,\
+                     "docs_read":2,\
+                     "docs_written":2,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"19-g1AAAAGjeJyVz10KwjAMB_BoJ4KX8AZF2tWPJ3eVpqnO0XUg27PeTG9Wa_VhwmT6kkDIPz_iACArGcGS0DRnWxDmHE9HdJ3lxjUdad9yb1sXF6cacB9CqEqmZ3UczKUh2uGhHxeD8U9i_Z3AIla8vJVJUlBIZYTqX5A_KMM7SfFZrHCNLUK3p7RIkl5tSRD-K6kx6f6S0k8sScpYJTb5uFQ9AI9Ch9c"},\
+             "start_time":null,\
+             "last_updated":"2017-04-13T14:53:50+00:00"},\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"3b749f320867d703550b0f758a4000ae",\
+             "id":null,\
+             "source":"https://examples.cloudant.com/animaldb/",\
+             "target":"https://tomblench.cloudant.com/animaldb/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":15,\
+                     "missing_revisions_found":15,\
+                     "docs_read":15,\
+                     "docs_written":15,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"56-g1AAAAGveJzLYWBgYMlgTmFQSElKzi9KdUhJstDLTS3KLElMT9VLzskvTUnMK9HLSy3JAapkSmRIsv___39WBnMiby5QgN04JS3FLDUJWb8Jdv0gSxThigyN8diS5AAkk-qhFvFALEo2MTEwMSXGDDSbTPHYlMcCJBkagBTQsv0g28TBtpkbGCQapaF4C4cxJFt2AGIZ2GscYMuMDEzMUizMkC0zw25MFgBKoovi"},\
+             "start_time":null,\
+             "last_updated":"2017-04-27T12:28:44+00:00"},\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"ad8f7896480b8081c8f0a2267ffd1859",\
+             "id":null,\
+             "source":"https://tortytherlediffecareette:*****@mikerhodestesty008.cloudant.com/moviesdb/",\
+             "target":"https://tomblench.cloudant.com/moviesdb_rep/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":5997,\
+                     "missing_revisions_found":5997,\
+                     "docs_read":5997,\
+                     "docs_written":5997,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"5997-g1AAAANreJy10UEKwjAQAMBgBcVP2BeUpEm1PdmfaDYJSKkVtB486U_0J_oBTz5AHyAI3jxIjUml1x7ayy67LDssmyKE-nNHIleCWK5ULIF6uVrnW4xDT6TLjeRZ7mUqT_VkhyMYFkWRzB3Q1XOhez3iczKKghor6jvg6giTiroYiuNQYYqbpeIfNa2oh72KhQGosFlq9qN2FfUyFPgUCKOnullXR7TXSWuHkvsYjjEWjQVvgTta7lRyV_szKgmRbVx3ttzNcs7AcEoKCHAb3N1y_9-9DYeBYzEiNTYlX3EcE0s"},\
+             "start_time":null,\
+             "last_updated":"2016-08-23T13:11:26+00:00"},\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"b63c053ecd95a4047b55ed8847b046f1",\
+             "id":null,\
+             "source":"https://tomblench.cloudant.com/atestdb2/",\
+             "target":"https://tomblench.cloudant.com/atestdb1/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":1,\
+                     "missing_revisions_found":1,\
+                     "docs_read":1,\
+                     "docs_written":1,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"2-g1AAAAFHeJyNjkEOgjAQRSdAYjyFN2jSFCtdyVU6nSKQWhJC13ozvVktsoEF0c2fTPL_-98BQNHmBCdCM4y2JuQMuxu6YJlxQyDtJ-bt5JIx04DXGGOvYRsR-xGsk-JjTrW5hnv6Dg0XplRngmPwZJvOW9ry5D7PF0nhmU5CvmZm9mVKVVacLr8pfy9fmt5L02q9qEhJbtbr-w-AQmfD"},\
+             "start_time":null,\
+             "last_updated":"2017-05-16T16:25:22+00:00"},\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"c71c9e69e30a182dc91d8938277bc85e",\
+             "id":null,\
+             "source":"https://tomblench.cloudant.com/animaldb/",\
+             "target":"https://tomblench.cloudant.com/animaldb_copy/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":15,\
+                     "missing_revisions_found":15,\
+                     "docs_read":15,\
+                     "docs_written":15,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"14-g1AAAAEueJzLYWBgYMlgTmGQSUlKzi9KdUhJMtTLTU1M0UvOyS9NScwr0ctLLckBqmJKZEiy____f1YGUyJrLlCAPdHEPCktJZk43UkOQDKpHmoAI9gAw2STxCTzJOIMyGMBkgwNQApoxv6sDGaoK0yN04wsk80IGEGKHQcgdoAdygxxaIplklFaWhYAu2FdOA"},\
+             "start_time":null,\
+             "last_updated":"2015-05-12T11:47:33+00:00"},\
+            {"database":"tomblench/_replicator",\
+             "doc_id":"e6242d1e9ce059b0388fc75af3116a39",\
+             "id":null,\
+             "source":"https://tomblench.cloudant.com/atestdb1/",\
+             "target":"https://tomblench.cloudant.com/atestdb2/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":1,\
+                     "missing_revisions_found":1,\
+                     "docs_read":1,\
+                     "docs_written":1,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"1-g1AAAAFheJyFzkEOgjAQBdBRSIyn8AZNgEJgJVeZ6bQCqSUhdK0305th1Q1dEDYzyWTy_rcAkHYJw4VJjZNumQpB_Y2s10LZ0TO6WTg92_B4RKDrsixDlyDcw-FUVUiFahjO3rE2vdMcY9k2Rm2Y9Ig8bWqspdz25Lbn0jDhGVYgX1_z8DMblnlp8n0lTir3kt7_pFV7NE2WYbluP3wATr5vQA"},\
+             "start_time":null,\
+             "last_updated":"2017-05-16T16:24:02+00:00"}]}')
 
         self.client.r_session.get = mock.Mock(return_value=m_response_ok)
         scheduler = Scheduler(self.client)
@@ -163,25 +161,24 @@ class SchedulerTests(UnitTestDbBase):
         Test scheduler doc
         """
         # set up mock response using a real captured response
-        m_response_ok = requests.Response()
-        m_response_ok.status_code = 200
-        m_response_ok.json = mock.Mock()
-        m_response_ok.json.return_value = {"database":"tomblench/_replicator",
-             "doc_id":"296e48244e003eba8764b2156b3bf302",
-             "id":None,
-             "source":"https://tomblench.cloudant.com/animaldb/",
-             "target":"https://tomblench.cloudant.com/animaldb_copy/",
-             "state":"completed",
-             "error_count":0,
-             "info":{"revisions_checked":15,
-                     "missing_revisions_found":2,
-                     "docs_read":2,
-                     "docs_written":2,
-                     "changes_pending":None,
-                     "doc_write_failures":0,
-                     "checkpointed_source_seq":"19-g1AAAAGjeJyVz10KwjAMB_BoJ4KX8AZF2tWPJ3eVpqnO0XUg27PeTG9Wa_VhwmT6kkDIPz_iACArGcGS0DRnWxDmHE9HdJ3lxjUdad9yb1sXF6cacB9CqEqmZ3UczKUh2uGhHxeD8U9i_Z3AIla8vJVJUlBIZYTqX5A_KMM7SfFZrHCNLUK3p7RIkl5tSRD-K6kx6f6S0k8sScpYJTb5uFQ9AI9Ch9c"},
-             "start_time":None,
-             "last_updated":"2017-04-13T14:53:50+00:00"};
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"database":"tomblench/_replicator",\
+             "doc_id":"296e48244e003eba8764b2156b3bf302",\
+             "id":null,\
+             "source":"https://tomblench.cloudant.com/animaldb/",\
+             "target":"https://tomblench.cloudant.com/animaldb_copy/",\
+             "state":"completed",\
+             "error_count":0,\
+             "info":{"revisions_checked":15,\
+                     "missing_revisions_found":2,\
+                     "docs_read":2,\
+                     "docs_written":2,\
+                     "changes_pending":null,\
+                     "doc_write_failures":0,\
+                     "checkpointed_source_seq":"19-g1AAAAGjeJyVz10KwjAMB_BoJ4KX8AZF2tWPJ3eVpqnO0XUg27PeTG9Wa_VhwmT6kkDIPz_iACArGcGS0DRnWxDmHE9HdJ3lxjUdad9yb1sXF6cacB9CqEqmZ3UczKUh2uGhHxeD8U9i_Z3AIla8vJVJUlBIZYTqX5A_KMM7SfFZrHCNLUK3p7RIkl5tSRD-K6kx6f6S0k8sScpYJTb5uFQ9AI9Ch9c"},\
+             "start_time":null,\
+             "last_updated":"2017-04-13T14:53:50+00:00"}')
         self.client.r_session.get = mock.Mock(return_value=m_response_ok)
         scheduler = Scheduler(self.client)
         response = scheduler.get_doc("296e48244e003eba8764b2156b3bf302")
@@ -197,23 +194,22 @@ class SchedulerTests(UnitTestDbBase):
         Test scheduler jobs
         """
         # set up mock response using a real captured response
-        m_response_ok = requests.Response()
-        m_response_ok.status_code = 200
-        m_response_ok.json = mock.Mock()
-        m_response_ok.json.return_value = {"total_rows":1,"offset":0,
-                              "jobs":[{"database":None,
-                                       "id":"f11105eaaded4981d21ff8ebf846f48b+create_target",
-                                       "pid":"<0.5866.6800>",
-                                       "source":"https://clientlibs-test:*****@clientlibs-test.cloudant.com/largedb1g/",
-                                       "target":"https://tomblench:*****@tomblench.cloudant.com/largedb1g/",
-                                       "user":"tomblench",
-                                       "doc_id":None,
-                                       "history":[{"timestamp":"2018-04-12T13:06:20Z",
-                                                   "type":"started"},
-                                                  {"timestamp":"2018-04-12T13:06:20Z",
-                                                   "type":"added"}],
-                                       "node":"dbcore@db2.bigblue.cloudant.net",
-                                       "start_time":"2018-04-12T13:06:20Z"}]}        
+        m_response_ok = mock.MagicMock()
+        type(m_response_ok).status_code = mock.PropertyMock(return_value=200)
+        type(m_response_ok).text = mock.PropertyMock(return_value='{"total_rows":1,"offset":0,\
+                              "jobs":[{"database":null,\
+                                       "id":"f11105eaaded4981d21ff8ebf846f48b+create_target",\
+                                       "pid":"<0.5866.6800>",\
+                                       "source":"https://clientlibs-test:*****@clientlibs-test.cloudant.com/largedb1g/",\
+                                       "target":"https://tomblench:*****@tomblench.cloudant.com/largedb1g/",\
+                                       "user":"tomblench",\
+                                       "doc_id":null,\
+                                       "history":[{"timestamp":"2018-04-12T13:06:20Z",\
+                                                   "type":"started"},\
+                                                  {"timestamp":"2018-04-12T13:06:20Z",\
+                                                   "type":"added"}],\
+                                       "node":"dbcore@db2.bigblue.cloudant.net",\
+                                       "start_time":"2018-04-12T13:06:20Z"}]}')
         self.client.r_session.get = mock.Mock(return_value=m_response_ok)
         scheduler = Scheduler(self.client)
         response = scheduler.list_jobs(skip=0, limit=10)