diff --git a/addons/website/tests/test_qweb.py b/addons/website/tests/test_qweb.py
index 9cc199291a98e23727bff95c8f0ae76b4269b746..7343b3910df4e53ac4b9da540a7446e96523f790 100644
--- a/addons/website/tests/test_qweb.py
+++ b/addons/website/tests/test_qweb.py
@@ -3,7 +3,7 @@
 
 import re
 
-from odoo import tools
+from odoo import http, tools
 from odoo.addons.website.tools import MockRequest
 from odoo.modules.module import get_module_resource
 from odoo.tests.common import TransactionCase
@@ -139,15 +139,12 @@ class TestQwebProcessAtt(TransactionCase):
             self._test_att('/my-page', {'href': '/fr/my-page'})
 
     def test_process_att_url_crap(self):
-        with MockRequest(self.env, website=self.website) as request:
+        with MockRequest(self.env, website=self.website):
+            match = http.root.get_db_router.return_value.bind.return_value.match
             # #{fragment} is stripped from URL when testing route
             self._test_att('/x#y?z', {'href': '/x#y?z'})
-            self.assertEqual(
-                request.httprequest.app._log_call[-1],
-                (('/x',), {'method': 'POST', 'query_args': None})
-            )
+            match.assert_called_with('/x', method='POST', query_args=None)
+
+            match.reset_calls()
             self._test_att('/x?y#z', {'href': '/x?y#z'})
-            self.assertEqual(
-                request.httprequest.app._log_call[-1],
-                (('/x',), {'method': 'POST', 'query_args': 'y'})
-            )
+            match.assert_called_with('/x', method='POST', query_args='y')
diff --git a/addons/website/tools.py b/addons/website/tools.py
index fa1eee4f47395b7a2215d7e296c2d4ec0fb20616..6930f8a5e68b276885f64d9c1bebb95b5e43977b 100644
--- a/addons/website/tools.py
+++ b/addons/website/tools.py
@@ -1,11 +1,13 @@
 # -*- encoding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import odoo
+import contextlib
 import re
+from unittest.mock import Mock, MagicMock, patch
+
 import werkzeug
 
-from odoo.tools import DotDict
+import odoo
+from odoo.tools.misc import DotDict
 
 
 def get_video_embed_code(video_url):
@@ -55,71 +57,58 @@ def get_video_embed_code(video_url):
         return '<iframe class="embed-responsive-item" src="%s" allowFullScreen="true" frameborder="0"></iframe>' % embedUrl
 
 
-class MockObject(object):
-    _log_call = []
-
-    def __init__(self, *args, **kwargs):
-        self.__dict__ = kwargs
-
-    def __call__(self, *args, **kwargs):
-        self._log_call.append((args, kwargs))
-        return self
-
-    def __getitem__(self, index):
-        return self
-
-
 def werkzeugRaiseNotFound(*args, **kwargs):
     raise werkzeug.exceptions.NotFound()
 
 
-class MockRequest(object):
-    """ Class with context manager mocking odoo.http.request for tests """
-    def __init__(self, env, **kw):
-        app = MockObject(routing={
+@contextlib.contextmanager
+def MockRequest(
+        env, *, routing=True, multilang=True,
+        context=None,
+        cookies=None, country_code=None, website=None, sale_order_id=None
+):
+    router = MagicMock()
+    match = router.return_value.bind.return_value.match
+    if routing:
+        match.return_value[0].routing = {
             'type': 'http',
             'website': True,
-            'multilang': kw.get('multilang', True),
-        })
-        app.get_db_router = app.bind = app.match = app
-        if not kw.get('routing', True):
-            app.match = werkzeugRaiseNotFound
-
-        lang = kw.get('lang')
-        if not lang:
-            lang_code = kw.get('context', {}).get('lang', env.context.get('lang', 'en_US'))
-            lang = env['res.lang']._lang_get(lang_code)
-
-        context = kw.get('context', {})
-        context.setdefault('lang', lang_code)
-
-        self.request = DotDict({
-            'context': context,
-            'db': None,
-            'env': env,
-            'httprequest': {
-                'path': '/hello/',
-                'app': app,
-                'environ': {
-                    'REMOTE_ADDR': '127.0.0.1',
-                },
-                'cookies': kw.get('cookies', {}),
-            },
-            'lang': lang,
-            'redirect': werkzeug.utils.redirect,
-            'session': {
-                'geoip': {
-                    'country_code': kw.get('country_code'),
-                },
-                'debug': False,
-                'sale_order_id': kw.get('sale_order_id'),
-            },
-            'website': kw.get('website'),
-        })
-
-    def __enter__(self):
-        odoo.http._request_stack.push(self.request)
-        return self.request
-
-    def __exit__(self, exc_type, exc_value, traceback):
-        odoo.http._request_stack.pop()
+            'multilang': multilang
+        }
+    else:
+        match.side_effect = werkzeugRaiseNotFound
+
+    if context is None:
+        context = {}
+    lang_code = context.get('lang', env.context.get('lang', 'en_US'))
+    context.setdefault('lang', lang_code)
+
+    request = Mock(
+        context=context,
+        db=None,
+        endpoint=match.return_value[0] if routing else None,
+        env=env,
+        httprequest=Mock(
+            host='localhost',
+            path='/hello/',
+            app=odoo.http.root,
+            environ={'REMOTE_ADDR': '127.0.0.1'},
+            cookies=cookies or {},
+            referrer='',
+        ),
+        lang=env['res.lang']._lang_get(lang_code),
+        redirect=werkzeug.utils.redirect,
+        session=DotDict(
+            geoip={'country_code': country_code},
+            debug=False,
+            sale_order_id=sale_order_id,
+        ),
+        website=website
+    )
+
+    with contextlib.ExitStack() as s:
+        odoo.http._request_stack.push(request)
+        s.callback(odoo.http._request_stack.pop)
+        s.enter_context(patch('odoo.http.root.get_db_router', router))
+
+        yield request