Introduce a proper mock for the RestApiService

Simplify all stubbing of the REST API in tests by adding a canonical
`stubRestApi` method.

Make sure to never use the <gr-rest-api-interface> element directly
anymore, thus paving the way for converting the element into a service
object and moving into to the services directory.

The reviewer should with looking at all non-test files first.

Change-Id: Ida7c9f0482477a0971fda106f18b8c51e20597c6
diff --git a/polygerrit-ui/app/constants/constants.ts b/polygerrit-ui/app/constants/constants.ts
index 061c5cd..50be7ee 100644
--- a/polygerrit-ui/app/constants/constants.ts
+++ b/polygerrit-ui/app/constants/constants.ts
@@ -18,6 +18,9 @@
 /**
  * @desc Tab names for primary tabs on change view page.
  */
+import {DiffPreferencesInfo} from '../types/diff';
+import {EditPreferencesInfo, PreferencesInfo} from '../types/common';
+
 export enum PrimaryTab {
   FILES = 'files',
   /**
@@ -390,3 +393,60 @@
   REF_UPDATED_AND_CHANGE_REINDEX = 'REF_UPDATED_AND_CHANGE_REINDEX',
   NEVER = 'NEVER',
 }
+
+// TODO(TS): Many properties are omitted here, but they are required.
+// Add default values for missing properties.
+export function createDefaultPreferences() {
+  return {
+    changes_per_page: 25,
+    default_diff_view: DiffViewMode.SIDE_BY_SIDE,
+    diff_view: DiffViewMode.SIDE_BY_SIDE,
+    size_bar_in_change_table: true,
+  } as PreferencesInfo;
+}
+
+// These defaults should match the defaults in
+// java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
+// NOTE: There are some settings that don't apply to PolyGerrit
+// (Render mode being at least one of them).
+export function createDefaultDiffPrefs(): DiffPreferencesInfo {
+  return {
+    auto_hide_diff_table_header: true,
+    context: 10,
+    cursor_blink_rate: 0,
+    font_size: 12,
+    ignore_whitespace: 'IGNORE_NONE',
+    intraline_difference: true,
+    line_length: 100,
+    line_wrapping: false,
+    show_line_endings: true,
+    show_tabs: true,
+    show_whitespace_errors: true,
+    syntax_highlighting: true,
+    tab_size: 8,
+    theme: 'DEFAULT',
+  };
+}
+
+// These defaults should match the defaults in
+// java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
+export function createDefaultEditPrefs(): EditPreferencesInfo {
+  return {
+    auto_close_brackets: false,
+    cursor_blink_rate: 0,
+    hide_line_numbers: false,
+    hide_top_menu: false,
+    indent_unit: 2,
+    indent_with_tabs: false,
+    key_map_type: 'DEFAULT',
+    line_length: 100,
+    line_wrapping: false,
+    match_brackets: true,
+    show_base: false,
+    show_tabs: true,
+    show_whitespace_errors: true,
+    syntax_highlighting: true,
+    tab_size: 8,
+    theme: 'DEFAULT',
+  };
+}
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
index 77a2a41..ed196e4 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-group-list/gr-admin-group-list_test.js
@@ -19,6 +19,7 @@
 import './gr-admin-group-list.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import 'lodash/lodash.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-admin-group-list');
 
@@ -73,24 +74,16 @@
   });
 
   suite('list with groups', () => {
-    setup(done => {
+    setup(async () => {
       groups = _.times(26, groupGenerator);
-
-      stub('gr-rest-api-interface', {
-        getGroups(num, offset) {
-          return Promise.resolve(groups);
-        },
-      });
-
-      element._paramsChanged(value).then(() => { flush(done); });
+      stubRestApi('getGroups').returns(Promise.resolve(groups));
+      element._paramsChanged(value);
+      await flush();
     });
 
-    test('test for test group in the list', done => {
-      flush(() => {
-        assert.equal(element._groups[1].name, '1');
-        assert.equal(element._groups[1].options.visible_to_all, false);
-        done();
-      });
+    test('test for test group in the list', () => {
+      assert.equal(element._groups[1].name, '1');
+      assert.equal(element._groups[1].options.visible_to_all, false);
     });
 
     test('_shownGroups', () => {
@@ -113,13 +106,7 @@
   suite('test with less then 25 groups', () => {
     setup(done => {
       groups = _.times(25, groupGenerator);
-
-      stub('gr-rest-api-interface', {
-        getGroups(num, offset) {
-          return Promise.resolve(groups);
-        },
-      });
-
+      stubRestApi('getGroups').returns(Promise.resolve(groups));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -129,20 +116,15 @@
   });
 
   suite('filter', () => {
-    test('_paramsChanged', done => {
-      sinon.stub(
-          element.restApiService,
-          'getGroups')
-          .callsFake(() => Promise.resolve(groups));
+    test('_paramsChanged', async () => {
+      const getGroupsStub = stubRestApi('getGroups');
+      getGroupsStub.returns(Promise.resolve(groups));
       const value = {
         filter: 'test',
         offset: 25,
       };
-      element._paramsChanged(value).then(() => {
-        assert.isTrue(element.restApiService.getGroups.lastCall
-            .calledWithExactly('test', 25, 25));
-        done();
-      });
+      await element._paramsChanged(value);
+      assert.isTrue(getGroupsStub.lastCall.calledWithExactly('test', 25, 25));
     });
   });
 
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
index cac409d..33ad694 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view_test.js
@@ -22,19 +22,24 @@
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
 import {GerritView} from '../../../services/router/router-model.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-admin-view');
 
+function createAdminCapabilities() {
+  return {
+    createGroup: true,
+    createProject: true,
+    viewPlugins: true,
+  };
+}
+
 suite('gr-admin-view tests', () => {
   let element;
 
   setup(done => {
     element = basicFixture.instantiate();
-    stub('gr-rest-api-interface', {
-      getProjectConfig() {
-        return Promise.resolve({});
-      },
-    });
+    stubRestApi('getProjectConfig').returns(Promise.resolve({}));
     const pluginsLoaded = Promise.resolve();
     sinon.stub(getPluginLoader(), 'awaitPluginsLoaded').returns(pluginsLoaded);
     pluginsLoaded.then(() => flush(done));
@@ -84,18 +89,11 @@
   });
 
   test('_filteredLinks admin', done => {
-    sinon.stub(element.restApiService, 'getAccount').returns(Promise.resolve({
+    stubRestApi('getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        })
-        );
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
     element.reload().then(() => {
       assert.equal(element._filteredLinks.length, 3);
 
@@ -111,38 +109,25 @@
     });
   });
 
-  test('_filteredLinks non admin authenticated', done => {
-    sinon.stub(element.restApiService, 'getAccount').returns(Promise.resolve({
-      name: 'test-user',
-    }));
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({})
-        );
-    element.reload().then(() => {
-      assert.equal(element._filteredLinks.length, 2);
-
-      // Repos
-      assert.isNotOk(element._filteredLinks[0].subsection);
-
-      // Groups
-      assert.isNotOk(element._filteredLinks[0].subsection);
-      done();
-    });
+  test('_filteredLinks non admin authenticated', async () => {
+    await element.reload();
+    assert.equal(element._filteredLinks.length, 2);
+    // Repos
+    assert.isNotOk(element._filteredLinks[0].subsection);
+    // Groups
+    assert.isNotOk(element._filteredLinks[0].subsection);
   });
 
-  test('_filteredLinks non admin unathenticated', done => {
-    element.reload().then(() => {
-      assert.equal(element._filteredLinks.length, 1);
-
-      // Repos
-      assert.isNotOk(element._filteredLinks[0].subsection);
-      done();
-    });
+  test('_filteredLinks non admin unathenticated', async () => {
+    stubRestApi('getAccount').returns(Promise.resolve(undefined));
+    await element.reload();
+    assert.equal(element._filteredLinks.length, 1);
+    // Repos
+    assert.isNotOk(element._filteredLinks[0].subsection);
   });
 
   test('_filteredLinks from plugin', () => {
+    stubRestApi('getAccount').returns(Promise.resolve(undefined));
     sinon.stub(element.$.jsAPI, 'getAdminMenuLinks').returns([
       {text: 'internal link text', url: '/internal/link/url'},
       {text: 'external link text', url: 'http://external/link/url'},
@@ -172,17 +157,11 @@
 
   test('Repo shows up in nav', done => {
     element._repoName = 'Test Repo';
-    sinon.stub(element.restApiService, 'getAccount').returns(Promise.resolve({
+    stubRestApi('getAccount').returns(Promise.resolve({
       name: 'test-user',
     }));
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        }));
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
     element.reload().then(() => {
       flush();
       assert.equal(dom(element.root)
@@ -197,53 +176,31 @@
     });
   });
 
-  test('Group shows up in nav', done => {
+  test('Group shows up in nav', async () => {
     element._groupId = 'a15262';
     element._groupName = 'my-group';
     element._groupIsInternal = true;
     element._isAdmin = true;
     element._groupOwner = false;
-    sinon.stub(element.restApiService, 'getAccount').returns(Promise.resolve({
-      name: 'test-user',
-    }));
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        }));
-    element.reload().then(() => {
-      flush();
-      assert.equal(element._filteredLinks.length, 3);
-
-      // Repos
-      assert.isNotOk(element._filteredLinks[0].subsection);
-
-      // Groups
-      assert.equal(element._filteredLinks[1].subsection.children.length, 2);
-      assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
-
-      // Plugins
-      assert.isNotOk(element._filteredLinks[2].subsection);
-      done();
-    });
+    stubRestApi('getAccount').returns(Promise.resolve({name: 'test-user'}));
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
+    await element.reload();
+    await flush();
+    assert.equal(element._filteredLinks.length, 3);
+    // Repos
+    assert.isNotOk(element._filteredLinks[0].subsection);
+    // Groups
+    assert.equal(element._filteredLinks[1].subsection.children.length, 2);
+    assert.equal(element._filteredLinks[1].subsection.name, 'my-group');
+    // Plugins
+    assert.isNotOk(element._filteredLinks[2].subsection);
   });
 
   test('Nav is reloaded when repo changes', () => {
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        }));
-    sinon.stub(
-        element.restApiService,
-        'getAccount')
-        .callsFake(() => Promise.resolve({_id: 1}));
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
+    stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
     sinon.stub(element, 'reload');
     element.params = {repo: 'Test Repo', view: GerritView.REPO};
     assert.equal(element.reload.callCount, 1);
@@ -254,18 +211,9 @@
 
   test('Nav is reloaded when group changes', () => {
     sinon.stub(element, '_computeGroupName');
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        }));
-    sinon.stub(
-        element.restApiService,
-        'getAccount')
-        .callsFake(() => Promise.resolve({_id: 1}));
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
+    stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
     sinon.stub(element, 'reload');
     element.params = {groupId: '1', view: GerritView.GROUP};
     assert.equal(element.reload.callCount, 1);
@@ -321,18 +269,9 @@
       view: GerritNav.View.REPO,
       detail: GerritNav.RepoDetailView.ACCESS,
     };
-    sinon.stub(
-        element.restApiService,
-        'getAccountCapabilities')
-        .callsFake(() => Promise.resolve({
-          createGroup: true,
-          createProject: true,
-          viewPlugins: true,
-        }));
-    sinon.stub(
-        element.restApiService,
-        'getAccount')
-        .callsFake(() => Promise.resolve({_id: 1}));
+    stubRestApi('getAccountCapabilities').returns(
+        Promise.resolve(createAdminCapabilities()));
+    stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
     flush();
     const expectedFilteredLinks = [
       {
@@ -484,19 +423,9 @@
 
   suite('_computeSelectedClass', () => {
     setup(() => {
-      sinon.stub(
-          element.restApiService,
-          'getAccountCapabilities')
-          .callsFake(() => Promise.resolve({
-            createGroup: true,
-            createProject: true,
-            viewPlugins: true,
-          }));
-      sinon.stub(
-          element.restApiService,
-          'getAccount')
-          .callsFake(() => Promise.resolve({_id: 1}));
-
+      stubRestApi('getAccountCapabilities').returns(
+          Promise.resolve(createAdminCapabilities()));
+      stubRestApi('getAccount').returns(Promise.resolve({_id: 1}));
       return element.reload();
     });
 
@@ -569,6 +498,7 @@
     });
 
     suite('groups', () => {
+      let getGroupConfigStub;
       setup(() => {
         stub('gr-group', {
           _loadGroup: () => Promise.resolve({}),
@@ -577,12 +507,12 @@
           _loadGroupDetails: () => {},
         });
 
-        sinon.stub(element.restApiService, 'getGroupConfig')
-            .returns(Promise.resolve({
-              name: 'foo',
-              id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
-            }));
-        sinon.stub(element.restApiService, 'getIsGroupOwner')
+        getGroupConfigStub = stubRestApi('getGroupConfig');
+        getGroupConfigStub.returns(Promise.resolve({
+          name: 'foo',
+          id: 'c0f83e941ce90caea30e6ad88f0d4ea0e841a7a9',
+        }));
+        stubRestApi('getIsGroupOwner')
             .returns(Promise.resolve(true));
         return element.reload();
       });
@@ -620,12 +550,10 @@
       });
 
       test('external group', () => {
-        element.restApiService.getGroupConfig.restore();
-        sinon.stub(element.restApiService, 'getGroupConfig')
-            .returns(Promise.resolve({
-              name: 'foo',
-              id: 'external-id',
-            }));
+        getGroupConfigStub.returns(Promise.resolve({
+          name: 'foo',
+          id: 'external-id',
+        }));
         element.params = {
           view: GerritNav.View.GROUP,
           groupId: 1234,
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
index 0a4e23d..198794e 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog_test.ts
@@ -21,6 +21,7 @@
 import {BranchName, GitRef, RepoName} from '../../../types/common';
 import {InheritedBooleanInfoConfiguredValue} from '../../../constants/constants';
 import {createChange, createConfig} from '../../../test/test-data-generators';
+import {stubRestApi} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-create-change-dialog');
 
@@ -28,23 +29,18 @@
   let element: GrCreateChangeDialog;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() {
-        return Promise.resolve(true);
-      },
-      getRepoBranches(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve([
-            {
-              ref: 'refs/heads/test-branch' as GitRef,
-              revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
-              can_delete: true,
-            },
-          ]);
-        } else {
-          return Promise.resolve([]);
-        }
-      },
+    stubRestApi('getRepoBranches').callsFake((input: string) => {
+      if (input.startsWith('test')) {
+        return Promise.resolve([
+          {
+            ref: 'refs/heads/test-branch' as GitRef,
+            revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+            can_delete: true,
+          },
+        ]);
+      } else {
+        return Promise.resolve([]);
+      }
     });
     element = basicFixture.instantiate();
     element.repoName = 'test-repo' as RepoName;
@@ -67,9 +63,9 @@
       work_in_progress: true,
     };
 
-    const saveStub = sinon
-      .stub(element.restApiService, 'createChange')
-      .callsFake(() => Promise.resolve(createChange()));
+    const saveStub = stubRestApi('createChange').returns(
+      Promise.resolve(createChange())
+    );
 
     element.branch = 'test-branch' as BranchName;
     element.topic = 'test-topic';
@@ -103,9 +99,9 @@
       work_in_progress: true,
     };
 
-    const saveStub = sinon
-      .stub(element.restApiService, 'createChange')
-      .callsFake(() => Promise.resolve(createChange()));
+    const saveStub = stubRestApi('createChange').returns(
+      Promise.resolve(createChange())
+    );
 
     element.branch = 'test-branch' as BranchName;
     element.topic = 'test-topic';
diff --git a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
index bf0c244..af33691 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-group-dialog/gr-create-group-dialog_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-create-group-dialog.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-create-group-dialog');
 
@@ -27,9 +28,6 @@
   const GROUP_NAME = 'test-group';
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
     element = basicFixture.instantiate();
   });
 
@@ -47,11 +45,8 @@
   });
 
   test('test for redirecting to group on successful creation', done => {
-    sinon.stub(element.restApiService, 'createGroup')
-        .returns(Promise.resolve({status: 201}));
-
-    sinon.stub(element.restApiService, 'getGroupConfig')
-        .returns(Promise.resolve({group_id: 551}));
+    stubRestApi('createGroup').returns(Promise.resolve({status: 201}));
+    stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
 
     const showStub = sinon.stub(page, 'show');
     element.handleCreateGroup()
@@ -62,11 +57,8 @@
   });
 
   test('test for unsuccessful group creation', done => {
-    sinon.stub(element.restApiService, 'createGroup')
-        .returns(Promise.resolve({status: 409}));
-
-    sinon.stub(element.restApiService, 'getGroupConfig')
-        .returns(Promise.resolve({group_id: 551}));
+    stubRestApi('createGroup').returns(Promise.resolve({status: 409}));
+    stubRestApi('getGroupConfig').returns(Promise.resolve({group_id: 551}));
 
     const showStub = sinon.stub(page, 'show');
     element.handleCreateGroup()
diff --git a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
index 9c281b1..60af4d5 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-pointer-dialog/gr-create-pointer-dialog_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-create-pointer-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-create-pointer-dialog');
 
@@ -28,17 +29,11 @@
   };
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
     element = basicFixture.instantiate();
   });
 
   test('branch created', done => {
-    sinon.stub(
-        element.restApiService,
-        'createRepoBranch')
-        .callsFake(() => Promise.resolve({}));
+    stubRestApi('createRepoBranch').returns(Promise.resolve({}));
 
     assert.isFalse(element.hasNewItemName);
 
@@ -57,10 +52,7 @@
   });
 
   test('tag created', done => {
-    sinon.stub(
-        element.restApiService,
-        'createRepoTag')
-        .callsFake(() => Promise.resolve({}));
+    stubRestApi('createRepoTag').returns(Promise.resolve({}));
 
     assert.isFalse(element.hasNewItemName);
 
@@ -79,10 +71,7 @@
   });
 
   test('tag created with annotations', done => {
-    sinon.stub(
-        element.restApiService,
-        'createRepoTag')
-        .callsFake(() => Promise.resolve({}));
+    stubRestApi('createRepoTag').returns(() => Promise.resolve({}));
 
     assert.isFalse(element.hasNewItemName);
 
diff --git a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
index 0af2e23..f10141a 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-create-repo-dialog/gr-create-repo-dialog_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-create-repo-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-create-repo-dialog');
 
@@ -24,9 +25,6 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
     element = basicFixture.instantiate();
   });
 
@@ -44,8 +42,7 @@
       owners: ['testId'],
     };
 
-    const saveStub = sinon.stub(element.restApiService,
-        'createRepo').callsFake(() => Promise.resolve({}));
+    const saveStub = stubRestApi('createRepo').returns(Promise.resolve({}));
 
     assert.isFalse(element.hasNewRepoName);
 
diff --git a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js
index ce4e4c3..25fd854 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-audit-log/gr-group-audit-log_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-group-audit-log.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group-audit-log');
 
@@ -79,11 +80,9 @@
       element.groupId = 1;
 
       const response = {status: 404};
-      sinon.stub(
-          element.restApiService, 'getGroupAuditLog')
-          .callsFake((group, errFn) => {
-            errFn(response);
-          });
+      stubRestApi('getGroupAuditLog').callsFake((group, errFn) => {
+        errFn(response);
+      });
 
       element.addEventListener('page-error', e => {
         assert.deepEqual(e.detail.response, response);
diff --git a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
index 59820b8..c09ac1e 100644
--- a/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group-members/gr-group-members_test.js
@@ -18,7 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-group-members.js';
 import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
-import {stubBaseUrl} from '../../../test/test-utils.js';
+import {stubBaseUrl, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group-members');
 
@@ -78,70 +78,52 @@
     },
     ];
 
-    stub('gr-rest-api-interface', {
-      getSuggestedAccounts(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve([
-            {
-              _account_id: 1000096,
-              name: 'test-account',
-              email: 'test.account@example.com',
-              username: 'test123',
-            },
-            {
-              _account_id: 1001439,
-              name: 'test-admin',
-              email: 'test.admin@example.com',
-              username: 'test_admin',
-            },
-            {
-              _account_id: 1001439,
-              name: 'test-git',
-              username: 'test_git',
-            },
-          ]);
-        } else {
-          return Promise.resolve({});
-        }
-      },
-      getSuggestedGroups(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve({
-            'test-admin': {
-              id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
-            },
-            'test/Administrator (admin)': {
-              id: 'test%3Aadmin',
-            },
-          });
-        } else {
-          return Promise.resolve({});
-        }
-      },
-      getLoggedIn() { return Promise.resolve(true); },
-      getConfig() {
-        return Promise.resolve();
-      },
-      getGroupMembers() {
-        return Promise.resolve(groupMembers);
-      },
-      getIsGroupOwner() {
-        return Promise.resolve(true);
-      },
-      getIncludedGroup() {
-        return Promise.resolve(includedGroups);
-      },
-      getAccountCapabilities() {
-        return Promise.resolve();
-      },
+    stubRestApi('getSuggestedAccounts').callsFake(input => {
+      if (input.startsWith('test')) {
+        return Promise.resolve([
+          {
+            _account_id: 1000096,
+            name: 'test-account',
+            email: 'test.account@example.com',
+            username: 'test123',
+          },
+          {
+            _account_id: 1001439,
+            name: 'test-admin',
+            email: 'test.admin@example.com',
+            username: 'test_admin',
+          },
+          {
+            _account_id: 1001439,
+            name: 'test-git',
+            username: 'test_git',
+          },
+        ]);
+      } else {
+        return Promise.resolve({});
+      }
     });
+    stubRestApi('getSuggestedGroups').callsFake(input => {
+      if (input.startsWith('test')) {
+        return Promise.resolve({
+          'test-admin': {
+            id: '1ce023d3fb4e4260776fb92cd08b52bbd21ce70a',
+          },
+          'test/Administrator (admin)': {
+            id: 'test%3Aadmin',
+          },
+        });
+      } else {
+        return Promise.resolve({});
+      }
+    });
+    stubRestApi('getGroupMembers').returns(Promise.resolve(groupMembers));
+    stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+    stubRestApi('getIncludedGroup').returns(Promise.resolve(includedGroups));
     element = basicFixture.instantiate();
     stubBaseUrl('https://test/site');
     element.groupId = 1;
-    groupStub = sinon.stub(
-        element.restApiService,
-        'getGroupConfig')
-        .callsFake(() => Promise.resolve(groups));
+    groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(groups));
     return element._loadGroupDetails();
   });
 
@@ -162,7 +144,7 @@
 
     const memberName = 'test-admin';
 
-    const saveStub = sinon.stub(element.restApiService, 'saveGroupMember')
+    const saveStub = stubRestApi('saveGroupMember')
         .callsFake(() => Promise.resolve({}));
 
     const button = element.$.saveGroupMember;
@@ -187,8 +169,7 @@
 
     const includedGroupName = 'testName';
 
-    const saveIncludedGroupStub = sinon.stub(
-        element.restApiService, 'saveIncludedGroup')
+    const saveIncludedGroupStub = stubRestApi('saveIncludedGroup')
         .callsFake(() => Promise.resolve({}));
 
     const button = element.$.saveIncludedGroups;
@@ -219,8 +200,14 @@
       status: 404,
       ok: false,
     };
-    sinon.stub(element.restApiService._restApiHelper, 'fetch').callsFake(
-        () => Promise.resolve(errorResponse));
+    stubRestApi('saveIncludedGroup').callsFake((
+        groupName,
+        includedGroup,
+        errFn
+    ) => {
+      errFn(errorResponse);
+      return Promise.resolve(undefined);
+    });
 
     element.$.groupMemberSearchInput.text = memberName;
     element.$.groupMemberSearchInput.value = 1234;
@@ -232,13 +219,8 @@
 
   test('add included group network-error throws an exception', async () => {
     element._groupOwner = true;
-
     const memberName = 'bad-name';
-    const alertStub = sinon.stub();
-    element.addEventListener('show-alert', alertStub);
-    const err = new Error();
-    sinon.stub(element.restApiService._restApiHelper, 'fetch').callsFake(
-        () => Promise.reject(err));
+    stubRestApi('saveIncludedGroup').throws(new Error());
 
     element.$.groupMemberSearchInput.text = memberName;
     element.$.groupMemberSearchInput.value = 1234;
@@ -366,8 +348,7 @@
     element.groupId = 1;
 
     const response = {status: 404};
-    sinon.stub(
-        element.restApiService, 'getGroupConfig')
+    stubRestApi('getGroupConfig')
         .callsFake((group, errFn) => {
           errFn(response);
         });
diff --git a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
index c006251..4a80946 100644
--- a/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-group/gr-group_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-group.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group');
 
@@ -36,14 +37,8 @@
   };
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-    });
     element = basicFixture.instantiate();
-    groupStub = sinon.stub(
-        element.restApiService,
-        'getGroupConfig')
-        .callsFake(() => Promise.resolve(group));
+    groupStub = stubRestApi('getGroupConfig').returns(Promise.resolve(group));
   });
 
   test('loading displays before group config is loaded', () => {
@@ -55,10 +50,7 @@
   });
 
   test('default values are populated with internal group', done => {
-    sinon.stub(
-        element.restApiService,
-        'getIsGroupOwner')
-        .callsFake(() => Promise.resolve(true));
+    stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = 1;
     element._loadGroup().then(() => {
       assert.isTrue(element._groupIsInternal);
@@ -71,14 +63,9 @@
     const groupExternal = {...group};
     groupExternal.id = 'external-group-id';
     groupStub.restore();
-    groupStub = sinon.stub(
-        element.restApiService,
-        'getGroupConfig')
-        .callsFake(() => Promise.resolve(groupExternal));
-    sinon.stub(
-        element.restApiService,
-        'getIsGroupOwner')
-        .callsFake(() => Promise.resolve(true));
+    groupStub = stubRestApi('getGroupConfig').returns(
+        Promise.resolve(groupExternal));
+    stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
     element.groupId = 1;
     element._loadGroup().then(() => {
       assert.isFalse(element._groupIsInternal);
@@ -96,15 +83,8 @@
     };
     element._groupName = groupName;
 
-    sinon.stub(
-        element.restApiService,
-        'getIsGroupOwner')
-        .callsFake(() => Promise.resolve(true));
-
-    sinon.stub(
-        element.restApiService,
-        'saveGroupName')
-        .callsFake(() => Promise.resolve({status: 200}));
+    stubRestApi('getIsGroupOwner').returns(Promise.resolve(true));
+    stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
 
     const button = element.$.inputUpdateNameBtn;
 
@@ -135,10 +115,7 @@
     element._groupConfigOwner = 'testId';
     element._groupOwner = true;
 
-    sinon.stub(
-        element.restApiService,
-        'getIsGroupOwner')
-        .callsFake(() => Promise.resolve({status: 200}));
+    stubRestApi('getIsGroupOwner').returns(Promise.resolve({status: 200}));
 
     const button = element.$.inputUpdateOwnerBtn;
 
@@ -162,10 +139,7 @@
   test('test for undefined group name', done => {
     groupStub.restore();
 
-    sinon.stub(
-        element.restApiService,
-        'getGroupConfig')
-        .callsFake(() => Promise.resolve({}));
+    stubRestApi('getGroupConfig').returns(Promise.resolve({}));
 
     assert.isUndefined(element.groupId);
 
@@ -189,8 +163,7 @@
       name: 'test-group',
     };
     element.groupId = 'gg';
-    sinon.stub(element.restApiService, 'saveGroupName')
-        .returns(Promise.resolve({status: 200}));
+    stubRestApi('saveGroupName').returns(Promise.resolve({status: 200}));
 
     const showStub = sinon.stub(element, 'dispatchEvent');
     element._handleSaveName()
@@ -239,8 +212,7 @@
     element.groupId = 1;
 
     const response = {status: 404};
-    sinon.stub(
-        element.restApiService, 'getGroupConfig').callsFake((group, errFn) => {
+    stubRestApi('getGroupConfig').callsFake((group, errFn) => {
       errFn(response);
     });
 
diff --git a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
index c9b0131..d5668d8 100644
--- a/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-permission/gr-permission_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-permission.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-permission');
 
@@ -25,7 +26,7 @@
 
   setup(() => {
     element = basicFixture.instantiate();
-    sinon.stub(element.restApiService, 'getSuggestedGroups').returns(
+    stubRestApi('getSuggestedGroups').returns(
         Promise.resolve({
           'Administrators': {
             id: '4c97682e6ce61b7247f3381b6f1789356666de7f',
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
index 6aa247f..82e0f8c 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-plugin-list.js';
 import 'lodash/lodash.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-plugin-list');
 
@@ -54,13 +55,7 @@
   suite('list with plugins', () => {
     setup(done => {
       plugins = _.times(26, pluginGenerator);
-
-      stub('gr-rest-api-interface', {
-        getPlugins(num, offset) {
-          return Promise.resolve(plugins);
-        },
-      });
-
+      stubRestApi('getPlugins').returns(Promise.resolve(plugins));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -113,13 +108,7 @@
   suite('list with less then 26 plugins', () => {
     setup(done => {
       plugins = _.times(25, pluginGenerator);
-
-      stub('gr-rest-api-interface', {
-        getPlugins(num, offset) {
-          return Promise.resolve(plugins);
-        },
-      });
-
+      stubRestApi('getPlugins').returns(Promise.resolve(plugins));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -129,24 +118,17 @@
   });
 
   suite('filter', () => {
-    test('_paramsChanged', done => {
-      sinon.stub(
-          element.restApiService,
-          'getPlugins')
-          .callsFake(() => Promise.resolve(plugins));
+    test('_paramsChanged', async () => {
+      const getPluginsStub = stubRestApi('getPlugins');
+      getPluginsStub.returns(Promise.resolve(plugins));
       const value = {
         filter: 'test',
         offset: 25,
       };
-      element._paramsChanged(value).then(() => {
-        assert.equal(element.restApiService.getPlugins.lastCall.args[0],
-            'test');
-        assert.equal(element.restApiService.getPlugins.lastCall.args[1],
-            25);
-        assert.equal(element.restApiService.getPlugins.lastCall.args[2],
-            25);
-        done();
-      });
+      await element._paramsChanged(value);
+      assert.equal(getPluginsStub.lastCall.args[0], 'test');
+      assert.equal(getPluginsStub.lastCall.args[1], 25);
+      assert.equal(getPluginsStub.lastCall.args[2], 25);
     });
   });
 
@@ -168,7 +150,7 @@
   suite('404', () => {
     test('fires page-error', done => {
       const response = {status: 404};
-      sinon.stub(element.restApiService, 'getPlugins').callsFake(
+      stubRestApi('getPlugins').callsFake(
           (filter, pluginsPerPage, opt_offset, errFn) => {
             errFn(response);
           });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
index 2af4b6f1..1f73b4c 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-access/gr-repo-access_test.js
@@ -20,6 +20,7 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {toSortedPermissionsArray} from '../../../utils/access-util.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-access');
 
@@ -101,11 +102,8 @@
   };
   setup(() => {
     element = basicFixture.instantiate();
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve(null); },
-    });
-    repoStub = sinon.stub(element.restApiService, 'getRepo').returns(
-        Promise.resolve(repoRes));
+    stubRestApi('getAccount').returns(Promise.resolve(null));
+    repoStub = stubRestApi('getRepo').returns(Promise.resolve(repoRes));
     element._loading = false;
     element._ownerOf = [];
     element._canUpload = false;
@@ -118,14 +116,14 @@
   });
 
   test('_repoChanged', done => {
-    const accessStub = sinon.stub(element.restApiService,
+    const accessStub = stubRestApi(
         'getRepoAccessRights');
 
     accessStub.withArgs('New Repo').returns(
         Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
     accessStub.withArgs('Another New Repo')
         .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
-    const capabilitiesStub = sinon.stub(element.restApiService,
+    const capabilitiesStub = stubRestApi(
         'getCapabilities');
     capabilitiesStub.returns(Promise.resolve(capabilitiesRes));
 
@@ -160,9 +158,9 @@
         name: 'Access Database',
       },
     };
-    const accessStub = sinon.stub(element.restApiService, 'getRepoAccessRights')
+    const accessStub = stubRestApi('getRepoAccessRights')
         .returns(Promise.resolve(JSON.parse(JSON.stringify(accessRes2))));
-    const capabilitiesStub = sinon.stub(element.restApiService,
+    const capabilitiesStub = stubRestApi(
         'getCapabilities').returns(Promise.resolve(capabilitiesRes));
 
     element._repoChanged().then(() => {
@@ -240,11 +238,9 @@
   test('fires page-error', done => {
     const response = {status: 404};
 
-    sinon.stub(
-        element.restApiService, 'getRepoAccessRights')
-        .callsFake((repoName, errFn) => {
-          errFn(response);
-        });
+    stubRestApi('getRepoAccessRights').callsFake((repoName, errFn) => {
+      errFn(response);
+    });
 
     element.addEventListener('page-error', e => {
       assert.deepEqual(e.detail.response, response);
@@ -378,7 +374,7 @@
 
     test('_handleSaveForReview', () => {
       const saveStub =
-          sinon.stub(element.restApiService, 'setRepoAccessRightsForReview');
+          stubRestApi('setRepoAccessRightsForReview');
       sinon.stub(element, '_computeAddAndRemove').returns({
         add: {},
         remove: {},
@@ -1161,11 +1157,11 @@
           },
         },
       };
-      sinon.stub(element.restApiService, 'getRepoAccessRights').returns(
+      stubRestApi('getRepoAccessRights').returns(
           Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
       sinon.stub(GerritNav, 'navigateToChange');
       let resolver;
-      const saveStub = sinon.stub(element.restApiService,
+      const saveStub = stubRestApi(
           'setRepoAccessRights')
           .returns(new Promise(r => resolver = r));
 
@@ -1208,11 +1204,11 @@
           },
         },
       };
-      sinon.stub(element.restApiService, 'getRepoAccessRights').returns(
+      stubRestApi('getRepoAccessRights').returns(
           Promise.resolve(JSON.parse(JSON.stringify(accessRes))));
       sinon.stub(GerritNav, 'navigateToChange');
       let resolver;
-      const saveForReviewStub = sinon.stub(element.restApiService,
+      const saveForReviewStub = stubRestApi(
           'setRepoAccessRightsForReview')
           .returns(new Promise(r => resolver = r));
 
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
index 60d5f20..b01b654 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-commands/gr-repo-commands_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-repo-commands.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-commands');
 
@@ -31,8 +32,7 @@
     // Note that this probably does not achieve what it is supposed to, because
     // getProjectConfig() is called as soon as the element is attached, so
     // stubbing it here has not effect anymore.
-    repoStub = sinon.stub(element.restApiService, 'getProjectConfig')
-        .returns(Promise.resolve({}));
+    repoStub = stubRestApi('getProjectConfig').returns(Promise.resolve({}));
   });
 
   suite('create new change dialog', () => {
@@ -68,7 +68,7 @@
     let alertStub;
 
     setup(() => {
-      createChangeStub = sinon.stub(element.restApiService, 'createChange');
+      createChangeStub = stubRestApi('createChange');
       urlStub = sinon.stub(GerritNav, 'getEditUrlForDiff');
       sinon.stub(GerritNav, 'navigateToRelativeUrl');
       handleSpy = sinon.spy(element, '_handleEditRepoConfig');
@@ -118,11 +118,9 @@
       element.repo = 'test';
 
       const response = {status: 404};
-      sinon.stub(
-          element.restApiService, 'getProjectConfig')
-          .callsFake((repo, errFn) => {
-            errFn(response);
-          });
+      stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
+        errFn(response);
+      });
       element.addEventListener('page-error', e => {
         assert.deepEqual(e.detail.response, response);
         done();
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
index f5d4365..b84b6b4 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-dashboards/gr-repo-dashboards_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-repo-dashboards.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-dashboards');
 
@@ -30,7 +31,7 @@
 
   suite('dashboard table', () => {
     setup(() => {
-      sinon.stub(element.restApiService, 'getRepoDashboards').returns(
+      stubRestApi('getRepoDashboards').returns(
           Promise.resolve([
             {
               id: 'default:contributor',
@@ -123,11 +124,9 @@
   suite('404', () => {
     test('fires page-error', done => {
       const response = {status: 404};
-      sinon.stub(
-          element.restApiService, 'getRepoDashboards')
-          .callsFake((repo, errFn) => {
-            errFn(response);
-          });
+      stubRestApi('getRepoDashboards').callsFake((repo, errFn) => {
+        errFn(response);
+      });
 
       element.addEventListener('page-error', e => {
         assert.deepEqual(e.detail.response, response);
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
index f20fd1e..ed8b762 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-detail-list/gr-repo-detail-list_test.js
@@ -20,6 +20,7 @@
 import 'lodash/lodash.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-detail-list');
 
@@ -74,18 +75,12 @@
           ref: 'HEAD',
           revision: 'master',
         }].concat(_.times(25, branchGenerator));
-
-        stub('gr-rest-api-interface', {
-          getRepoBranches(num, project, offset) {
-            return Promise.resolve(branches);
-          },
-        });
+        stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
 
         const params = {
           repo: 'test',
           detail: 'branches',
         };
-
         element._paramsChanged(params).then(() => { flush(done); });
       });
 
@@ -118,7 +113,7 @@
 
       test('Edit HEAD button not admin', done => {
         sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-        sinon.stub(element.restApiService, 'getRepoAccess').returns(
+        stubRestApi('getRepoAccess').returns(
             Promise.resolve({
               test: {is_owner: false},
             }));
@@ -142,7 +137,7 @@
             .querySelector('.revisionWithEditing');
 
         sinon.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
-        sinon.stub(element.restApiService, 'getRepoAccess').returns(
+        stubRestApi('getRepoAccess').returns(
             Promise.resolve({
               test: {is_owner: true},
             }));
@@ -219,7 +214,7 @@
       test('_handleSaveRevision with invalid rev', done => {
         const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
-        sinon.stub(element.restApiService, 'setRepoHead').returns(
+        stubRestApi('setRepoHead').returns(
             Promise.resolve({
               status: 400,
             })
@@ -235,7 +230,7 @@
       test('_handleSaveRevision with valid rev', done => {
         const event = {model: {set: sinon.stub()}};
         element._isEditing = true;
-        sinon.stub(element.restApiService, 'setRepoHead').returns(
+        stubRestApi('setRepoHead').returns(
             Promise.resolve({
               status: 200,
             })
@@ -257,12 +252,7 @@
     suite('list with less then 25 branches', () => {
       setup(done => {
         branches = _.times(25, branchGenerator);
-
-        stub('gr-rest-api-interface', {
-          getRepoBranches(num, repo, offset) {
-            return Promise.resolve(branches);
-          },
-        });
+        stubRestApi('getRepoBranches').returns(Promise.resolve(branches));
 
         const params = {
           repo: 'test',
@@ -278,35 +268,27 @@
     });
 
     suite('filter', () => {
-      test('_paramsChanged', done => {
-        sinon.stub(
-            element.restApiService,
-            'getRepoBranches')
-            .callsFake(() => Promise.resolve(branches));
+      test('_paramsChanged', async () => {
+        const stub = stubRestApi('getRepoBranches').returns(
+            Promise.resolve(branches));
         const params = {
           detail: 'branches',
           repo: 'test',
           filter: 'test',
           offset: 25,
         };
-        element._paramsChanged(params).then(() => {
-          assert.equal(element.restApiService.getRepoBranches.lastCall.args[0],
-              'test');
-          assert.equal(element.restApiService.getRepoBranches.lastCall.args[1],
-              'test');
-          assert.equal(element.restApiService.getRepoBranches.lastCall.args[2],
-              25);
-          assert.equal(element.restApiService.getRepoBranches.lastCall.args[3],
-              25);
-          done();
-        });
+        await element._paramsChanged(params);
+        assert.equal(stub.lastCall.args[0], 'test');
+        assert.equal(stub.lastCall.args[1], 'test');
+        assert.equal(stub.lastCall.args[2], 25);
+        assert.equal(stub.lastCall.args[3], 25);
       });
     });
 
     suite('404', () => {
       test('fires page-error', done => {
         const response = {status: 404};
-        sinon.stub(element.restApiService, 'getRepoBranches').callsFake(
+        stubRestApi('getRepoBranches').callsFake(
             (filter, repo, reposBranchesPerPage, opt_offset, errFn) => {
               errFn(response);
             });
@@ -360,12 +342,7 @@
     suite('list of repo tags', () => {
       setup(done => {
         tags = _.times(26, tagGenerator);
-
-        stub('gr-rest-api-interface', {
-          getRepoTags(num, repo, offset) {
-            return Promise.resolve(tags);
-          },
-        });
+        stubRestApi('getRepoTags').returns(Promise.resolve(tags));
 
         const params = {
           repo: 'test',
@@ -435,12 +412,7 @@
     suite('list with less then 25 tags', () => {
       setup(done => {
         tags = _.times(25, tagGenerator);
-
-        stub('gr-rest-api-interface', {
-          getRepoTags(num, project, offset) {
-            return Promise.resolve(tags);
-          },
-        });
+        stubRestApi('getRepoTags').returns(Promise.resolve(tags));
 
         const params = {
           repo: 'test',
@@ -456,28 +428,19 @@
     });
 
     suite('filter', () => {
-      test('_paramsChanged', done => {
-        sinon.stub(
-            element.restApiService,
-            'getRepoTags')
-            .callsFake(() => Promise.resolve(tags));
+      test('_paramsChanged', async () => {
+        const stub = stubRestApi('getRepoTags').returns(Promise.resolve(tags));
         const params = {
           repo: 'test',
           detail: 'tags',
           filter: 'test',
           offset: 25,
         };
-        element._paramsChanged(params).then(() => {
-          assert.equal(element.restApiService.getRepoTags.lastCall.args[0],
-              'test');
-          assert.equal(element.restApiService.getRepoTags.lastCall.args[1],
-              'test');
-          assert.equal(element.restApiService.getRepoTags.lastCall.args[2],
-              25);
-          assert.equal(element.restApiService.getRepoTags.lastCall.args[3],
-              25);
-          done();
-        });
+        await element._paramsChanged(params);
+        assert.equal(stub.lastCall.args[0], 'test');
+        assert.equal(stub.lastCall.args[1], 'test');
+        assert.equal(stub.lastCall.args[2], 25);
+        assert.equal(stub.lastCall.args[3], 25);
       });
     });
 
@@ -520,7 +483,7 @@
     suite('404', () => {
       test('fires page-error', done => {
         const response = {status: 404};
-        sinon.stub(element.restApiService, 'getRepoTags').callsFake(
+        stubRestApi('getRepoTags').callsFake(
             (filter, repo, reposTagsPerPage, opt_offset, errFn) => {
               errFn(response);
             });
diff --git a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
index baa33a7..6bf73d1 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo-list/gr-repo-list_test.js
@@ -19,6 +19,7 @@
 import './gr-repo-list.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
 import 'lodash/lodash.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-list');
 
@@ -51,11 +52,7 @@
   suite('list with repos', () => {
     setup(done => {
       repos = _.times(26, repoGenerator);
-      stub('gr-rest-api-interface', {
-        getRepos(num, offset) {
-          return Promise.resolve(repos);
-        },
-      });
+      stubRestApi('getRepos').returns(Promise.resolve(repos));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -86,13 +83,7 @@
   suite('list with less then 25 repos', () => {
     setup(done => {
       repos = _.times(25, repoGenerator);
-
-      stub('gr-rest-api-interface', {
-        getRepos(num, offset) {
-          return Promise.resolve(repos);
-        },
-      });
-
+      stubRestApi('getRepos').returns(Promise.resolve(repos));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -108,22 +99,19 @@
       reposFiltered = _.times(1, repoGenerator);
     });
 
-    test('_paramsChanged', done => {
-      sinon.stub(element.restApiService, 'getRepos')
-          .callsFake( () => Promise.resolve(repos));
+    test('_paramsChanged', async () => {
+      const repoStub = stubRestApi('getRepos');
+      repoStub.returns(Promise.resolve(repos));
       const value = {
         filter: 'test',
         offset: 25,
       };
-      element._paramsChanged(value).then(() => {
-        assert.isTrue(element.restApiService.getRepos.lastCall
-            .calledWithExactly('test', 25, 25));
-        done();
-      });
+      await element._paramsChanged(value);
+      assert.isTrue(repoStub.lastCall.calledWithExactly('test', 25, 25));
     });
 
     test('latest repos requested are always set', done => {
-      const repoStub = sinon.stub(element.restApiService, 'getRepos');
+      const repoStub = stubRestApi('getRepos');
       repoStub.withArgs('test').returns(Promise.resolve(repos));
       repoStub.withArgs('filter').returns(Promise.resolve(reposFiltered));
       element._filter = 'test';
diff --git a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
index 29a83941..e2b26df 100644
--- a/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
+++ b/polygerrit-ui/app/elements/admin/gr-repo/gr-repo_test.js
@@ -18,12 +18,13 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-repo.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo');
 
 suite('gr-repo tests', () => {
   let element;
-
+  let loggedInStub;
   let repoStub;
   const repoConf = {
     description: 'Access inherited by all other projects.',
@@ -98,17 +99,11 @@
   }
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(false); },
-      getConfig() {
-        return Promise.resolve({download: {}});
-      },
-    });
+    loggedInStub = stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getConfig').returns(Promise.resolve({download: {}}));
+    repoStub =
+        stubRestApi('getProjectConfig').returns(Promise.resolve(repoConf));
     element = basicFixture.instantiate();
-    repoStub = sinon.stub(
-        element.restApiService,
-        'getProjectConfig')
-        .callsFake(() => Promise.resolve(repoConf));
   });
 
   test('_computePluginData', () => {
@@ -159,37 +154,28 @@
     assert.isTrue(element._readOnly);
   });
 
-  test('form defaults to read only when not logged in', done => {
+  test('form defaults to read only when not logged in', async () => {
     element.repo = REPO;
-    element._loadRepo().then(() => {
-      assert.isTrue(element._readOnly);
-      done();
-    });
+    await element._loadRepo();
+    assert.isTrue(element._readOnly);
   });
 
-  test('form defaults to read only when logged in and not admin', done => {
+  test('form defaults to read only when logged in and not admin', async () => {
     element.repo = REPO;
-    sinon.stub(element, '_getLoggedIn').callsFake(() => Promise.resolve(true));
-    sinon.stub(
-        element.restApiService,
-        'getRepoAccess')
+    stubRestApi('getRepoAccess')
         .callsFake(() => Promise.resolve({'test-repo': {}}));
-    element._loadRepo().then(() => {
-      assert.isTrue(element._readOnly);
-      done();
-    });
+    await element._loadRepo();
+    assert.isTrue(element._readOnly);
   });
 
-  test('all form elements are disabled when not admin', done => {
+  test('all form elements are disabled when not admin', async () => {
     element.repo = REPO;
-    element._loadRepo().then(() => {
-      flush();
-      const formFields = getFormFields();
-      for (const field of formFields) {
-        assert.isTrue(field.hasAttribute('disabled'));
-      }
-      done();
-    });
+    await element._loadRepo();
+    flush();
+    const formFields = getFormFields();
+    for (const field of formFields) {
+      assert.isTrue(field.hasAttribute('disabled'));
+    }
   });
 
   test('_formatBooleanSelect', () => {
@@ -246,8 +232,7 @@
     element.repo = 'test';
 
     const response = {status: 404};
-    sinon.stub(
-        element.restApiService, 'getProjectConfig').callsFake((repo, errFn) => {
+    stubRestApi('getProjectConfig').callsFake((repo, errFn) => {
       errFn(response);
     });
     element.addEventListener('page-error', e => {
@@ -261,48 +246,38 @@
   suite('admin', () => {
     setup(() => {
       element.repo = REPO;
-      sinon.stub(element, '_getLoggedIn')
-          .callsFake(() => Promise.resolve(true));
-      sinon.stub(
-          element.restApiService,
-          'getRepoAccess')
-          .callsFake(() => Promise.resolve({'test-repo': {is_owner: true}}));
+      loggedInStub.returns(Promise.resolve(true));
+      stubRestApi('getRepoAccess')
+          .returns(Promise.resolve({'test-repo': {is_owner: true}}));
     });
 
-    test('all form elements are enabled', done => {
-      element._loadRepo().then(() => {
-        flush();
-        const formFields = getFormFields();
-        for (const field of formFields) {
-          assert.isFalse(field.hasAttribute('disabled'));
-        }
-        assert.isFalse(element._loading);
-        done();
-      });
+    test('all form elements are enabled', async () => {
+      await element._loadRepo();
+      await flush();
+      const formFields = getFormFields();
+      for (const field of formFields) {
+        assert.isFalse(field.hasAttribute('disabled'));
+      }
+      assert.isFalse(element._loading);
     });
 
-    test('state gets set correctly', done => {
-      element._loadRepo().then(() => {
-        assert.equal(element._repoConfig.state, 'ACTIVE');
-        assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
-        done();
-      });
+    test('state gets set correctly', async () => {
+      await element._loadRepo();
+      assert.equal(element._repoConfig.state, 'ACTIVE');
+      assert.equal(element.$.stateSelect.bindValue, 'ACTIVE');
     });
 
-    test('inherited submit type value is calculated correctly', done => {
-      element
-          ._loadRepo().then(() => {
-            const sel = element.$.submitTypeSelect;
-            assert.equal(sel.bindValue, 'INHERIT');
-            assert.equal(
-                sel.nativeSelect.options[0].text,
-                'Inherit (Merge if necessary)'
-            );
-            done();
-          });
+    test('inherited submit type value is calculated correctly', async () => {
+      await element._loadRepo();
+      const sel = element.$.submitTypeSelect;
+      assert.equal(sel.bindValue, 'INHERIT');
+      assert.equal(
+          sel.nativeSelect.options[0].text,
+          'Inherit (Merge if necessary)'
+      );
     });
 
-    test('fields update and save correctly', () => {
+    test('fields update and save correctly', async () => {
       const configInputObj = {
         description: 'new description',
         use_contributor_agreements: 'TRUE',
@@ -322,59 +297,57 @@
         enable_reviewer_by_email: 'TRUE',
       };
 
-      const saveStub = sinon.stub(element.restApiService, 'saveRepoConfig')
+      const saveStub = stubRestApi('saveRepoConfig')
           .callsFake(() => Promise.resolve({}));
 
       const button = element.root.querySelector('gr-button');
 
-      return element._loadRepo().then(() => {
-        assert.isTrue(button.hasAttribute('disabled'));
-        assert.isFalse(element.$.Title.classList.contains('edited'));
-        element.$.descriptionInput.bindValue = configInputObj.description;
-        element.$.stateSelect.bindValue = configInputObj.state;
-        element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
-        element.$.contentMergeSelect.bindValue =
-            configInputObj.use_content_merge;
-        element.$.newChangeSelect.bindValue =
-            configInputObj.create_new_change_for_all_not_in_target;
-        element.$.requireChangeIdSelect.bindValue =
-            configInputObj.require_change_id;
-        element.$.enableSignedPush.bindValue =
-            configInputObj.enable_signed_push;
-        element.$.requireSignedPush.bindValue =
-            configInputObj.require_signed_push;
-        element.$.rejectImplicitMergesSelect.bindValue =
-            configInputObj.reject_implicit_merges;
-        element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
-            configInputObj.private_by_default;
-        element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
-            configInputObj.match_author_to_committer_date;
-        const inputElement = PolymerElement ?
-          element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
-        inputElement.bindValue = configInputObj.max_object_size_limit;
-        element.$.contributorAgreementSelect.bindValue =
-            configInputObj.use_contributor_agreements;
-        element.$.useSignedOffBySelect.bindValue =
-            configInputObj.use_signed_off_by;
-        element.$.rejectEmptyCommitSelect.bindValue =
-            configInputObj.reject_empty_commit;
-        element.$.unRegisteredCcSelect.bindValue =
-            configInputObj.enable_reviewer_by_email;
+      await element._loadRepo();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.isFalse(element.$.Title.classList.contains('edited'));
+      element.$.descriptionInput.bindValue = configInputObj.description;
+      element.$.stateSelect.bindValue = configInputObj.state;
+      element.$.submitTypeSelect.bindValue = configInputObj.submit_type;
+      element.$.contentMergeSelect.bindValue =
+          configInputObj.use_content_merge;
+      element.$.newChangeSelect.bindValue =
+          configInputObj.create_new_change_for_all_not_in_target;
+      element.$.requireChangeIdSelect.bindValue =
+          configInputObj.require_change_id;
+      element.$.enableSignedPush.bindValue =
+          configInputObj.enable_signed_push;
+      element.$.requireSignedPush.bindValue =
+          configInputObj.require_signed_push;
+      element.$.rejectImplicitMergesSelect.bindValue =
+          configInputObj.reject_implicit_merges;
+      element.$.setAllnewChangesPrivateByDefaultSelect.bindValue =
+          configInputObj.private_by_default;
+      element.$.matchAuthoredDateWithCommitterDateSelect.bindValue =
+          configInputObj.match_author_to_committer_date;
+      const inputElement = PolymerElement ?
+        element.$.maxGitObjSizeIronInput : element.$.maxGitObjSizeInput;
+      inputElement.bindValue = configInputObj.max_object_size_limit;
+      element.$.contributorAgreementSelect.bindValue =
+          configInputObj.use_contributor_agreements;
+      element.$.useSignedOffBySelect.bindValue =
+          configInputObj.use_signed_off_by;
+      element.$.rejectEmptyCommitSelect.bindValue =
+          configInputObj.reject_empty_commit;
+      element.$.unRegisteredCcSelect.bindValue =
+          configInputObj.enable_reviewer_by_email;
 
-        assert.isFalse(button.hasAttribute('disabled'));
-        assert.isTrue(element.$.configurations.classList.contains('edited'));
+      assert.isFalse(button.hasAttribute('disabled'));
+      assert.isTrue(element.$.configurations.classList.contains('edited'));
 
-        const formattedObj =
-            element._formatRepoConfigForSave(element._repoConfig);
-        assert.deepEqual(formattedObj, configInputObj);
+      const formattedObj =
+          element._formatRepoConfigForSave(element._repoConfig);
+      assert.deepEqual(formattedObj, configInputObj);
 
-        return element._handleSaveRepoConfig().then(() => {
-          assert.isTrue(button.hasAttribute('disabled'));
-          assert.isFalse(element.$.Title.classList.contains('edited'));
-          assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
-              configInputObj));
-        });
-      });
+      await element._handleSaveRepoConfig();
+      assert.isTrue(button.hasAttribute('disabled'));
+      assert.isFalse(element.$.Title.classList.contains('edited'));
+      assert.isTrue(saveStub.lastCall.calledWithExactly(REPO,
+          configInputObj));
     });
   });
 });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
index 38cc772..acf71c3 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-item/gr-change-list-item_test.js
@@ -19,6 +19,7 @@
 import './gr-change-list-item.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {LabelCategory} from './gr-change-list-item.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-list-item');
 
@@ -26,10 +27,8 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getLoggedIn() { return Promise.resolve(false); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
index af3acd8..445254a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view_test.js
@@ -20,6 +20,7 @@
 import {page} from '../../../utils/page-wrapper-utils.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import 'lodash/lodash.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-list-view');
 
@@ -30,14 +31,10 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(false); },
-      getChanges(num, query) {
-        return Promise.resolve([]);
-      },
-      getAccountDetails() { return Promise.resolve({}); },
-      getAccountStatus() { return Promise.resolve({}); },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getChanges').returns(Promise.resolve([]));
+    stubRestApi('getAccountDetails').returns(Promise.resolve({}));
+    stubRestApi('getAccountStatus').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
index 1f5d519..35d69f6 100644
--- a/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-dashboard-view/gr-dashboard-view_test.js
@@ -23,6 +23,7 @@
 import {changeIsOpen} from '../../../utils/change-util.js';
 import {ChangeStatus} from '../../../constants/constants.js';
 import {createAccountWithId} from '../../../test/test-data-generators.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-dashboard-view');
 
@@ -33,16 +34,14 @@
   let getChangesStub;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(false); },
-      getAccountDetails() { return Promise.resolve({}); },
-      getAccountStatus() { return Promise.resolve(false); },
-    });
-    element = basicFixture.instantiate();
-
-    getChangesStub = sinon.stub(element.restApiService, 'getChanges').callsFake(
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getAccountDetails').returns(Promise.resolve({}));
+    stubRestApi('getAccountStatus').returns(Promise.resolve(false));
+    getChangesStub= stubRestApi('getChanges').callsFake(
         (_, qs) => Promise.resolve(qs.map(() => [])));
 
+    element = basicFixture.instantiate();
+
     let resolver;
     paramsChangedPromise = new Promise(resolve => {
       resolver = resolve;
@@ -125,15 +124,14 @@
       const deleteDraftCommentsPromise = new Promise(resolve => {
         deleteDraftCommentsPromiseResolver = resolve;
       });
-      sinon.stub(element.restApiService, 'deleteDraftComments')
+      const deleteStub = stubRestApi('deleteDraftComments')
           .returns(deleteDraftCommentsPromise);
 
       // Open confirmation dialog and tap confirm button.
       await element.$.confirmDeleteOverlay.open();
       MockInteractions.tap(element.$.confirmDeleteDialog.$.confirm);
       flush();
-      assert.isTrue(element.restApiService.deleteDraftComments
-          .calledWithExactly('-is:open'));
+      assert.isTrue(deleteStub.calledWithExactly('-is:open'));
       assert.isTrue(element.$.confirmDeleteDialog.disabled);
       assert.equal(element._reload.callCount, 0);
 
@@ -255,7 +253,7 @@
 
   suite('_getProjectDashboard', () => {
     test('dashboard with foreach', () => {
-      sinon.stub(element.restApiService, 'getDashboard')
+      stubRestApi('getDashboard')
           .callsFake( () => Promise.resolve({
             title: 'title',
             foreach: 'foreach for ${project}',
@@ -281,7 +279,7 @@
     });
 
     test('dashboard without foreach', () => {
-      sinon.stub(element.restApiService, 'getDashboard').callsFake(
+      stubRestApi('getDashboard').callsFake(
           () => Promise.resolve({
             title: 'title',
             sections: [
@@ -309,7 +307,7 @@
       {name: 'test2', query: 'test2', hideIfEmpty: true},
     ];
     getChangesStub.restore();
-    sinon.stub(element.restApiService, 'getChanges')
+    stubRestApi('getChanges')
         .returns(Promise.resolve([[], ['nonempty']]));
 
     return element._fetchDashboardChanges({sections}, false).then(() => {
@@ -324,7 +322,7 @@
       {name: 'test2', query: 'test2'},
     ];
     getChangesStub.restore();
-    sinon.stub(element.restApiService, 'getChanges')
+    stubRestApi('getChanges')
         .returns(Promise.resolve([[], []]));
 
     return element._fetchDashboardChanges({sections}, false).then(() => {
@@ -375,7 +373,7 @@
 
   test('404 page', done => {
     const response = {status: 404};
-    sinon.stub(element.restApiService, 'getDashboard').callsFake(
+    stubRestApi('getDashboard').callsFake(
         async (project, dashboard, errFn) => {
           errFn(response);
         });
@@ -390,16 +388,19 @@
     };
   });
 
-  test('params change triggers dashboardDisplayed()', () => {
+  test('params change triggers dashboardDisplayed()', async () => {
+    stubRestApi('getDashboard').returns(Promise.resolve({
+      title: 'title',
+      sections: [],
+    }));
     sinon.stub(element.reporting, 'dashboardDisplayed');
     element.params = {
       view: GerritNav.View.DASHBOARD,
       project: 'project',
       dashboard: 'dashboard',
     };
-    return paramsChangedPromise.then(() => {
-      assert.isTrue(element.reporting.dashboardDisplayed.calledOnce);
-    });
+    await paramsChangedPromise;
+    assert.isTrue(element.reporting.dashboardDisplayed.calledOnce);
   });
 });
 
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
index 78560907..4ec799f 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-user-header.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-user-header');
 
@@ -28,7 +29,7 @@
   });
 
   test('loads and clears account info', done => {
-    sinon.stub(element.restApiService, 'getAccountDetails')
+    stubRestApi('getAccountDetails')
         .returns(Promise.resolve({
           name: 'foo',
           email: 'bar',
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
index f7bca82..0641182 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.js
@@ -27,8 +27,8 @@
   createChangeMessages,
   createRevisions,
 } from '../../../test/test-data-generators.js';
-import {appContext} from '../../../services/app-context.js';
 import {ChangeStatus} from '../../../constants/constants.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-actions');
 
@@ -42,56 +42,50 @@
 
   suite('basic tests', () => {
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getChangeRevisionActions() {
+      stubRestApi('getChangeRevisionActions').returns(Promise.resolve({
+        cherrypick: {
+          method: 'POST',
+          label: 'Cherry Pick',
+          title: 'Cherry pick change to a different branch',
+          enabled: true,
+        },
+        rebase: {
+          method: 'POST',
+          label: 'Rebase',
+          title: 'Rebase onto tip of branch or parent change',
+          enabled: true,
+        },
+        submit: {
+          method: 'POST',
+          label: 'Submit',
+          title: 'Submit patch set 2 into master',
+          enabled: true,
+        },
+        revert_submission: {
+          method: 'POST',
+          label: 'Revert submission',
+          title: 'Revert this submission',
+          enabled: true,
+        },
+      }));
+      stubRestApi('send').callsFake((method, url, payload) => {
+        if (method !== 'POST') {
+          return Promise.reject(new Error('bad method'));
+        }
+        if (url === '/changes/test~42/revisions/2/submit') {
           return Promise.resolve({
-            cherrypick: {
-              method: 'POST',
-              label: 'Cherry Pick',
-              title: 'Cherry pick change to a different branch',
-              enabled: true,
-            },
-            rebase: {
-              method: 'POST',
-              label: 'Rebase',
-              title: 'Rebase onto tip of branch or parent change',
-              enabled: true,
-            },
-            submit: {
-              method: 'POST',
-              label: 'Submit',
-              title: 'Submit patch set 2 into master',
-              enabled: true,
-            },
-            revert_submission: {
-              method: 'POST',
-              label: 'Revert submission',
-              title: 'Revert this submission',
-              enabled: true,
-            },
+            ok: true,
+            text() { return Promise.resolve(')]}\'\n{}'); },
           });
-        },
-        send(method, url, payload) {
-          if (method !== 'POST') {
-            return Promise.reject(new Error('bad method'));
-          }
-
-          if (url === '/changes/test~42/revisions/2/submit') {
-            return Promise.resolve({
-              ok: true,
-              text() { return Promise.resolve(')]}\'\n{}'); },
-            });
-          } else if (url === '/changes/test~42/revisions/2/rebase') {
-            return Promise.resolve({
-              ok: true,
-              text() { return Promise.resolve(')]}\'\n{}'); },
-            });
-          }
-
-          return Promise.reject(new Error('bad url'));
-        },
-        getProjectConfig() { return Promise.resolve({}); },
+        } else if (url === '/changes/test~42/revisions/2/rebase') {
+          return Promise.resolve({
+            ok: true,
+            text() { return Promise.resolve(')]}\'\n{}'); },
+          });
+        }
+        return Promise.reject(new Error('bad url'));
       });
+      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
 
       sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
           .returns(Promise.resolve());
@@ -111,8 +105,7 @@
       element.account = {
         _account_id: 123,
       };
-      sinon.stub(appContext.restApiService, 'getRepoBranches').returns(
-          Promise.resolve([]));
+      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
 
       return element.reload();
     });
@@ -148,14 +141,14 @@
     });
 
     test('plugin revision actions', done => {
-      sinon.stub(element.restApiService, 'getChangeActionURL').returns(
+      const stub = stubRestApi('getChangeActionURL').returns(
           Promise.resolve('the-url'));
       element.revisionActions = {
         'plugin~action': {},
       };
       assert.isOk(element.revisionActions['plugin~action']);
       flush(() => {
-        assert.isTrue(element.restApiService.getChangeActionURL.calledWith(
+        assert.isTrue(stub.calledWith(
             element.changeNum, element.latestPatchNum, '/plugin~action'));
         assert.equal(element.revisionActions['plugin~action'].__url, 'the-url');
         done();
@@ -163,14 +156,14 @@
     });
 
     test('plugin change actions', async () => {
-      sinon.stub(element.restApiService, 'getChangeActionURL').returns(
+      const stub = stubRestApi('getChangeActionURL').returns(
           Promise.resolve('the-url'));
       element.actions = {
         'plugin~action': {},
       };
       assert.isOk(element.actions['plugin~action']);
       await flush();
-      assert.isTrue(element.restApiService.getChangeActionURL.calledWith(
+      assert.isTrue(stub.calledWith(
           element.changeNum, undefined, '/plugin~action'));
       assert.equal(element.actions['plugin~action'].__url, 'the-url');
     });
@@ -278,7 +271,7 @@
 
     test('submit change', () => {
       const showSpy = sinon.spy(element, '_showActionDialog');
-      sinon.stub(element.restApiService, 'getFromProjectLookup')
+      stubRestApi('getFromProjectLookup')
           .returns(Promise.resolve('test'));
       sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
@@ -300,7 +293,7 @@
 
     test('submit change, tap on icon', done => {
       sinon.stub(element.$.confirmSubmitDialog, 'resetFocus').callsFake( done);
-      sinon.stub(element.restApiService, 'getFromProjectLookup')
+      stubRestApi('getFromProjectLookup')
           .returns(Promise.resolve('test'));
       sinon.stub(element.$.overlay, 'open').returns(Promise.resolve());
       element.change = {
@@ -404,7 +397,7 @@
 
     test('rebase change fires reload event', done => {
       const eventStub = sinon.stub(element, 'dispatchEvent');
-      sinon.stub(element.restApiService, 'getResponseObject').returns(
+      stubRestApi('getResponseObject').returns(
           Promise.resolve({}));
       element._handleResponse({__key: 'rebase'}, {});
       flush(() => {
@@ -443,7 +436,7 @@
       MockInteractions.tap(rebaseButton);
       await flush();
       assert.isFalse(element.$.confirmRebase.hidden);
-      sinon.stub(element.restApiService, 'getChanges')
+      stubRestApi('getChanges')
           .returns(Promise.resolve([]));
       element._handleCherrypickTap();
       await flush();
@@ -475,7 +468,7 @@
       const labels = {'Foo': 1, 'Bar-Baz': -2};
       const changeId = 1234;
       sinon.stub(element.$.jsAPI, 'getLabelValuesPostRevert').returns(labels);
-      const saveStub = sinon.stub(element.restApiService, 'saveChangeReview')
+      const saveStub = stubRestApi('saveChangeReview')
           .returns(Promise.resolve());
       return element._setLabelValuesOnRevert(changeId).then(() => {
         assert.isTrue(saveStub.calledOnce);
@@ -750,7 +743,7 @@
           },
         ];
         setup(done => {
-          sinon.stub(element.restApiService, 'getChanges')
+          stubRestApi('getChanges')
               .returns(Promise.resolve(changes));
           element._handleCherrypickTap();
           flush(() => {
@@ -996,7 +989,7 @@
         element.change = {
           current_revision: 'abc1234',
         };
-        sinon.stub(element.restApiService, 'getChanges')
+        stubRestApi('getChanges')
             .returns(Promise.resolve([
               {change_id: '12345678901234', topic: 'T', subject: 'random'},
               {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -1021,7 +1014,7 @@
             submission_id: '199 0',
             current_revision: '2000',
           };
-          getChangesStub = sinon.stub(element.restApiService, 'getChanges')
+          getChangesStub = stubRestApi('getChanges')
               .returns(Promise.resolve([
                 {change_id: '12345678901234', topic: 'T', subject: 'random'},
                 {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -1131,7 +1124,7 @@
             submission_id: '199',
             current_revision: '2000',
           };
-          sinon.stub(element.restApiService, 'getChanges')
+          stubRestApi('getChanges')
               .returns(Promise.resolve([
                 {change_id: '12345678901234', topic: 'T', subject: 'random'},
               ]));
@@ -1875,7 +1868,7 @@
         };
 
         test('succeed', () => {
-          sinon.stub(element.restApiService, 'getChange')
+          stubRestApi('getChange')
               .callsFake( makeGetChange(5));
           return element._waitForChangeReachable(123).then(success => {
             assert.isTrue(success);
@@ -1883,7 +1876,7 @@
         });
 
         test('fail', () => {
-          sinon.stub(element.restApiService, 'getChange')
+          stubRestApi('getChange')
               .callsFake( makeGetChange(6));
           return element._waitForChangeReachable(123).then(success => {
             assert.isFalse(success);
@@ -1920,16 +1913,16 @@
       suite('happy path', () => {
         let sendStub;
         setup(() => {
-          sinon.stub(element.restApiService, 'getChangeDetail')
+          stubRestApi('getChangeDetail')
               .returns(Promise.resolve({
                 ...createChange(),
                 // element has latest info
                 revisions: createRevisions(element.latestPatchNum),
                 messages: createChangeMessages(1),
               }));
-          sendStub = sinon.stub(element.restApiService, 'executeChangeAction')
+          sendStub = stubRestApi('executeChangeAction')
               .returns(Promise.resolve({}));
-          getResponseObjectStub = sinon.stub(element.restApiService,
+          getResponseObjectStub = stubRestApi(
               'getResponseObject');
           sinon.stub(GerritNav,
               'navigateToChange').returns(Promise.resolve(true));
@@ -1947,7 +1940,7 @@
           setup(() => {
             element.change.submission_id = '199';
             element.change.current_revision = '2000';
-            sinon.stub(element.restApiService, 'getChanges')
+            stubRestApi('getChanges')
                 .returns(Promise.resolve([
                   {change_id: '12345678901234', topic: 'T', subject: 'random'},
                   {change_id: '23456', topic: 'T', subject: 'a'.repeat(100)},
@@ -2039,14 +2032,14 @@
 
       suite('failure modes', () => {
         test('non-latest', () => {
-          sinon.stub(element.restApiService, 'getChangeDetail')
+          stubRestApi('getChangeDetail')
               .returns(Promise.resolve({
                 ...createChange(),
                 // new patchset was uploaded
                 revisions: createRevisions(element.latestPatchNum + 1),
                 messages: createChangeMessages(1),
               }));
-          const sendStub = sinon.stub(element.restApiService,
+          const sendStub = stubRestApi(
               'executeChangeAction');
 
           return element._send('DELETE', payload, '/endpoint', true, cleanup)
@@ -2059,14 +2052,14 @@
         });
 
         test('send fails', () => {
-          sinon.stub(element.restApiService, 'getChangeDetail')
+          stubRestApi('getChangeDetail')
               .returns(Promise.resolve({
                 ...createChange(),
                 // element has latest info
                 revisions: createRevisions(element.latestPatchNum),
                 messages: createChangeMessages(1),
               }));
-          const sendStub = sinon.stub(element.restApiService,
+          const sendStub = stubRestApi(
               'executeChangeAction').callsFake(
               (num, method, patchNum, endpoint, payload, onErr) => {
                 onErr();
@@ -2107,15 +2100,10 @@
     let changeRevisionActions;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getChangeRevisionActions() {
-          return Promise.resolve(changeRevisionActions);
-        },
-        send(method, url, payload) {
-          return Promise.reject(new Error('error'));
-        },
-        getProjectConfig() { return Promise.resolve({}); },
-      });
+      stubRestApi('getChangeRevisionActions').returns(
+          Promise.resolve(changeRevisionActions));
+      stubRestApi('send').returns(Promise.reject(new Error('error')));
+      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
 
       sinon.stub(getPluginLoader(), 'awaitPluginsLoaded')
           .returns(Promise.resolve());
@@ -2127,8 +2115,7 @@
       element.changeNum = '42';
       element.latestPatchNum = '2';
 
-      sinon.stub(appContext.restApiService, 'getRepoBranches').returns(
-          Promise.resolve([]));
+      stubRestApi('getRepoBranches').returns(Promise.resolve([]));
       return element.reload();
     });
 
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
index e812657..e71c086 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata-it_test.js
@@ -21,6 +21,7 @@
 import {resetPlugins} from '../../../test/test-utils.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const testHtmlPlugin = document.createElement('dom-module');
 testHtmlPlugin.innerHTML = `
@@ -86,11 +87,9 @@
   }
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getLoggedIn() { return Promise.resolve(false); },
-      deleteVote() { return Promise.resolve({ok: true}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('deleteVote').returns(Promise.resolve({ok: true}));
   });
 
   teardown(() => {
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
index ab7d09b..6a16040 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.ts
@@ -64,6 +64,7 @@
 import {GrEditableLabel} from '../../shared/gr-editable-label/gr-editable-label';
 import {PluginApi} from '../../plugins/gr-plugin-types';
 import {GrEndpointDecorator} from '../../plugins/gr-endpoint-decorator/gr-endpoint-decorator';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-change-metadata');
 
@@ -73,21 +74,16 @@
   let element: GrChangeMetadata;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() {
-        return Promise.resolve({
-          ...createServerInfo(),
-          user: {
-            ...createUserConfig(),
-            anonymous_coward_name: 'test coward name',
-          },
-        });
-      },
-      getLoggedIn() {
-        return Promise.resolve(false);
-      },
-    });
-
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getConfig').returns(
+      Promise.resolve({
+        ...createServerInfo(),
+        user: {
+          ...createUserConfig(),
+          anonymous_coward_name: 'test coward name',
+        },
+      })
+    );
     element = basicFixture.instantiate();
   });
 
@@ -803,8 +799,8 @@
       let setStub: SinonStubbedMember<RestApiService['setAssignee']>;
 
       setup(() => {
-        deleteStub = sinon.stub(element.restApiService, 'deleteAssignee');
-        setStub = sinon.stub(element.restApiService, 'setAssignee');
+        deleteStub = stubRestApi('deleteAssignee');
+        setStub = stubRestApi('setAssignee');
         element.serverConfig = {
           ...createServerInfo(),
           change: {
@@ -850,9 +846,9 @@
 
     test('changing topic', () => {
       const newTopic = 'the new topic' as TopicName;
-      const setChangeTopicStub = sinon
-        .stub(element.restApiService, 'setChangeTopic')
-        .returns(Promise.resolve(newTopic));
+      const setChangeTopicStub = stubRestApi('setChangeTopic').returns(
+        Promise.resolve(newTopic)
+      );
       element._handleTopicChanged(new CustomEvent('test', {detail: newTopic}));
       const topicChangedSpy = sinon.spy();
       element.addEventListener('topic-changed', topicChangedSpy);
@@ -867,9 +863,9 @@
 
     test('topic removal', () => {
       const newTopic = 'the new topic' as TopicName;
-      const setChangeTopicStub = sinon
-        .stub(element.restApiService, 'setChangeTopic')
-        .returns(Promise.resolve(newTopic));
+      const setChangeTopicStub = stubRestApi('setChangeTopic').returns(
+        Promise.resolve(newTopic)
+      );
       const chip = element.shadowRoot!.querySelector('gr-linked-chip');
       const remove = chip!.$.remove;
       const topicChangedSpy = sinon.spy();
@@ -888,9 +884,9 @@
       flush();
       element._newHashtag = 'new hashtag' as Hashtag;
       const newHashtag: Hashtag[] = ['new hashtag' as Hashtag];
-      const setChangeHashtagStub = sinon
-        .stub(element.restApiService, 'setChangeHashtag')
-        .returns(Promise.resolve(newHashtag));
+      const setChangeHashtagStub = stubRestApi('setChangeHashtag').returns(
+        Promise.resolve(newHashtag)
+      );
       element._handleHashtagChanged();
       assert.isTrue(
         setChangeHashtagStub.calledWith(42 as NumericChangeId, {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 5cb84b4..0fc470f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -36,7 +36,10 @@
 import {EventType, PluginApi} from '../../plugins/gr-plugin-types';
 
 import 'lodash/lodash';
-import {TestKeyboardShortcutBinder} from '../../../test/test-utils';
+import {
+  stubRestApi,
+  TestKeyboardShortcutBinder,
+} from '../../../test/test-utils';
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
 import {
@@ -72,7 +75,6 @@
   GitRef,
   NumericChangeId,
   ParentPatchSetNum,
-  ParsedJSON,
   PatchRange,
   PatchSetNum,
   RevisionInfo,
@@ -366,29 +368,19 @@
     _testOnly_resetEndpoints();
     navigateToChangeStub = sinon.stub(GerritNav, 'navigateToChange');
 
-    function getCommentsStub() {
-      return Promise.resolve({});
-    }
-    stub('gr-rest-api-interface', {
-      getConfig() {
-        return Promise.resolve({
-          ...createServerInfo(),
-          user: {
-            ...createUserConfig(),
-            anonymous_coward_name: 'test coward name',
-          },
-        });
-      },
-      getAccount() {
-        return Promise.resolve(undefined);
-      },
-      getDiffComments: (getCommentsStub as unknown) as RestApiService['getDiffComments'],
-      getDiffRobotComments: (getCommentsStub as unknown) as RestApiService['getDiffRobotComments'],
-      getDiffDrafts: (getCommentsStub as unknown) as RestApiService['getDiffDrafts'],
-      _fetchSharedCacheURL() {
-        return Promise.resolve({} as ParsedJSON);
-      },
-    });
+    stubRestApi('getConfig').returns(
+      Promise.resolve({
+        ...createServerInfo(),
+        user: {
+          ...createUserConfig(),
+          anonymous_coward_name: 'test coward name',
+        },
+      })
+    );
+    stubRestApi('getAccount').returns(Promise.resolve(undefined));
+    stubRestApi('getDiffComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
     element = fixture.instantiate();
     element._changeNum = 1 as NumericChangeId;
     sinon.stub(element.$.actions, 'reload').returns(Promise.resolve());
@@ -705,7 +697,7 @@
         messages: createChangeMessages(1),
       };
       element._change.labels = {};
-      sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+      stubRestApi('getChangeDetail').callsFake(() =>
         Promise.resolve({
           ...createChange(),
           // element has latest info
@@ -1420,15 +1412,17 @@
   });
 
   test('change num change', () => {
+    const change = {
+      ...createChange(),
+      labels: {},
+    } as ParsedChangeInfo;
+    stubRestApi('getChangeDetail').returns(Promise.resolve(change));
     element._changeNum = undefined;
     element._patchRange = {
       basePatchNum: ParentPatchSetNum,
       patchNum: 2 as PatchSetNum,
     };
-    element._change = {
-      ...createChange(),
-      labels: {},
-    };
+    element._change = change;
     element.viewState.changeNum = null;
     element.viewState.diffMode = DiffViewMode.UNIFIED;
     assert.equal(element.viewState.numFilesShown, 200);
@@ -1474,9 +1468,7 @@
   });
 
   test('diffMode defaults to side by side without preferences', done => {
-    sinon
-      .stub(element.restApiService, 'getPreferences')
-      .returns(Promise.resolve(createPreferences()));
+    stubRestApi('getPreferences').returns(Promise.resolve(createPreferences()));
     // No user prefs or diff view mode set.
 
     element._setDiffViewMode()!.then(() => {
@@ -1486,7 +1478,7 @@
   });
 
   test('diffMode defaults to preference when not already set', done => {
-    sinon.stub(element.restApiService, 'getPreferences').returns(
+    stubRestApi('getPreferences').returns(
       Promise.resolve({
         ...createPreferences(),
         default_diff_view: DiffViewMode.UNIFIED,
@@ -1501,7 +1493,7 @@
 
   test('existing diffMode overrides preference', done => {
     element.viewState.diffMode = DiffViewMode.SIDE_BY_SIDE;
-    sinon.stub(element.restApiService, 'getPreferences').returns(
+    stubRestApi('getPreferences').returns(
       Promise.resolve({
         ...createPreferences(),
         default_diff_view: DiffViewMode.UNIFIED,
@@ -1647,9 +1639,9 @@
   test('_handleCommitMessageSave trims trailing whitespace', () => {
     element._change = createChange();
     // Response code is 500, because we want to avoid window reloading
-    const putStub = sinon
-      .stub(element.restApiService, 'putChangeCommitMessage')
-      .returns(Promise.resolve(new Response(null, {status: 500})));
+    const putStub = stubRestApi('putChangeCommitMessage').returns(
+      Promise.resolve(new Response(null, {status: 500}))
+    );
 
     const mockEvent = (content: string) => {
       return new CustomEvent('', {detail: {content}});
@@ -1765,7 +1757,7 @@
 
   test('topic is coalesced to null', done => {
     sinon.stub(element, '_changeChanged');
-    sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+    stubRestApi('getChangeDetail').returns(
       Promise.resolve({
         ...createChange(),
         labels: {},
@@ -1782,7 +1774,7 @@
 
   test('commit sha is populated from getChangeDetail', done => {
     sinon.stub(element, '_changeChanged');
-    sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+    stubRestApi('getChangeDetail').callsFake(() =>
       Promise.resolve({
         ...createChange(),
         labels: {},
@@ -1800,7 +1792,7 @@
   test('edit is added to change', () => {
     sinon.stub(element, '_changeChanged');
     const changeRevision = createRevision();
-    sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+    stubRestApi('getChangeDetail').callsFake(() =>
       Promise.resolve({
         ...createChange(),
         labels: {},
@@ -1957,9 +1949,7 @@
   });
 
   test('revert dialog opened with revert param', done => {
-    sinon
-      .stub(element.restApiService, 'getLoggedIn')
-      .callsFake(() => Promise.resolve(true));
+    stubRestApi('getLoggedIn').returns(Promise.resolve(true));
     const awaitPluginsLoadedStub = sinon
       .stub(getPluginLoader(), 'awaitPluginsLoaded')
       .callsFake(() => Promise.resolve());
@@ -2035,7 +2025,7 @@
         messages: createChangeMessages(1),
       };
       element._change.labels = {};
-      sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+      stubRestApi('getChangeDetail').callsFake(() =>
         Promise.resolve({
           ...createChange(),
           // element has latest info
@@ -2127,7 +2117,7 @@
         messages: createChangeMessages(1),
       };
       element._change.labels = {};
-      sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+      stubRestApi('getChangeDetail').callsFake(() =>
         Promise.resolve({
           ...createChange(),
           // new patchset was uploaded
@@ -2306,17 +2296,15 @@
       });
 
       test('_startUpdateCheckTimer negative delay', () => {
-        const getChangeDetailStub = sinon
-          .stub(element.restApiService, 'getChangeDetail')
-          .callsFake(() =>
-            Promise.resolve({
-              ...createChange(),
-              // element has latest info
-              revisions: {rev1: createRevision()},
-              messages: createChangeMessages(1),
-              current_revision: 'rev1' as CommitId,
-            })
-          );
+        const getChangeDetailStub = stubRestApi('getChangeDetail').returns(
+          Promise.resolve({
+            ...createChange(),
+            // element has latest info
+            revisions: {rev1: createRevision()},
+            messages: createChangeMessages(1),
+            current_revision: 'rev1' as CommitId,
+          })
+        );
 
         element._serverConfig = {
           ...createServerInfo(),
@@ -2328,9 +2316,8 @@
       });
 
       test('_startUpdateCheckTimer up-to-date', async () => {
-        const getChangeDetailStub = sinon
-          .stub(element.restApiService, 'getChangeDetail')
-          .callsFake(() =>
+        const getChangeDetailStub = stubRestApi('getChangeDetail').callsFake(
+          () =>
             Promise.resolve({
               ...createChange(),
               // element has latest info
@@ -2338,7 +2325,7 @@
               messages: createChangeMessages(1),
               current_revision: 'rev1' as CommitId,
             })
-          );
+        );
 
         element._serverConfig = {
           ...createServerInfo(),
@@ -2352,7 +2339,7 @@
       });
 
       test('_startUpdateCheckTimer out-of-date shows an alert', done => {
-        sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+        stubRestApi('getChangeDetail').callsFake(() =>
           Promise.resolve({
             ...createChange(),
             // new patchset was uploaded
@@ -2375,7 +2362,7 @@
       });
 
       test('_startUpdateCheckTimer respects _loading', async () => {
-        sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+        stubRestApi('getChangeDetail').callsFake(() =>
           Promise.resolve({
             ...createChange(),
             // new patchset was uploaded
@@ -2397,7 +2384,7 @@
       });
 
       test('_startUpdateCheckTimer new status shows an alert', done => {
-        sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+        stubRestApi('getChangeDetail').callsFake(() =>
           Promise.resolve({
             ...createChange(),
             // element has latest info
@@ -2419,7 +2406,7 @@
       });
 
       test('_startUpdateCheckTimer new messages shows an alert', done => {
-        sinon.stub(element.restApiService, 'getChangeDetail').callsFake(() =>
+        stubRestApi('getChangeDetail').callsFake(() =>
           Promise.resolve({
             ...createChange(),
             revisions: {rev1: createRevision()},
@@ -2661,7 +2648,7 @@
   test('_selectedRevision updates when patchNum is changed', () => {
     const revision1: RevisionInfo = createRevision(1);
     const revision2: RevisionInfo = createRevision(2);
-    sinon.stub(element.restApiService, 'getChangeDetail').returns(
+    stubRestApi('getChangeDetail').returns(
       Promise.resolve({
         ...createChange(),
         revisions: {
@@ -2690,7 +2677,7 @@
     const revision1 = createRevision(1);
     const revision2 = createRevision(2);
     const revision3 = createEditRevision();
-    sinon.stub(element.restApiService, 'getChangeDetail').returns(
+    stubRestApi('getChangeDetail').returns(
       Promise.resolve({
         ...createChange(),
         revisions: {
@@ -2839,9 +2826,9 @@
     let getMergeableStub: SinonStubbedMember<RestApiService['getMergeable']>;
     setup(() => {
       element._change = {...createChange(), labels: {}};
-      getMergeableStub = sinon
-        .stub(element.restApiService, 'getMergeable')
-        .returns(Promise.resolve({...createMergeable(), mergeable: true}));
+      getMergeableStub = stubRestApi('getMergeable').returns(
+        Promise.resolve({...createMergeable(), mergeable: true})
+      );
     });
 
     test('merged change', () => {
@@ -2874,16 +2861,14 @@
   test('_paramsChanged sets in projectLookup', () => {
     sinon.stub(element.$.relatedChanges, 'reload');
     sinon.stub(element, '_reload').returns(Promise.resolve([]));
-    const setStub = sinon.stub(element.restApiService, 'setInProjectLookup');
+    const setStub = stubRestApi('setInProjectLookup');
     element._paramsChanged({
       view: GerritNav.View.CHANGE,
       changeNum: 101 as NumericChangeId,
       project: TEST_PROJECT_NAME,
     });
     assert.isTrue(setStub.calledOnce);
-    assert.isTrue(
-      setStub.calledWith(101 as NumericChangeId, TEST_PROJECT_NAME)
-    );
+    assert.isTrue(setStub.calledWith(101 as never, TEST_PROJECT_NAME as never));
   });
 
   test('_handleToggleStar called when star is tapped', () => {
@@ -2911,9 +2896,12 @@
       sinon.stub(element, '_reloadComments').returns(Promise.resolve());
       sinon.stub(element, '_getMergeability').returns(Promise.resolve());
       sinon.stub(element, '_getLatestCommitMessage').returns(Promise.resolve());
+      sinon
+        .stub(element, '_reloadPatchNumDependentResources')
+        .returns(Promise.resolve([undefined, undefined]));
     });
 
-    test("don't report changedDisplayed on reply", done => {
+    test("don't report changeDisplayed on reply", done => {
       const changeDisplayStub = sinon.stub(
         element.reporting,
         'changeDisplayed'
@@ -2930,7 +2918,7 @@
       });
     });
 
-    test('report changedDisplayed on _paramsChanged', done => {
+    test('report changeDisplayed on _paramsChanged', done => {
       const changeDisplayStub = sinon.stub(
         element.reporting,
         'changeDisplayed'
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
index 5564fcf..e4ed533 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-confirm-cherrypick-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-confirm-cherrypick-dialog');
 
@@ -28,20 +29,18 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getRepoBranches(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve([
-            {
-              ref: 'refs/heads/test-branch',
-              revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
-              can_delete: true,
-            },
-          ]);
-        } else {
-          return Promise.resolve({});
-        }
-      },
+    stubRestApi('getRepoBranches').callsFake(input => {
+      if (input.startsWith('test')) {
+        return Promise.resolve([
+          {
+            ref: 'refs/heads/test-branch',
+            revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+            can_delete: true,
+          },
+        ]);
+      } else {
+        return Promise.resolve({});
+      }
     });
     element = basicFixture.instantiate();
     element.project = 'test-project';
@@ -116,7 +115,7 @@
 
     test('cherry pick topic submit', done => {
       element.branch = 'master';
-      const executeChangeActionStub = sinon.stub(element.restApiService,
+      const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       MockInteractions.tap(element.shadowRoot.
           querySelector('gr-dialog').$.confirm);
@@ -134,7 +133,7 @@
 
     test('deselecting a change removes it from being cherry picked', () => {
       element.branch = 'master';
-      const executeChangeActionStub = sinon.stub(element.restApiService,
+      const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       const checkboxes = element.shadowRoot.querySelectorAll(
           'input[type="checkbox"]');
@@ -149,7 +148,7 @@
 
     test('deselecting all change shows error message', () => {
       element.branch = 'master';
-      const executeChangeActionStub = sinon.stub(element.restApiService,
+      const executeChangeActionStub = stubRestApi(
           'executeChangeAction').returns(Promise.resolve([]));
       const checkboxes = element.shadowRoot.querySelectorAll(
           'input[type="checkbox"]');
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
index 43bde75..db00f6b 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-confirm-move-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-confirm-move-dialog');
 
@@ -24,20 +25,18 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getRepoBranches(input) {
-        if (input.startsWith('test')) {
-          return Promise.resolve([
-            {
-              ref: 'refs/heads/test-branch',
-              revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
-              can_delete: true,
-            },
-          ]);
-        } else {
-          return Promise.resolve(undefined);
-        }
-      },
+    stubRestApi('getRepoBranches').callsFake(input => {
+      if (input.startsWith('test')) {
+        return Promise.resolve([
+          {
+            ref: 'refs/heads/test-branch',
+            revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+            can_delete: true,
+          },
+        ]);
+      } else {
+        return Promise.resolve(undefined);
+      }
     });
     element = basicFixture.instantiate();
     element.project = 'test-project';
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
index 8b3b73a..0faf604 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-confirm-rebase-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-confirm-rebase-dialog');
 
@@ -102,6 +103,7 @@
 
   suite('parent suggestions', () => {
     let recentChanges;
+    let getChangesStub;
     setup(() => {
       recentChanges = [
         {
@@ -118,7 +120,7 @@
         },
       ];
 
-      sinon.stub(element.restApiService, 'getChanges').returns(Promise.resolve(
+      getChangesStub = stubRestApi('getChanges').returns(Promise.resolve(
           [
             {
               _number: 123,
@@ -141,29 +143,26 @@
       return element._getRecentChanges()
           .then(() => {
             assert.deepEqual(element._recentChanges, recentChanges);
-            assert.equal(element.restApiService.getChanges.callCount, 1);
+            assert.equal(getChangesStub.callCount, 1);
             // When called a second time, should not re-request recent changes.
             element._getRecentChanges();
           })
           .then(() => {
             assert.equal(element._getRecentChanges.callCount, 2);
-            assert.equal(element.restApiService.getChanges.callCount, 1);
+            assert.equal(getChangesStub.callCount, 1);
           });
     });
 
     test('_filterChanges', () => {
       assert.equal(element._filterChanges('123', recentChanges).length, 1);
       assert.equal(element._filterChanges('12', recentChanges).length, 2);
-      assert.equal(element._filterChanges('awesome', recentChanges).length,
-          3);
-      assert.equal(element._filterChanges('third', recentChanges).length,
-          1);
+      assert.equal(element._filterChanges('awesome', recentChanges).length, 3);
+      assert.equal(element._filterChanges('third', recentChanges).length, 1);
 
       element.changeNumber = 123;
       assert.equal(element._filterChanges('123', recentChanges).length, 0);
       assert.equal(element._filterChanges('124', recentChanges).length, 1);
-      assert.equal(element._filterChanges('awesome', recentChanges).length,
-          2);
+      assert.equal(element._filterChanges('awesome', recentChanges).length, 2);
     });
 
     test('input text change triggers function', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
index b8670c0..f877527 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header_test.js
@@ -21,6 +21,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import 'lodash/lodash.js';
 import {createRevisions} from '../../../test/test-data-generators.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-file-list-header');
 
@@ -28,11 +29,8 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({test: 'config'}); },
-      getAccount() { return Promise.resolve(null); },
-      _fetchSharedCacheURL() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({test: 'config'}));
+    stubRestApi('getAccount').returns(Promise.resolve(null));
     element = basicFixture.instantiate();
   });
 
@@ -86,7 +84,7 @@
   });
 
   test('description editing', () => {
-    const putDescStub = sinon.stub(element.restApiService, 'setDescription')
+    const putDescStub = stubRestApi('setDescription')
         .returns(Promise.resolve({ok: true}));
 
     element.changeNum = '42';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
index d0ae833..77855df 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.js
@@ -26,7 +26,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
-import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
+import {TestKeyboardShortcutBinder, stubRestApi, spyRestApi} from '../../../test/test-utils.js';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {createCommentThreads} from '../../../utils/comment-util.js';
 import {createChangeComments} from '../../../test/test-data-generators.js';
@@ -80,15 +80,12 @@
 
   suite('basic tests', () => {
     setup(done => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-        getPreferences() { return Promise.resolve({}); },
-        getDiffPreferences() { return Promise.resolve({}); },
-        getDiffComments() { return Promise.resolve({}); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-        getAccountCapabilities() { return Promise.resolve({}); },
-      });
+      stubRestApi('getLoggedIn').returns(Promise.resolve(true));
+      stubRestApi('getPreferences').returns(Promise.resolve({}));
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+      stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
       stub('gr-date-formatter', {
         _loadTimeFormat() { return Promise.resolve(''); },
       });
@@ -1419,13 +1416,10 @@
     }
 
     setup(done => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-        getPreferences() { return Promise.resolve({}); },
-        getDiffComments() { return Promise.resolve({}); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-      });
+      stubRestApi('getPreferences').returns(Promise.resolve({}));
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       stub('gr-date-formatter', {
         _loadTimeFormat() { return Promise.resolve(''); },
       });
@@ -1674,7 +1668,7 @@
       });
 
       test('_getReviewedFiles does not call API', () => {
-        const apiSpy = sinon.spy(element.restApiService, 'getReviewedFiles');
+        const apiSpy = spyRestApi('getReviewedFiles');
         element.editMode = true;
         return element._getReviewedFiles().then(files => {
           assert.equal(files.length, 0);
diff --git a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
index ae639e1..5135e439 100644
--- a/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
+++ b/polygerrit-ui/app/elements/change/gr-label-scores/gr-label-scores_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-label-scores.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-label-scores');
 
@@ -24,9 +25,7 @@
   let element;
 
   setup(done => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(false); },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
     element = basicFixture.instantiate();
     element.change = {
       _number: '123',
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
index facde01..e2c7e86 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.js
@@ -19,6 +19,7 @@
 import './gr-message.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {createChange, createRevisions} from '../../../test/test-data-generators.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-message');
 
@@ -27,13 +28,11 @@
 
   suite('when admin and logged in', () => {
     setup(done => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-        getPreferences() { return Promise.resolve({}); },
-        getConfig() { return Promise.resolve({}); },
-        getIsAdmin() { return Promise.resolve(true); },
-        deleteChangeCommitMessage() { return Promise.resolve({}); },
-      });
+      stubRestApi('getLoggedIn').returns(Promise.resolve(true));
+      stubRestApi('getPreferences').returns(Promise.resolve({}));
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('getIsAdmin').returns(Promise.resolve(true));
+      stubRestApi('deleteChangeCommitMessage').returns(Promise.resolve({}));
       element = basicFixture.instantiate();
       flush(done);
     });
@@ -415,13 +414,11 @@
 
   suite('when not logged in', () => {
     setup(done => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(false); },
-        getPreferences() { return Promise.resolve({}); },
-        getConfig() { return Promise.resolve({}); },
-        getIsAdmin() { return Promise.resolve(false); },
-        deleteChangeCommitMessage() { return Promise.resolve({}); },
-      });
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+      stubRestApi('getPreferences').returns(Promise.resolve({}));
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('getIsAdmin').returns(Promise.resolve(false));
+      stubRestApi('deleteChangeCommitMessage').returns(Promise.resolve({}));
       element = basicFixture.instantiate();
       flush(done);
     });
@@ -510,15 +507,13 @@
   });
 
   suite('when logged in but not admin', () => {
-    setup(done => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(true); },
-        getConfig() { return Promise.resolve({}); },
-        getIsAdmin() { return Promise.resolve(false); },
-        deleteChangeCommitMessage() { return Promise.resolve({}); },
-      });
+    setup(async () => {
+      stubRestApi('getLoggedIn').returns(Promise.resolve(true));
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('getIsAdmin').returns(Promise.resolve(false));
+      stubRestApi('deleteChangeCommitMessage').returns(Promise.resolve({}));
       element = basicFixture.instantiate();
-      flush(done);
+      await flush();
     });
 
     test('can see reply but not delete button', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
index c27f9fd..d22c7d0 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.js
@@ -22,6 +22,7 @@
 import {TEST_ONLY} from './gr-messages-list.js';
 import {MessageTag} from '../../../constants/constants.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 createCommentApiMockWithTemplateElement(
     'gr-messages-list-comment-mock-api', html`
@@ -132,13 +133,11 @@
 
   suite('basic tests', () => {
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({}); },
-        getLoggedIn() { return Promise.resolve(false); },
-        getDiffComments() { return Promise.resolve(comments); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-      });
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+      stubRestApi('getDiffComments').returns(Promise.resolve(comments));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
       messages = generateRandomMessages(3);
       // Element must be wrapped in an element with direct access to the
@@ -441,13 +440,11 @@
     let commentApiWrapper;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({}); },
-        getLoggedIn() { return Promise.resolve(false); },
-        getDiffComments() { return Promise.resolve({}); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-      });
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
       messages = [
         randomMessage(),
diff --git a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
index e71df971..f54b819 100644
--- a/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-related-changes-list/gr-related-changes-list_test.js
@@ -21,6 +21,7 @@
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {resetPlugins} from '../../../test/test-utils.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
@@ -237,14 +238,10 @@
     };
     element.mergeable = true;
     element.addEventListener('new-section-loaded', loadedStub);
-    sinon.stub(element.restApiService, 'getRelatedChanges')
-        .returns(Promise.resolve({changes: []}));
-    sinon.stub(element.restApiService, 'getChangesSubmittedTogether')
-        .returns(Promise.resolve());
-    sinon.stub(element.restApiService, 'getChangeCherryPicks')
-        .returns(Promise.resolve());
-    sinon.stub(element.restApiService, 'getChangeConflicts')
-        .returns(Promise.resolve());
+    stubRestApi('getRelatedChanges').returns(Promise.resolve({changes: []}));
+    stubRestApi('getChangesSubmittedTogether').returns(Promise.resolve());
+    stubRestApi('getChangeCherryPicks').returns(Promise.resolve());
+    stubRestApi('getChangeConflicts').returns(Promise.resolve());
 
     return element.reload().then(() => {
       assert.equal(loadedStub.callCount, 4);
@@ -257,14 +254,10 @@
     setup(() => {
       element = basicFixture.instantiate();
 
-      sinon.stub(element.restApiService, 'getRelatedChanges')
-          .returns(Promise.resolve({changes: []}));
-      sinon.stub(element.restApiService, 'getChangesSubmittedTogether')
-          .returns(Promise.resolve());
-      sinon.stub(element.restApiService, 'getChangeCherryPicks')
-          .returns(Promise.resolve());
-      sinon.stub(element.restApiService, 'getChangeConflicts')
-          .returns(Promise.resolve());
+      stubRestApi('getRelatedChanges').returns(Promise.resolve({changes: []}));
+      stubRestApi('getChangesSubmittedTogether').returns(Promise.resolve());
+      stubRestApi('getChangeCherryPicks').returns(Promise.resolve());
+      stubRestApi('getChangeConflicts').returns(Promise.resolve());
     });
 
     test('_conflicts are an empty array', () => {
@@ -286,14 +279,11 @@
     setup(() => {
       element = basicFixture.instantiate();
 
-      sinon.stub(element.restApiService, 'getRelatedChanges')
-          .returns(Promise.resolve({changes: []}));
-      sinon.stub(element.restApiService, 'getChangesSubmittedTogether')
-          .returns(Promise.resolve());
-      sinon.stub(element.restApiService, 'getChangeCherryPicks')
-          .returns(Promise.resolve());
-      conflictsStub = sinon.stub(element.restApiService, 'getChangeConflicts')
-          .returns(Promise.resolve());
+      stubRestApi('getRelatedChanges').returns(Promise.resolve({changes: []}));
+      stubRestApi('getChangesSubmittedTogether').returns(Promise.resolve());
+      stubRestApi('getChangeCherryPicks').returns(Promise.resolve());
+      conflictsStub = stubRestApi('getChangeConflicts').returns(
+          Promise.resolve());
     });
 
     test('request conflicts if open and mergeable', () => {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
index 8a4b1f6..b09510ba 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.js
@@ -21,6 +21,8 @@
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {getPluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
+
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 const pluginApi = _testOnly_initGerritPluginApi();
 
@@ -73,10 +75,8 @@
     changeNum = 42;
     patchNum = 1;
 
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getAccount() { return Promise.resolve({_account_id: 42}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getAccount').returns(Promise.resolve({_account_id: 42}));
 
     element = basicFixture.instantiate();
     setupElement(element);
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
index 09f8636..7b045a7 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.js
@@ -22,6 +22,8 @@
 import {SpecialFilePath} from '../../../constants/constants.js';
 import {appContext} from '../../../services/app-context.js';
 import {addListenerForTest} from '../../../test/test-utils.js';
+import {stubRestApi} from '../../../test/test-utils.js';
+import {JSON_PREFIX} from '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
@@ -61,12 +63,10 @@
     changeNum = 42;
     patchNum = 1;
 
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getAccount() { return Promise.resolve({}); },
-      getChange() { return Promise.resolve({}); },
-      getChangeSuggestedReviewers() { return Promise.resolve([]); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getAccount').returns(Promise.resolve({}));
+    stubRestApi('getChange').returns(Promise.resolve({}));
+    stubRestApi('getChangeSuggestedReviewers').returns(Promise.resolve([]));
 
     sinon.stub(appContext.flagsService, 'isEnabled').returns(true);
 
@@ -131,8 +131,7 @@
         .callsFake(review => new Promise((resolve, reject) => {
           try {
             const result = jsonResponseProducer(review) || {};
-            const resultStr =
-            element.restApiService.JSON_PREFIX + JSON.stringify(result);
+            const resultStr = JSON_PREFIX + JSON.stringify(result);
             resolve({
               ok: true,
               text() {
@@ -780,10 +779,15 @@
   });
 
   test('400 converts to human-readable server-error', done => {
-    sinon.stub(window, 'fetch').callsFake(() => {
-      const text = '....{"reviewers":{"id1":{"error":"human readable"}}}';
-      return Promise.resolve(cloneableResponse(400, text));
-    });
+    stubRestApi('saveChangeReview').callsFake(
+        (changeNum, patchNum, review, errFn) => {
+          errFn(cloneableResponse(
+              400,
+              '....{"reviewers":{"id1":{"error":"human readable"}}}'
+          ));
+          return Promise.resolve(undefined);
+        }
+    );
 
     const listener = event => {
       if (event.target !== document) return;
@@ -799,10 +803,12 @@
   });
 
   test('non-json 400 is treated as a normal server-error', done => {
-    sinon.stub(window, 'fetch').callsFake(() => {
-      const text = 'Comment validation error!';
-      return Promise.resolve(cloneableResponse(400, text));
-    });
+    stubRestApi('saveChangeReview').callsFake(
+        (changeNum, patchNum, review, errFn) => {
+          errFn(cloneableResponse(400, 'Comment validation error!'));
+          return Promise.resolve(undefined);
+        }
+    );
 
     const listener = event => {
       if (event.target !== document) return;
@@ -978,7 +984,7 @@
   });
 
   test('_removeAccount', done => {
-    sinon.stub(element.restApiService, 'removeChangeReviewer')
+    stubRestApi('removeChangeReviewer')
         .returns(Promise.resolve({ok: true}));
     const arr = [makeAccount(), makeAccount()];
     element.change.reviewers = {
@@ -1338,32 +1344,29 @@
     });
 
     suite('pending diff drafts?', () => {
-      test('yes', () => {
+      test('yes', async () => {
         const promise = mockPromise();
-        const refreshHandler = sinon.stub();
+        const refreshSpy = sinon.spy();
+        element.addEventListener('comment-refresh', refreshSpy);
+        stubRestApi('hasPendingDiffDrafts').returns(true);
+        stubRestApi('awaitPendingDiffDrafts').returns(promise);
 
-        element.addEventListener('comment-refresh', refreshHandler);
-        sinon.stub(element.restApiService, 'hasPendingDiffDrafts').returns(
-            true);
-        element.restApiService._pendingRequests.sendDiffDraft = [promise];
         element.open();
 
-        assert.isFalse(refreshHandler.called);
+        assert.isFalse(refreshSpy.called);
         assert.isTrue(element._savingComments);
 
         promise.resolve();
+        await flush();
 
-        return element.restApiService.awaitPendingDiffDrafts().then(() => {
-          assert.isTrue(refreshHandler.called);
-          assert.isFalse(element._savingComments);
-        });
+        assert.isTrue(refreshSpy.called);
+        assert.isFalse(element._savingComments);
       });
 
       test('no', () => {
-        sinon.stub(element.restApiService, 'hasPendingDiffDrafts').returns(
-            false);
+        stubRestApi('hasPendingDiffDrafts').returns(false);
         element.open();
-        assert.notOk(element._savingComments);
+        assert.isFalse(element._savingComments);
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
index 89332c2..f77f736 100644
--- a/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
+++ b/polygerrit-ui/app/elements/change/gr-reviewer-list/gr-reviewer-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-reviewer-list.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-reviewer-list');
 
@@ -27,12 +28,8 @@
     element = basicFixture.instantiate();
     element.serverConfig = {};
 
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      removeChangeReviewer() {
-        return Promise.resolve({ok: true});
-      },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
   });
 
   test('controls hidden on immutable element', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
index a8f206c..dc5d1b4 100644
--- a/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
+++ b/polygerrit-ui/app/elements/core/gr-account-dropdown/gr-account-dropdown_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-account-dropdown.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-account-dropdown');
 
@@ -24,9 +25,7 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
index e868f03..8e5a2b3 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.ts
@@ -28,9 +28,6 @@
 import {appContext} from '../../../services/app-context';
 import {IronA11yAnnouncer} from '@polymer/iron-a11y-announcer/iron-a11y-announcer';
 import {customElement, property} from '@polymer/decorators';
-import {ReportingService} from '../../../services/gr-reporting/gr-reporting';
-import {AuthService} from '../../../services/gr-auth/gr-auth';
-import {EventEmitterService} from '../../../services/gr-event-interface/gr-event-interface';
 import {GrOverlay} from '../../shared/gr-overlay/gr-overlay';
 import {GrErrorDialog} from '../gr-error-dialog/gr-error-dialog';
 import {GrAlert} from '../../shared/gr-alert/gr-alert';
@@ -105,23 +102,16 @@
   @property({type: String})
   loginUrl = '/login';
 
-  reporting: ReportingService;
+  private readonly reporting = appContext.reportingService;
 
-  _authService: AuthService;
+  private readonly _authService = appContext.authService;
 
-  eventEmitter: EventEmitterService;
+  private readonly eventEmitter = appContext.eventEmitter;
 
   _authErrorHandlerDeregistrationHook?: Function;
 
   private readonly restApiService = appContext.restApiService;
 
-  constructor() {
-    super();
-    this._authService = appContext.authService;
-    this.reporting = appContext.reportingService;
-    this.eventEmitter = appContext.eventEmitter;
-  }
-
   /** @override */
   attached() {
     super.attached();
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
index 7fc35c6..092daa2 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.js
@@ -19,6 +19,8 @@
 import './gr-error-manager.js';
 import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 import {__testOnly_ErrorType} from './gr-error-manager.js';
+import {stubRestApi} from '../../../test/test-utils.js';
+import {appContext} from '../../../services/app-context.js';
 
 const basicFixture = fixtureFromElement('gr-error-manager');
 
@@ -30,10 +32,14 @@
   suite('when authed', () => {
     let toastSpy;
     let openOverlaySpy;
+    let fetchStub;
+    let getLoggedInStub;
 
     setup(() => {
-      sinon.stub(window, 'fetch')
+      fetchStub = sinon.stub(window, 'fetch')
           .returns(Promise.resolve({ok: true, status: 204}));
+      getLoggedInStub = stubRestApi('getLoggedIn')
+          .callsFake(() => appContext.authService.authCheck());
       element = basicFixture.instantiate();
       element._authService.clearCache();
       toastSpy = sinon.spy(element, '_createToastAlert');
@@ -67,8 +73,7 @@
               element, '_showAuthErrorAlert'
           );
           const responseText = Promise.resolve('Authentication required\n');
-          sinon.stub(element.restApiService, 'getLoggedIn')
-              .returns(Promise.resolve(true));
+          getLoggedInStub.returns(Promise.resolve(true));
           element.dispatchEvent(
               new CustomEvent('server-error', {
                 detail:
@@ -81,36 +86,33 @@
           });
         });
 
-    test('recheck auth for 403 with auth error if authed before', done => {
-      // starts with authed state
-      element.restApiService.getLoggedIn();
+    test('recheck auth for 403 with auth error if authed before', async () => {
+      // Set status to AUTHED.
+      appContext.authService.authCheck();
       const responseText = Promise.resolve('Authentication required\n');
-      sinon.stub(element.restApiService, 'getLoggedIn')
-          .returns(Promise.resolve(true));
+      getLoggedInStub.returns(Promise.resolve(true));
       element.dispatchEvent(
           new CustomEvent('server-error', {
             detail:
           {response: {status: 403, text() { return responseText; }}},
             composed: true, bubbles: true,
           }));
-      flush(() => {
-        assert.isTrue(element.restApiService.getLoggedIn.calledOnce);
-        done();
-      });
+      await flush();
+      assert.isTrue(getLoggedInStub.calledOnce);
     });
 
     test('show logged in error', () => {
-      sinon.stub(element, '_showAuthErrorAlert');
+      const spy = sinon.spy(element, '_showAuthErrorAlert');
       element.dispatchEvent(
           new CustomEvent('show-auth-required', {
             composed: true, bubbles: true,
           }));
-      assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
+      assert.isTrue(spy.calledWithExactly(
           'Log in is required to perform that action.', 'Log in.'));
     });
 
     test('show normal Error', done => {
-      const showErrorStub = sinon.stub(element, '_showErrorDialog');
+      const showErrorSpy = sinon.spy(element, '_showErrorDialog');
       const textSpy = sinon.spy(() => Promise.resolve('ZOMG'));
       element.dispatchEvent(
           new CustomEvent('server-error', {
@@ -120,8 +122,8 @@
 
       assert.isTrue(textSpy.called);
       flush(() => {
-        assert.isTrue(showErrorStub.calledOnce);
-        assert.isTrue(showErrorStub.lastCall.calledWithExactly(
+        assert.isTrue(showErrorSpy.calledOnce);
+        assert.isTrue(showErrorSpy.lastCall.calledWithExactly(
             'Error 500: ZOMG'));
         done();
       });
@@ -240,27 +242,27 @@
     });
 
     test('show auth refresh toast', async () => {
-      // starts with authed state
-      element.restApiService.getLoggedIn();
-      const refreshStub = sinon.stub(element.restApiService,
+      // Set status to AUTHED.
+      appContext.authService.authCheck();
+      const refreshStub = stubRestApi(
           'getAccount').callsFake(
           () => Promise.resolve({}));
       const windowOpen = sinon.stub(window, 'open');
       const responseText = Promise.resolve('Authentication required\n');
       // fake failed auth
-      window.fetch.returns(Promise.resolve({status: 403}));
+      fetchStub.returns(Promise.resolve({status: 403}));
       element.dispatchEvent(
           new CustomEvent('server-error', {
             detail:
           {response: {status: 403, text() { return responseText; }}},
             composed: true, bubbles: true,
           }));
-      assert.equal(window.fetch.callCount, 1);
+      assert.equal(fetchStub.callCount, 1);
       await flush();
 
       // here needs two flush as there are two chanined
       // promises on server-error handler and flush only flushes one
-      assert.equal(window.fetch.callCount, 2);
+      assert.equal(fetchStub.callCount, 2);
       await flush();
       // Sometime overlay opens with delay, waiting while open is complete
       await openOverlaySpy.lastCall.returnValue;
@@ -294,7 +296,7 @@
       const hideToastSpy = sinon.spy(toast, 'hide');
 
       // now fake authed
-      window.fetch.returns(Promise.resolve({status: 204}));
+      fetchStub.returns(Promise.resolve({status: 204}));
       element._handleWindowFocus();
       element.flushDebouncer('checkLoggedIn');
       await flush();
@@ -313,8 +315,8 @@
     });
 
     test('auth toast should dismiss existing toast', async () => {
-      // starts with authed state
-      element.restApiService.getLoggedIn();
+      // Set status to AUTHED.
+      appContext.authService.authCheck();
       const responseText = Promise.resolve('Authentication required\n');
 
       // fake an alert
@@ -329,18 +331,18 @@
           toast.root.textContent, 'test reload');
 
       // fake auth
-      window.fetch.returns(Promise.resolve({status: 403}));
+      fetchStub.returns(Promise.resolve({status: 403}));
       element.dispatchEvent(
           new CustomEvent('server-error', {
             detail:
           {response: {status: 403, text() { return responseText; }}},
             composed: true, bubbles: true,
           }));
-      assert.equal(window.fetch.callCount, 1);
+      assert.equal(fetchStub.callCount, 1);
       await flush();
       // here needs two flush as there are two chained
       // promises on server-error handler and flush only flushes one
-      assert.equal(window.fetch.callCount, 2);
+      assert.equal(fetchStub.callCount, 2);
       await flush();
       // Sometime overlay opens with delay, waiting while open is complete
       await openOverlaySpy.lastCall.returnValue;
@@ -353,8 +355,8 @@
     });
 
     test('regular toast should dismiss regular toast', () => {
-      // starts with authed state
-      element.restApiService.getLoggedIn();
+      // Set status to AUTHED.
+      appContext.authService.authCheck();
 
       // fake an alert
       element.dispatchEvent(
@@ -379,23 +381,23 @@
     });
 
     test('regular toast should not dismiss auth toast', done => {
-      // starts with authed state
-      element.restApiService.getLoggedIn();
+      // Set status to AUTHED.
+      appContext.authService.authCheck();
       const responseText = Promise.resolve('Authentication required\n');
 
       // fake auth
-      window.fetch.returns(Promise.resolve({status: 403}));
+      fetchStub.returns(Promise.resolve({status: 403}));
       element.dispatchEvent(
           new CustomEvent('server-error', {
             detail:
           {response: {status: 403, text() { return responseText; }}},
             composed: true, bubbles: true,
           }));
-      assert.equal(window.fetch.callCount, 1);
+      assert.equal(fetchStub.callCount, 1);
       flush(() => {
         // here needs two flush as there are two chained
         // promises on server-error handler and flush only flushes one
-        assert.equal(window.fetch.callCount, 2);
+        assert.equal(fetchStub.callCount, 2);
         flush(() => {
           let toast = toastSpy.lastCall.returnValue;
           assert.include(
@@ -457,7 +459,7 @@
 
     test('refreshes with same credentials', done => {
       const accountPromise = Promise.resolve({_account_id: 1234});
-      sinon.stub(element.restApiService, 'getAccount')
+      stubRestApi('getAccount')
           .returns(accountPromise);
       const requestCheckStub = sinon.stub(element, '_requestCheckLoggedIn');
       const handleRefreshStub = sinon.stub(element,
@@ -514,7 +516,7 @@
 
     test('reloads when refreshed credentials differ', done => {
       const accountPromise = Promise.resolve({_account_id: 1234});
-      sinon.stub(element.restApiService, 'getAccount')
+      stubRestApi('getAccount')
           .returns(accountPromise);
       const requestCheckStub = sinon.stub(
           element,
@@ -539,9 +541,7 @@
   suite('when not authed', () => {
     let toastSpy;
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return Promise.resolve(false); },
-      });
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       element = basicFixture.instantiate();
       toastSpy = sinon.spy(element, '_createToastAlert');
     });
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index 3ab40e9..b13e2e6 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -16,7 +16,7 @@
  */
 
 import '../../../test/common-test-setup-karma';
-import {isHidden, query} from '../../../test/test-utils';
+import {isHidden, query, stubRestApi} from '../../../test/test-utils';
 import './gr-main-header';
 import {GrMainHeader} from './gr-main-header';
 import {
@@ -33,14 +33,7 @@
   let element: GrMainHeader;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() {
-        return Promise.resolve(createServerInfo());
-      },
-      probePath(_) {
-        return Promise.resolve(false);
-      },
-    });
+    stubRestApi('probePath').returns(Promise.resolve(false));
     stub('gr-main-header', {
       _loadAccount() {
         return Promise.resolve();
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
index ff8f8df..16d6370 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router_test.js
@@ -21,6 +21,7 @@
 import {GerritNav} from '../gr-navigation/gr-navigation.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
 import {_testOnly_RoutePattern} from './gr-router.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-router');
 
@@ -229,7 +230,7 @@
   });
 
   test('_redirectIfNotLoggedIn while logged in', () => {
-    sinon.stub(element.restApiService, 'getLoggedIn')
+    stubRestApi('getLoggedIn')
         .returns(Promise.resolve(true));
     const data = {canonicalPath: ''};
     const redirectStub = sinon.stub(element, '_redirectToLogin');
@@ -239,7 +240,7 @@
   });
 
   test('_redirectIfNotLoggedIn while logged out', () => {
-    sinon.stub(element.restApiService, 'getLoggedIn')
+    stubRestApi('getLoggedIn')
         .returns(Promise.resolve(false));
     const redirectStub = sinon.stub(element, '_redirectToLogin');
     const data = {canonicalPath: ''};
@@ -523,8 +524,7 @@
     let generateUrlStub;
 
     setup(() => {
-      projectLookupStub = sinon
-          .stub(element.restApiService, 'getFromProjectLookup');
+      projectLookupStub = stubRestApi('getFromProjectLookup');
       generateUrlStub = sinon.stub(element, '_generateUrl');
     });
 
@@ -794,8 +794,7 @@
       });
 
       test('redirects to dashboard if logged in', () => {
-        sinon.stub(element.restApiService, 'getLoggedIn')
-            .returns(Promise.resolve(true));
+        stubRestApi('getLoggedIn').returns(Promise.resolve(true));
         const data = {
           canonicalPath: '/', path: '/', querystring: '', hash: '',
         };
@@ -807,8 +806,7 @@
       });
 
       test('redirects to open changes if not logged in', () => {
-        sinon.stub(element.restApiService, 'getLoggedIn')
-            .returns(Promise.resolve(false));
+        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
         const data = {
           canonicalPath: '/', path: '/', querystring: '', hash: '',
         };
@@ -905,8 +903,7 @@
       });
 
       test('own dashboard but signed out redirects to login', () => {
-        sinon.stub(element.restApiService, 'getLoggedIn')
-            .returns(Promise.resolve(false));
+        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
         const data = {canonicalPath: '/dashboard/', params: {0: 'seLF'}};
         return element._handleDashboardRoute(data, '').then(() => {
           assert.isTrue(redirectToLoginStub.calledOnce);
@@ -916,8 +913,7 @@
       });
 
       test('non-self dashboard but signed out does not redirect', () => {
-        sinon.stub(element.restApiService, 'getLoggedIn')
-            .returns(Promise.resolve(false));
+        stubRestApi('getLoggedIn').returns(Promise.resolve(false));
         const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
         return element._handleDashboardRoute(data, '').then(() => {
           assert.isFalse(redirectToLoginStub.called);
@@ -928,8 +924,7 @@
       });
 
       test('dashboard while signed in sets params', () => {
-        sinon.stub(element.restApiService, 'getLoggedIn')
-            .returns(Promise.resolve(true));
+        stubRestApi('getLoggedIn').returns(Promise.resolve(true));
         const data = {canonicalPath: '/dashboard/', params: {0: 'foo'}};
         return element._handleDashboardRoute(data, '').then(() => {
           assert.isFalse(redirectToLoginStub.called);
@@ -1444,7 +1439,7 @@
         setup(() => {
           normalizeRangeStub = sinon.stub(element,
               '_normalizePatchRangeParams');
-          sinon.stub(element.restApiService, 'setInProjectLookup');
+          stubRestApi('setInProjectLookup');
         });
 
         test('needs redirect', () => {
@@ -1498,7 +1493,7 @@
         setup(() => {
           normalizeRangeStub = sinon.stub(element,
               '_normalizePatchRangeParams');
-          sinon.stub(element.restApiService, 'setInProjectLookup');
+          stubRestApi('setInProjectLookup');
         });
 
         test('needs redirect', () => {
@@ -1551,7 +1546,7 @@
       test('_handleDiffEditRoute', () => {
         const normalizeRangeSpy =
             sinon.spy(element, '_normalizePatchRangeParams');
-        sinon.stub(element.restApiService, 'setInProjectLookup');
+        stubRestApi('setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
@@ -1580,7 +1575,7 @@
       test('_handleDiffEditRoute with lineNum', () => {
         const normalizeRangeSpy =
             sinon.spy(element, '_normalizePatchRangeParams');
-        sinon.stub(element.restApiService, 'setInProjectLookup');
+        stubRestApi('setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
@@ -1610,7 +1605,7 @@
       test('_handleChangeEditRoute', () => {
         const normalizeRangeSpy =
             sinon.spy(element, '_normalizePatchRangeParams');
-        sinon.stub(element.restApiService, 'setInProjectLookup');
+        stubRestApi('setInProjectLookup');
         const ctx = {
           params: [
             'foo/bar', // 0 Project
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
index e470618..e553402 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.js
@@ -21,6 +21,7 @@
 import {TestKeyboardShortcutBinder} from '../../../test/test-utils.js';
 import {Shortcut} from '../../../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin.js';
 import {_testOnly_clearDocsBaseUrlCache} from '../../../utils/url-util.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-search-bar');
 
@@ -123,6 +124,12 @@
   });
 
   suite('_getSearchSuggestions', () => {
+    setup(() => {
+      // Ensure that config.change.mergeability_computation_behavior is not set.
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      element = basicFixture.instantiate();
+    });
+
     test('Autocompletes accounts', () => {
       sinon.stub(element, 'accountSuggestions').callsFake(() =>
         Promise.resolve([{text: 'owner:fred@goog.co'}])
@@ -188,15 +195,11 @@
   ].forEach(mergeability => {
     suite(`mergeability as ${mergeability}`, () => {
       setup(done => {
-        stub('gr-rest-api-interface', {
-          getConfig() {
-            return Promise.resolve({
-              change: {
-                mergeability_computation_behavior: mergeability,
-              },
-            });
+        stubRestApi('getConfig').returns(Promise.resolve({
+          change: {
+            mergeability_computation_behavior: mergeability,
           },
-        });
+        }));
 
         element = basicFixture.instantiate();
         flush(done);
@@ -217,15 +220,11 @@
 
   suite('doc url', () => {
     setup(done => {
-      stub('gr-rest-api-interface', {
-        getConfig() {
-          return Promise.resolve({
-            gerrit: {
-              doc_url: 'https://doc.com/',
-            },
-          });
+      stubRestApi('getConfig').returns(Promise.resolve({
+        gerrit: {
+          doc_url: 'https://doc.com/',
         },
-      });
+      }));
 
       _testOnly_clearDocsBaseUrlCache();
       element = basicFixture.instantiate();
diff --git a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
index d7e6c27..f3a9965 100644
--- a/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
+++ b/polygerrit-ui/app/elements/core/gr-smart-search/gr-smart-search_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-smart-search.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-smart-search');
 
@@ -28,7 +29,7 @@
   });
 
   test('Autocompletes accounts', () => {
-    sinon.stub(element.restApiService, 'getSuggestedAccounts').callsFake(() =>
+    stubRestApi('getSuggestedAccounts').callsFake(() =>
       Promise.resolve([
         {
           name: 'fred',
@@ -42,7 +43,7 @@
   });
 
   test('Inserts self as option when valid', () => {
-    sinon.stub(element.restApiService, 'getSuggestedAccounts').callsFake( () =>
+    stubRestApi('getSuggestedAccounts').callsFake( () =>
       Promise.resolve([
         {
           name: 'fred',
@@ -62,7 +63,7 @@
   });
 
   test('Inserts me as option when valid', () => {
-    sinon.stub(element.restApiService, 'getSuggestedAccounts').callsFake( () =>
+    stubRestApi('getSuggestedAccounts').callsFake( () =>
       Promise.resolve([
         {
           name: 'fred',
@@ -82,7 +83,7 @@
   });
 
   test('Autocompletes groups', () => {
-    sinon.stub(element.restApiService, 'getSuggestedGroups').callsFake( () =>
+    stubRestApi('getSuggestedGroups').callsFake( () =>
       Promise.resolve({
         Polygerrit: 0,
         gerrit: 0,
@@ -95,7 +96,7 @@
   });
 
   test('Autocompletes projects', () => {
-    sinon.stub(element.restApiService, 'getSuggestedProjects').callsFake( () =>
+    stubRestApi('getSuggestedProjects').callsFake( () =>
       Promise.resolve({Polygerrit: 0}));
     return element._fetchProjects('project', 'pol').then(s => {
       assert.deepEqual(s[0], {text: 'project:Polygerrit'});
@@ -103,7 +104,7 @@
   });
 
   test('Autocomplete doesnt override exact matches to input', () => {
-    sinon.stub(element.restApiService, 'getSuggestedGroups').callsFake( () =>
+    stubRestApi('getSuggestedGroups').callsFake( () =>
       Promise.resolve({
         Polygerrit: 0,
         gerrit: 0,
@@ -118,7 +119,7 @@
   });
 
   test('Autocompletes accounts with no email', () => {
-    sinon.stub(element.restApiService, 'getSuggestedAccounts').callsFake( () =>
+    stubRestApi('getSuggestedAccounts').callsFake( () =>
       Promise.resolve([{name: 'fred'}]));
     return element._fetchAccounts('owner', 'fr').then(s => {
       assert.deepEqual(s[0], {text: 'owner:"fred"', label: 'fred'});
@@ -126,7 +127,7 @@
   });
 
   test('Autocompletes accounts with email', () => {
-    sinon.stub(element.restApiService, 'getSuggestedAccounts').callsFake( () =>
+    stubRestApi('getSuggestedAccounts').callsFake( () =>
       Promise.resolve([{email: 'fred@goog.co'}]));
     return element._fetchAccounts('owner', 'fr').then(s => {
       assert.deepEqual(s[0], {text: 'owner:fred@goog.co', label: ''});
diff --git a/polygerrit-ui/app/elements/custom-dark-theme_test.js b/polygerrit-ui/app/elements/custom-dark-theme_test.js
index ad12e14..d4b05e2 100644
--- a/polygerrit-ui/app/elements/custom-dark-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-dark-theme_test.js
@@ -17,7 +17,6 @@
 
 import '../test/common-test-setup-karma.js';
 import {getComputedStyleValue} from '../utils/dom-util.js';
-import './shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import './gr-app.js';
 import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
 import {removeTheme} from '../styles/themes/dark-theme.js';
diff --git a/polygerrit-ui/app/elements/custom-light-theme_test.js b/polygerrit-ui/app/elements/custom-light-theme_test.js
index 6d5b61e..35bc3a6 100644
--- a/polygerrit-ui/app/elements/custom-light-theme_test.js
+++ b/polygerrit-ui/app/elements/custom-light-theme_test.js
@@ -17,9 +17,9 @@
 
 import '../test/common-test-setup-karma.js';
 import {getComputedStyleValue} from '../utils/dom-util.js';
-import './shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import './gr-app.js';
 import {getPluginLoader} from './shared/gr-js-api-interface/gr-plugin-loader.js';
+import {stubRestApi} from '../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-app');
 
@@ -27,14 +27,11 @@
   let element;
   setup(done => {
     window.localStorage.removeItem('dark-theme');
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({test: 'config'}); },
-      getAccount() { return Promise.resolve({}); },
-      getDiffComments() { return Promise.resolve({}); },
-      getDiffRobotComments() { return Promise.resolve({}); },
-      getDiffDrafts() { return Promise.resolve({}); },
-      _fetchSharedCacheURL() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({test: 'config'}));
+    stubRestApi('getAccount').returns(Promise.resolve({}));
+    stubRestApi('getDiffComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
     getPluginLoader().loadPlugins([]);
     getPluginLoader().awaitPluginsLoaded()
diff --git a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
index 869b518..d78961c 100644
--- a/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-apply-fix-dialog/gr-apply-fix-dialog_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-apply-fix-dialog.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-apply-fix-dialog');
 
@@ -56,7 +57,7 @@
 
   suite('dialog open', () => {
     setup(() => {
-      sinon.stub(element.restApiService, 'getRobotCommentFixPreview')
+      stubRestApi('getRobotCommentFixPreview')
           .returns(Promise.resolve({
             f1: {
               meta_a: {},
@@ -147,8 +148,7 @@
   });
 
   test('next button state updated when suggestions changed', done => {
-    sinon.stub(element.restApiService, 'getRobotCommentFixPreview')
-        .returns(Promise.resolve({}));
+    stubRestApi('getRobotCommentFixPreview').returns(Promise.resolve({}));
     sinon.stub(element.$.applyFixOverlay, 'open').returns(Promise.resolve());
 
     element.open({detail: {patchNum: 2, comment: ROBOT_COMMENT_WITH_ONE_FIX}})
@@ -162,38 +162,24 @@
         });
   });
 
-  test('preview endpoint throws error should reset dialog', done => {
-    sinon.stub(window, 'fetch').callsFake((url => {
-      if (url.endsWith('/preview')) {
-        return Promise.reject(new Error('backend error'));
-      }
-      return Promise.resolve({
-        ok: true,
-        text() { return Promise.resolve(''); },
-        status: 200,
-      });
-    }));
-    const errorStub = sinon.stub();
-    document.addEventListener('network-error', errorStub);
+  test('preview endpoint throws error should reset dialog', async () => {
+    stubRestApi('getRobotCommentFixPreview').returns(
+        Promise.reject(new Error('backend error')));
     element.open({detail: {patchNum: 2,
       comment: ROBOT_COMMENT_WITH_TWO_FIXES}});
-    flush(() => {
-      assert.isTrue(errorStub.called);
-      assert.equal(element._currentFix, undefined);
-      done();
-    });
+    await flush();
+    assert.equal(element._currentFix, undefined);
   });
 
   test('apply fix button should call apply ' +
   'and navigate to change view', () => {
-    sinon.stub(element.restApiService, 'applyFixSuggestion')
-        .returns(Promise.resolve({ok: true}));
+    const stub = stubRestApi('applyFixSuggestion').returns(
+        Promise.resolve({ok: true}));
     sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
 
     return element._handleApplyFix().then(() => {
-      assert.isTrue(element.restApiService.applyFixSuggestion
-          .calledWithExactly('1', 2, '123'));
+      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
       assert.isTrue(GerritNav.navigateToChange.calledWithExactly({
         _number: '1',
         project: 'project',
@@ -211,14 +197,12 @@
   });
 
   test('should not navigate to change view if incorect reponse', done => {
-    sinon.stub(element.restApiService, 'applyFixSuggestion')
-        .returns(Promise.resolve({}));
+    const stub = stubRestApi('applyFixSuggestion').returns(Promise.resolve({}));
     sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
 
     element._handleApplyFix().then(() => {
-      assert.isTrue(element.restApiService.applyFixSuggestion
-          .calledWithExactly('1', 2, '123'));
+      assert.isTrue(stub.calledWithExactly('1', 2, '123'));
       assert.isTrue(GerritNav.navigateToChange.notCalled);
 
       assert.equal(element._isApplyFixLoading, false);
@@ -227,7 +211,7 @@
   });
 
   test('select fix forward and back of multiple suggested fixes', done => {
-    sinon.stub(element.restApiService, 'getRobotCommentFixPreview')
+    stubRestApi('getRobotCommentFixPreview')
         .returns(Promise.resolve({
           f1: {
             meta_a: {},
@@ -273,18 +257,8 @@
   });
 
   test('server-error should throw for failed apply call', async () => {
-    sinon.stub(window, 'fetch').callsFake((url => {
-      if (url.endsWith('/apply')) {
-        return Promise.reject(new Error('backend error'));
-      }
-      return Promise.resolve({
-        ok: true,
-        text() { return Promise.resolve(''); },
-        status: 200,
-      });
-    }));
-    const errorStub = sinon.stub();
-    document.addEventListener('network-error', errorStub);
+    stubRestApi('applyFixSuggestion').returns(
+        Promise.reject(new Error('backend error')));
     sinon.stub(GerritNav, 'navigateToChange');
     element._currentFix = {fix_id: '123'};
     let expectedError;
@@ -293,7 +267,6 @@
     });
     assert.isOk(expectedError);
     assert.isFalse(GerritNav.navigateToChange.called);
-    assert.isTrue(errorStub.called);
   });
 });
 
diff --git a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
index e107c16..94ec78f 100644
--- a/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-comment-api/gr-comment-api_test.js
@@ -21,6 +21,7 @@
 import {isInRevisionOfPatchRange, isInBaseOfPatchRange, isDraftThread, isUnresolved, createCommentThreads} from '../../../utils/comment-util.js';
 import {createDraft, createComment, createChangeComments, createCommentThread} from '../../../test/test-data-generators.js';
 import {CommentSide, Side} from '../../../constants/constants.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-comment-api');
 
@@ -36,25 +37,20 @@
   test('loads logged-out', () => {
     const changeNum = 1234;
 
-    sinon.stub(element.restApiService, 'getLoggedIn')
-        .returns(Promise.resolve(false));
-    sinon.stub(element.restApiService, 'getDiffComments')
-        .returns(Promise.resolve({
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    const diffCommentsSpy = stubRestApi('getDiffComments').returns(
+        Promise.resolve({
           'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
         }));
-    sinon.stub(element.restApiService, 'getDiffRobotComments')
-        .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
-    sinon.stub(element.restApiService, 'getDiffDrafts')
-        .returns(Promise.resolve({}));
+    const diffRobotCommentsSpy = stubRestApi('getDiffRobotComments').returns(
+        Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
+    const diffDraftsSpy = stubRestApi('getDiffDrafts').returns(
+        Promise.resolve({}));
 
     return element.loadAll(changeNum).then(() => {
-      assert.isTrue(element.restApiService.getDiffComments.calledWithExactly(
-          changeNum));
-      assert.isTrue(
-          element.restApiService.getDiffRobotComments.calledWithExactly(
-              changeNum));
-      assert.isTrue(element.restApiService.getDiffDrafts.calledWithExactly(
-          changeNum));
+      assert.isTrue(diffCommentsSpy.calledWithExactly(changeNum));
+      assert.isTrue(diffRobotCommentsSpy.calledWithExactly(changeNum));
+      assert.isTrue(diffDraftsSpy.calledWithExactly(changeNum));
       assert.isOk(element._changeComments._comments);
       assert.isOk(element._changeComments._robotComments);
       assert.deepEqual(element._changeComments._drafts, {});
@@ -64,25 +60,21 @@
   test('loads logged-in', () => {
     const changeNum = 1234;
 
-    sinon.stub(element.restApiService, 'getLoggedIn')
-        .returns(Promise.resolve(true));
-    sinon.stub(element.restApiService, 'getDiffComments')
-        .returns(Promise.resolve({
+    stubRestApi('getLoggedIn').returns(Promise.resolve(true));
+    const getCommentsStub = stubRestApi('getDiffComments').returns(
+        Promise.resolve({
           'foo.c': [{id: '123', message: 'foo bar', in_reply_to: '321'}],
-        }));
-    sinon.stub(element.restApiService, 'getDiffRobotComments')
+        })
+    );
+    const getRobotCommentsStub = stubRestApi('getDiffRobotComments')
         .returns(Promise.resolve({'foo.c': [{id: '321', message: 'done'}]}));
-    sinon.stub(element.restApiService, 'getDiffDrafts')
+    const getDraftsStub = stubRestApi('getDiffDrafts')
         .returns(Promise.resolve({'foo.c': [{id: '555', message: 'ack'}]}));
 
     return element.loadAll(changeNum).then(() => {
-      assert.isTrue(element.restApiService.getDiffComments.calledWithExactly(
-          changeNum));
-      assert.isTrue(
-          element.restApiService.getDiffRobotComments.calledWithExactly(
-              changeNum));
-      assert.isTrue(element.restApiService.getDiffDrafts.calledWithExactly(
-          changeNum));
+      assert.isTrue(getCommentsStub.calledWithExactly(changeNum));
+      assert.isTrue(getRobotCommentsStub.calledWithExactly(changeNum));
+      assert.isTrue(getDraftsStub.calledWithExactly(changeNum));
       assert.isOk(element._changeComments._comments);
       assert.isOk(element._changeComments._robotComments);
       assert.notDeepEqual(element._changeComments._drafts, {});
@@ -94,11 +86,11 @@
     let robotCommentStub;
     let draftStub;
     setup(() => {
-      commentStub = sinon.stub(element.restApiService, 'getDiffComments')
+      commentStub = stubRestApi('getDiffComments')
           .returns(Promise.resolve({}));
-      robotCommentStub = sinon.stub(element.restApiService,
+      robotCommentStub = stubRestApi(
           'getDiffRobotComments').returns(Promise.resolve({}));
-      draftStub = sinon.stub(element.restApiService, 'getDiffDrafts')
+      draftStub = stubRestApi('getDiffDrafts')
           .returns(Promise.resolve({}));
     });
 
@@ -138,11 +130,9 @@
   suite('_changeComment methods', () => {
     setup(done => {
       const changeNum = 1234;
-      stub('gr-rest-api-interface', {
-        getDiffComments() { return Promise.resolve({}); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-      });
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
       element.loadAll(changeNum).then(() => {
         done();
       });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
index b10b251..532922c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element_test.js
@@ -18,7 +18,6 @@
 import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff-group.js';
 import './gr-diff-builder.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff-builder-element.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
@@ -29,6 +28,7 @@
 import {GrDiffBuilder} from './gr-diff-builder.js';
 import {GrDiffBuilderSideBySide} from './gr-diff-builder-side-by-side.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
     <gr-diff-builder>
@@ -60,10 +60,8 @@
 
   setup(() => {
     element = basicFixture.instantiate();
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(false); },
-      getProjectConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('getProjectConfig').returns(Promise.resolve({}));
     stubBaseUrl('/r');
     prefs = {
       line_length: 10,
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 1e1d17e..3b366ce 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -18,11 +18,10 @@
 import '../../../test/common-test-setup-karma.js';
 import '../gr-diff/gr-diff.js';
 import './gr-diff-cursor.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {listenOnce} from '../../../test/test-utils.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
-import {appContext} from '../../../services/app-context.js';
+import {createDefaultDiffPrefs} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromTemplate(html`
   <gr-diff></gr-diff>
@@ -40,7 +39,6 @@
     const fixtureElems = basicFixture.instantiate();
     diffElement = fixtureElems[0];
     cursorElement = fixtureElems[1];
-    const restAPI = appContext.restApiService;
 
     // Register the diff with the cursor.
     cursorElement.push('diffs', diffElement);
@@ -61,10 +59,8 @@
     diffElement.addEventListener('render', setupDone);
 
     diff = getMockDiffResponse();
-    restAPI.getDiffPreferences().then(prefs => {
-      diffElement.prefs = prefs;
-      diffElement.diff = diff;
-    });
+    diffElement.prefs = createDefaultDiffPrefs();
+    diffElement.diff = diff;
   });
 
   test('diff cursor functionality (side-by-side)', () => {
@@ -572,20 +568,17 @@
 
     let diffElements;
 
-    setup(async () => {
+    setup(() => {
       const fixtureElems = multiDiffFixture.instantiate();
       diffElements = fixtureElems.slice(0, 3);
       cursorElement = fixtureElems[3];
-      const restAPI = appContext.restApiService;
 
       // Register the diff with the cursor.
       cursorElement.push('diffs', ...diffElements);
 
-      await restAPI.getDiffPreferences().then(prefs => {
-        for (const el of diffElements) {
-          el.prefs = prefs;
-        }
-      });
+      for (const el of diffElements) {
+        el.prefs = createDefaultDiffPrefs();
+      }
     });
 
     function getTargetDiffIndex() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
index d09c811..99e6391 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host_test.js
@@ -25,6 +25,8 @@
 import {createChange} from '../../../test/test-data-generators.js';
 import {FILE} from '../gr-diff/gr-diff-line.js';
 import {CoverageType} from '../../../types/types.js';
+import {stubRestApi} from '../../../test/test-utils.js';
+import {createDefaultDiffPrefs} from '../../../constants/constants.js';
 
 const basicFixture = fixtureFromElement('gr-diff-host');
 
@@ -35,9 +37,7 @@
 
   setup(() => {
     getLoggedIn = false;
-    stub('gr-rest-api-interface', {
-      async getLoggedIn() { return getLoggedIn; },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(getLoggedIn));
     element = basicFixture.instantiate();
     element.changeNum = 123;
     element.path = 'some/path';
@@ -130,18 +130,16 @@
     test('ends total and syntax timer after syntax layer', async () => {
       sinon.stub(element.reporting, 'diffViewContentDisplayed');
       let notifySyntaxProcessed;
-      sinon.stub(element.$.syntaxLayer, 'process').returns(new Promise(
-          resolve => {
+      sinon.stub(element.$.syntaxLayer, 'process').returns(
+          new Promise(resolve => {
             notifySyntaxProcessed = resolve;
-          }));
-      sinon.stub(element.restApiService, 'getDiff').returns(
-          Promise.resolve({content: []}));
+          })
+      );
+      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
       element.patchRange = {};
       element.change = createChange();
-      element.restApiService.getDiffPreferences().then(prefs => {
-        element.prefs = prefs;
-        return element.reload(true);
-      });
+      element.prefs = createDefaultDiffPrefs();
+      element.reload(true);
       // Multiple cascading microtasks are scheduled.
       await flush();
       notifySyntaxProcessed();
@@ -155,8 +153,7 @@
     });
 
     test('ends total timer w/ no syntax layer processing', async () => {
-      sinon.stub(element.restApiService, 'getDiff').returns(
-          Promise.resolve({content: []}));
+      stubRestApi('getDiff').returns(Promise.resolve({content: []}));
       element.patchRange = {};
       element.change = createChange();
       element.reload();
@@ -177,19 +174,15 @@
           resolve => {
             notifySyntaxProcessed = resolve;
           }));
-      sinon.stub(element.restApiService, 'getDiff').returns(
+      stubRestApi('getDiff').returns(
           Promise.resolve({content: []}));
       element.patchRange = {};
       element.change = createChange();
       let reloadComplete = false;
-      element.restApiService.getDiffPreferences()
-          .then(prefs => {
-            element.prefs = prefs;
-            return element.reload();
-          })
-          .then(() => {
-            reloadComplete = true;
-          });
+      element.prefs = createDefaultDiffPrefs();
+      element.reload().then(() => {
+        reloadComplete = true;
+      });
       // Multiple cascading microtasks are scheduled.
       await flush();
       assert.isFalse(reloadComplete);
@@ -231,7 +224,7 @@
     test('reload() loads files weblinks', () => {
       const weblinksStub = sinon.stub(GerritNav, '_generateWeblinks')
           .returns({name: 'stubb', url: '#s'});
-      sinon.stub(element.restApiService, 'getDiff').returns(Promise.resolve({
+      stubRestApi('getDiff').returns(Promise.resolve({
         content: [],
       }));
       element.projectName = 'test-project';
@@ -264,7 +257,7 @@
     });
 
     test('prefetch getDiff', done => {
-      const diffRestApiStub = sinon.stub(element.restApiService, 'getDiff')
+      const diffRestApiStub = stubRestApi('getDiff')
           .returns(Promise.resolve({content: []}));
       element.changeNum = 123;
       element.patchRange = {basePatchNum: 1, patchNum: 2};
@@ -277,9 +270,7 @@
     });
 
     test('_getDiff handles null diff responses', done => {
-      stub('gr-rest-api-interface', {
-        getDiff() { return Promise.resolve(null); },
-      });
+      stubRestApi('getDiff').returns(Promise.resolve(null));
       element.changeNum = 123;
       element.patchRange = {basePatchNum: 1, patchNum: 2};
       element.path = 'file.txt';
@@ -289,7 +280,7 @@
     test('reload resolves on error', () => {
       const onErrStub = sinon.stub(element, '_handleGetDiffError');
       const error = new Response(null, {ok: false, status: 500});
-      sinon.stub(element.restApiService, 'getDiff').callsFake(
+      stubRestApi('getDiff').callsFake(
           (changeNum, basePatchNum, patchNum, path, whitespace, onErr) => {
             onErr(error);
           });
@@ -352,12 +343,6 @@
           'wsAAAAAAAAAAAAA/////w==',
           type: 'image/bmp',
         };
-        sinon.stub(element.restApiService,
-            'getB64FileContents')
-            .callsFake(
-                (changeId, patchNum, path, opt_parentIndex) => Promise.resolve(
-                    opt_parentIndex === 1 ? mockFile1 : mockFile2)
-            );
 
         element.patchRange = {basePatchNum: 'PARENT', patchNum: 1};
         element.change = createChange();
@@ -385,8 +370,19 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sinon.stub(element.restApiService, 'getDiff')
-            .returns(Promise.resolve(mockDiff));
+        stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+        stubRestApi('getImagesForDiff').returns(Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+        }));
 
         const rendered = () => {
           // Recognizes that it should be an image diff.
@@ -442,11 +438,8 @@
         };
 
         element.addEventListener('render', rendered);
-
-        element.restApiService.getDiffPreferences().then(prefs => {
-          element.prefs = prefs;
-          element.reload();
-        });
+        element.prefs = createDefaultDiffPrefs();
+        element.reload();
       });
 
       test('renders image diffs with a different file name', done => {
@@ -466,8 +459,19 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sinon.stub(element.restApiService, 'getDiff')
-            .returns(Promise.resolve(mockDiff));
+        stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+        stubRestApi('getImagesForDiff').returns(Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot2.jpg',
+          },
+        }));
 
         const rendered = () => {
           // Recognizes that it should be an image diff.
@@ -525,11 +529,8 @@
         };
 
         element.addEventListener('render', rendered);
-
-        element.restApiService.getDiffPreferences().then(prefs => {
-          element.prefs = prefs;
-          element.reload();
-        });
+        element.prefs = createDefaultDiffPrefs();
+        element.reload();
       });
 
       test('renders added image', done => {
@@ -548,8 +549,15 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sinon.stub(element.restApiService, 'getDiff')
-            .returns(Promise.resolve(mockDiff));
+        stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+        stubRestApi('getImagesForDiff').returns(Promise.resolve({
+          baseImage: null,
+          revisionImage: {
+            ...mockFile2,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot2.jpg',
+          },
+        }));
 
         element.addEventListener('render', () => {
           // Recognizes that it should be an image diff.
@@ -567,10 +575,8 @@
           done();
         });
 
-        element.restApiService.getDiffPreferences().then(prefs => {
-          element.prefs = prefs;
-          element.reload();
-        });
+        element.prefs = createDefaultDiffPrefs();
+        element.reload();
       });
 
       test('renders removed image', done => {
@@ -589,8 +595,15 @@
           content: [{skip: 66}],
           binary: true,
         };
-        sinon.stub(element.restApiService, 'getDiff')
-            .returns(Promise.resolve(mockDiff));
+        stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+        stubRestApi('getImagesForDiff').returns(Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: null,
+        }));
 
         element.addEventListener('render', () => {
           // Recognizes that it should be an image diff.
@@ -608,10 +621,8 @@
           done();
         });
 
-        element.restApiService.getDiffPreferences().then(prefs => {
-          element.prefs = prefs;
-          element.reload();
-        });
+        element.prefs = createDefaultDiffPrefs();
+        element.reload();
       });
 
       test('does not render disallowed image type', done => {
@@ -632,8 +643,15 @@
         };
         mockFile1.type = 'image/jpeg-evil';
 
-        sinon.stub(element.restApiService, 'getDiff')
-            .returns(Promise.resolve(mockDiff));
+        stubRestApi('getDiff').returns(Promise.resolve(mockDiff));
+        stubRestApi('getImagesForDiff').returns(Promise.resolve({
+          baseImage: {
+            ...mockFile1,
+            _expectedType: 'image/jpeg',
+            _name: 'carrot.jpg',
+          },
+          revisionImage: null,
+        }));
 
         element.addEventListener('render', () => {
           // Recognizes that it should be an image diff.
@@ -646,10 +664,8 @@
           done();
         });
 
-        element.restApiService.getDiffPreferences().then(prefs => {
-          element.prefs = prefs;
-          element.reload();
-        });
+        element.prefs = createDefaultDiffPrefs();
+        element.reload();
       });
     });
   });
@@ -707,7 +723,7 @@
       const mockBlame = [{id: 'commit id', ranges: [{start: 1, end: 2}]}];
       const showAlertStub = sinon.stub();
       element.addEventListener('show-alert', showAlertStub);
-      const getBlameStub = sinon.stub(element.restApiService, 'getBlame')
+      const getBlameStub = stubRestApi('getBlame')
           .returns(Promise.resolve(mockBlame));
       element.changeNum = 42;
       element.patchRange = {patchNum: 5, basePatchNum: 4};
@@ -725,7 +741,7 @@
       const mockBlame = [];
       const showAlertStub = sinon.stub();
       element.addEventListener('show-alert', showAlertStub);
-      sinon.stub(element.restApiService, 'getBlame')
+      stubRestApi('getBlame')
           .returns(Promise.resolve(mockBlame));
       element.changeNum = 42;
       element.patchRange = {patchNum: 5, basePatchNum: 4};
@@ -1272,7 +1288,7 @@
     test('starts syntax layer processing on render event', async () => {
       sinon.stub(element.$.syntaxLayer, 'process')
           .returns(Promise.resolve());
-      sinon.stub(element.restApiService, 'getDiff').returns(
+      stubRestApi('getDiff').returns(
           Promise.resolve({content: []}));
       element.reload();
       await flush();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
index 049f01d..f554227 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-diff-mode-selector.js';
 import {DiffViewMode} from '../../../constants/constants.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-diff-mode-selector');
 
@@ -40,7 +41,7 @@
   });
 
   test('setMode', () => {
-    const saveStub = sinon.stub(element.restApiService, 'savePreferences');
+    const saveStub = stubRestApi('savePreferences');
 
     // Setting the mode initially does not save prefs.
     element.saveOnChange = true;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 72e1a8f..91d0187 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -29,6 +29,7 @@
   createRevisions,
   createComment,
 } from '../../../test/test-data-generators.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-diff-view');
 
@@ -88,43 +89,24 @@
       };
     }
 
+    let getDiffChangeDetailStub;
+    let getReviewedFilesStub;
     setup(async () => {
       clock = sinon.useFakeTimers();
-      stub('gr-rest-api-interface', {
-        getConfig() {
-          return Promise.resolve({change: {}});
-        },
-        getLoggedIn() {
-          return Promise.resolve(false);
-        },
-        getProjectConfig() {
-          return Promise.resolve({});
-        },
-        getDiffChangeDetail() {
-          return Promise.resolve({});
-        },
-        getChangeFiles() {
-          return Promise.resolve({});
-        },
-        saveFileReviewed() {
-          return Promise.resolve();
-        },
-        getDiffComments() {
-          return Promise.resolve({});
-        },
-        getDiffRobotComments() {
-          return Promise.resolve({});
-        },
-        getDiffDrafts() {
-          return Promise.resolve({});
-        },
-        getPortedComments() {
-          return Promise.resolve({});
-        },
-        getReviewedFiles() {
-          return Promise.resolve([]);
-        },
-      });
+      stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
+      getDiffChangeDetailStub = stubRestApi('getDiffChangeDetail').returns(
+          Promise.resolve({}));
+      stubRestApi('getChangeFiles').returns(Promise.resolve({}));
+      stubRestApi('saveFileReviewed').returns(Promise.resolve());
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+      stubRestApi('getPortedComments').returns(Promise.resolve({}));
+      getReviewedFilesStub = stubRestApi('getReviewedFiles').returns(
+          Promise.resolve([]));
+
       element = basicFixture.instantiate();
       element._changeNum = '42';
       element._path = 'some/path.txt';
@@ -345,13 +327,11 @@
       sinon.stub(element, '_loadBlame');
       sinon.stub(element.$.diffHost, 'reload').returns(Promise.resolve());
       sinon.spy(element, '_paramsChanged');
-      element.restApiService.getDiffChangeDetail.restore();
-      sinon.stub(element.restApiService, 'getDiffChangeDetail')
-          .returns(
-              Promise.resolve({
-                ...createChange(),
-                revisions: createRevisions(11),
-              }));
+      getDiffChangeDetailStub.returns(
+          Promise.resolve({
+            ...createChange(),
+            revisions: createRevisions(11),
+          }));
       element._patchRange = {
         patchNum: 2,
         basePatchNum: 1,
@@ -1218,7 +1198,7 @@
       const prefsPromise = new Promise(resolve => {
         resolvePrefs = resolve;
       });
-      sinon.stub(element.restApiService, 'getPreferences')
+      stubRestApi('getPreferences')
           .callsFake(() => prefsPromise);
 
       // Attach a new gr-diff-view so we can intercept the preferences fetch.
@@ -1607,10 +1587,7 @@
 
     test('_getReviewedStatus', () => {
       const promises = [];
-      element.restApiService.getReviewedFiles.restore();
-
-      sinon.stub(element.restApiService, 'getReviewedFiles')
-          .returns(Promise.resolve(['path']));
+      getReviewedFilesStub.returns(Promise.resolve(['path']));
 
       promises.push(element._getReviewedStatus(true, null, null, 'path')
           .then(reviewed => assert.isFalse(reviewed)));
@@ -1674,7 +1651,7 @@
 
     test('_paramsChanged sets in projectLookup', () => {
       sinon.stub(element, '_initLineOfInterestAndCursor');
-      const setStub = sinon.stub(element.restApiService, 'setInProjectLookup');
+      const setStub = stubRestApi('setInProjectLookup');
       element._paramsChanged({
         view: GerritNav.View.DIFF,
         changeNum: 101,
@@ -1851,18 +1828,17 @@
         'file1.txt': {},
         'a/b/test.c': {},
       };
-      stub('gr-rest-api-interface', {
-        getConfig() { return Promise.resolve({change: {}}); },
-        getLoggedIn() { return Promise.resolve(false); },
-        getProjectConfig() { return Promise.resolve({}); },
-        getDiffChangeDetail() { return Promise.resolve({}); },
-        getChangeFiles() { return Promise.resolve(changedFiles); },
-        saveFileReviewed() { return Promise.resolve(); },
-        getDiffComments() { return Promise.resolve({}); },
-        getDiffRobotComments() { return Promise.resolve({}); },
-        getDiffDrafts() { return Promise.resolve({}); },
-        getReviewedFiles() { return Promise.resolve([]); },
-      });
+      stubRestApi('getConfig').returns(Promise.resolve({change: {}}));
+      stubRestApi('getLoggedIn').returns(Promise.resolve(true));
+      stubRestApi('getProjectConfig').returns(Promise.resolve({}));
+      stubRestApi('getDiffChangeDetail').returns(Promise.resolve({}));
+      stubRestApi('getChangeFiles').returns(Promise.resolve(changedFiles));
+      stubRestApi('saveFileReviewed').returns(Promise.resolve());
+      stubRestApi('getDiffComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+      stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
+      stubRestApi('getReviewedFiles').returns(
+          Promise.resolve([]));
       element = basicFixture.instantiate();
       element._changeNum = '42';
       return element._loadComments();
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
index c5a8db4..fbe996d 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.js
@@ -16,7 +16,6 @@
  */
 
 import '../../../test/common-test-setup-karma.js';
-import '../../shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 import './gr-diff.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
@@ -26,6 +25,7 @@
 import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 import '@polymer/paper-button/paper-button.js';
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-diff');
 
@@ -176,9 +176,7 @@
   suite('not logged in', () => {
     setup(() => {
       const getLoggedInPromise = Promise.resolve(false);
-      stub('gr-rest-api-interface', {
-        getLoggedIn() { return getLoggedInPromise; },
-      });
+      stubRestApi('getLoggedIn').returns(getLoggedInPromise);
       element = basicFixture.instantiate();
       return getLoggedInPromise;
     });
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
index 15841d9..8b40db3 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select_test.js
@@ -26,6 +26,7 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
 import {ChangeComments} from '../gr-comment-api/gr-comment-api.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const commentApiMockElement = createCommentApiMockWithTemplateElement(
     'gr-patch-range-select-comment-api-mock', html`
@@ -50,11 +51,9 @@
   }
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getDiffComments() { return Promise.resolve({}); },
-      getDiffRobotComments() { return Promise.resolve({}); },
-      getDiffDrafts() { return Promise.resolve({}); },
-    });
+    stubRestApi('getDiffComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffRobotComments').returns(Promise.resolve({}));
+    stubRestApi('getDiffDrafts').returns(Promise.resolve({}));
 
     // Element must be wrapped in an element with direct access to the
     // comment API.
diff --git a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js
index 79d40b5..f5e47ca 100644
--- a/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js
+++ b/polygerrit-ui/app/elements/documentation/gr-documentation-search/gr-documentation-search_test.js
@@ -19,6 +19,7 @@
 import './gr-documentation-search.js';
 import {page} from '../../../utils/page-wrapper-utils.js';
 import 'lodash/lodash.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-documentation-search');
 
@@ -45,11 +46,8 @@
   suite('list with searches for documentation', () => {
     setup(done => {
       documentationSearches = _.times(26, documentationGenerator);
-      stub('gr-rest-api-interface', {
-        getDocumentationSearches() {
-          return Promise.resolve(documentationSearches);
-        },
-      });
+      stubRestApi('getDocumentationSearches').returns(
+          Promise.resolve(documentationSearches));
       element._paramsChanged(value).then(() => { flush(done); });
     });
 
@@ -70,19 +68,12 @@
       _.times(1, documentationSearches);
     });
 
-    test('_paramsChanged', done => {
-      sinon.stub(
-          element.restApiService,
-          'getDocumentationSearches')
-          .callsFake(() => Promise.resolve(documentationSearches));
-      const value = {
-        filter: 'test',
-      };
-      element._paramsChanged(value).then(() => {
-        assert.isTrue(element.restApiService.getDocumentationSearches.lastCall
-            .calledWithExactly('test'));
-        done();
-      });
+    test('_paramsChanged', async () => {
+      const stub = stubRestApi('getDocumentationSearches').returns(
+          Promise.resolve(documentationSearches));
+      const value = {filter: 'test'};
+      await element._paramsChanged(value);
+      assert.isTrue(stub.lastCall.calledWithExactly('test'));
     });
   });
 
diff --git a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
index e366233..be6ffc4 100644
--- a/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-edit-controls/gr-edit-controls_test.js
@@ -19,6 +19,7 @@
 import './gr-edit-controls.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-edit-controls');
 
@@ -35,8 +36,7 @@
     showDialogSpy = sinon.spy(element, '_showDialog');
     closeDialogSpy = sinon.spy(element, '_closeDialog');
     sinon.stub(element, '_hideAllDialogs');
-    queryStub = sinon.stub(element.restApiService, 'queryChangeFiles')
-        .returns(Promise.resolve([]));
+    queryStub = stubRestApi('queryChangeFiles').returns(Promise.resolve([]));
     flush();
   });
 
@@ -114,7 +114,7 @@
 
     setup(() => {
       navStub = sinon.stub(GerritNav, 'navigateToChange');
-      deleteStub = sinon.stub(element.restApiService, 'deleteFileInChangeEdit');
+      deleteStub = stubRestApi('deleteFileInChangeEdit');
       deleteAutocomplete =
           element.$.deleteDialog.querySelector('gr-autocomplete');
     });
@@ -198,7 +198,7 @@
 
     setup(() => {
       navStub = sinon.stub(GerritNav, 'navigateToChange');
-      renameStub = sinon.stub(element.restApiService, 'renameFileInChangeEdit');
+      renameStub = stubRestApi('renameFileInChangeEdit');
       renameAutocomplete =
           element.$.renameDialog.querySelector('gr-autocomplete');
     });
@@ -291,7 +291,7 @@
 
     setup(() => {
       navStub = sinon.stub(GerritNav, 'navigateToChange');
-      restoreStub = sinon.stub(element.restApiService,
+      restoreStub = stubRestApi(
           'restoreFileInChangeEdit');
     });
 
@@ -356,7 +356,7 @@
 
     setup(() => {
       navStub = sinon.stub(GerritNav, 'navigateToChange');
-      fileStub = sinon.stub(element.restApiService, 'saveFileUploadChangeEdit');
+      fileStub = stubRestApi('saveFileUploadChangeEdit');
     });
 
     test('_handleUploadConfirm', () => {
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
index 419e63e..03e0b4c 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.js
@@ -20,6 +20,7 @@
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 import {SPECIAL_PATCH_SET_NUM} from '../../../utils/patch-set-util.js';
 import {HttpMethod} from '../../../constants/constants.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-editor-view');
 
@@ -37,15 +38,10 @@
   };
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-      getEditPreferences() { return Promise.resolve({}); },
-    });
-
     element = basicFixture.instantiate();
-    savePathStub = sinon.stub(element.restApiService, 'renameFileInChangeEdit');
-    saveFileStub = sinon.stub(element.restApiService, 'saveChangeEdit');
-    changeDetailStub = sinon.stub(element.restApiService,
+    savePathStub = stubRestApi('renameFileInChangeEdit');
+    saveFileStub = stubRestApi('saveChangeEdit');
+    changeDetailStub = stubRestApi(
         'getDiffChangeDetail');
     navigateStub = sinon.stub(element, '_viewEditInChangeView');
   });
@@ -200,7 +196,7 @@
       const saveSpy = sinon.spy(element, '_saveEdit');
       const alertStub = sinon.stub(element, '_showAlert');
       const changeActionsStub =
-        sinon.stub(element.restApiService, 'executeChangeAction');
+        stubRestApi('executeChangeAction');
       saveFileStub.returns(Promise.resolve({ok: true}));
       element._newContent = newText;
       flush();
@@ -256,7 +252,7 @@
     });
 
     test('res.ok', () => {
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
@@ -272,7 +268,7 @@
     });
 
     test('!res.ok', () => {
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({}));
 
       // Ensure no data is set with a bad response.
@@ -284,7 +280,7 @@
     });
 
     test('content is undefined', () => {
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
@@ -298,7 +294,7 @@
     });
 
     test('content and type is undefined', () => {
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({
             ok: true,
           }));
@@ -383,7 +379,7 @@
     test('local edit exists', () => {
       sinon.stub(element.$.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
@@ -406,7 +402,7 @@
     test('local edit exists, is same as remote edit', () => {
       sinon.stub(element.$.storage, 'getEditableContentItem')
           .returns({message: 'pending edit'});
-      sinon.stub(element.restApiService, 'getFileContent')
+      stubRestApi('getFileContent')
           .returns(Promise.resolve({
             ok: true,
             type: 'text/javascript',
diff --git a/polygerrit-ui/app/elements/gr-app-global-var-init.ts b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
index 7a27b03..8a632bb 100644
--- a/polygerrit-ui/app/elements/gr-app-global-var-init.ts
+++ b/polygerrit-ui/app/elements/gr-app-global-var-init.ts
@@ -36,7 +36,6 @@
 import {GrPluginActionContext} from './shared/gr-js-api-interface/gr-plugin-action-context';
 import {
   getPluginNameFromUrl,
-  getRestAPI,
   PLUGIN_LOADING_TIMEOUT_MS,
   PRELOADED_PROTOCOL,
   send,
@@ -62,7 +61,6 @@
   window._apiUtils = {
     getPluginNameFromUrl,
     send,
-    getRestAPI,
     getBaseUrl,
     PRELOADED_PROTOCOL,
     PLUGIN_LOADING_TIMEOUT_MS,
diff --git a/polygerrit-ui/app/elements/gr-app_test.js b/polygerrit-ui/app/elements/gr-app_test.js
index 013d9387..1a04411 100644
--- a/polygerrit-ui/app/elements/gr-app_test.js
+++ b/polygerrit-ui/app/elements/gr-app_test.js
@@ -20,11 +20,13 @@
 import {appContext} from '../services/app-context.js';
 import {GerritNav} from './core/gr-navigation/gr-navigation.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`<gr-app id="app"></gr-app>`);
 
 suite('gr-app tests', () => {
   let element;
+  let configStub;
 
   setup(done => {
     sinon.stub(appContext.reportingService, 'appStarted');
@@ -34,23 +36,17 @@
     stub('gr-router', {
       start: sinon.stub(),
     });
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve({}); },
-      getAccountCapabilities() { return Promise.resolve({}); },
-      getConfig() {
-        return Promise.resolve({
-          plugin: {},
-          auth: {
-            auth_type: undefined,
-          },
-        });
+    stubRestApi('getAccount').returns(Promise.resolve({}));
+    stubRestApi('getAccountCapabilities').returns(Promise.resolve({}));
+    configStub = stubRestApi('getConfig').returns(Promise.resolve({
+      plugin: {},
+      auth: {
+        auth_type: undefined,
       },
-      getPreferences() { return Promise.resolve({my: []}); },
-      getDiffPreferences() { return Promise.resolve({}); },
-      getEditPreferences() { return Promise.resolve({}); },
-      getVersion() { return Promise.resolve(42); },
-      probePath() { return Promise.resolve(42); },
-    });
+    }));
+    stubRestApi('getPreferences').returns(Promise.resolve({my: []}));
+    stubRestApi('getVersion').returns(Promise.resolve(42));
+    stubRestApi('probePath').returns(Promise.resolve(42));
 
     element = basicFixture.instantiate();
     flush(done);
@@ -69,12 +65,11 @@
     sinon.assert.callOrder(appStartedStub, routerStartStub);
   });
 
-  test('passes config to gr-plugin-host', () => {
-    const config = appElement().restApiService.getConfig;
-    return config.lastCall.returnValue.then(config => {
+  test('passes config to gr-plugin-host', () =>
+    configStub.lastCall.returnValue.then(config => {
       assert.deepEqual(appElement().$.plugins.config, config);
-    });
-  });
+    })
+  );
 
   test('_paramsChanged sets search page', () => {
     appElement()._paramsChanged({base: {view: GerritNav.View.CHANGE}});
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
index a92c023..c7cb44e 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-account-info.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-account-info');
 
@@ -46,13 +47,11 @@
     };
     config = {auth: {editable_account_fields: []}};
 
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve(account); },
-      getConfig() { return Promise.resolve(config); },
-      getPreferences() {
-        return Promise.resolve({time_format: 'HHMM_12'});
-      },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve(account));
+    stubRestApi('getConfig').returns(Promise.resolve(config));
+    stubRestApi('getPreferences').returns(
+        Promise.resolve({time_format: 'HHMM_12'}));
+
     element = basicFixture.instantiate();
     // Allow the element to render.
     element.loadData().then(() => { flush(done); });
@@ -133,11 +132,11 @@
       element.set('_serverConfig',
           {auth: {editable_account_fields: ['FULL_NAME', 'USER_NAME']}});
 
-      nameStub = sinon.stub(element.restApiService, 'setAccountName').callsFake(
+      nameStub = stubRestApi('setAccountName').callsFake(
           name => Promise.resolve());
-      usernameStub = sinon.stub(element.restApiService, 'setAccountUsername')
+      usernameStub = stubRestApi('setAccountUsername')
           .callsFake(username => Promise.resolve());
-      statusStub = sinon.stub(element.restApiService,
+      statusStub = stubRestApi(
           'setAccountStatus').callsFake(
           status => Promise.resolve());
     });
@@ -218,12 +217,12 @@
       element.set('_serverConfig',
           {auth: {editable_account_fields: ['FULL_NAME']}});
 
-      nameStub = sinon.stub(element.restApiService, 'setAccountName').callsFake(
+      nameStub = stubRestApi('setAccountName').callsFake(
           name => Promise.resolve());
-      statusStub = sinon.stub(element.restApiService,
+      statusStub = stubRestApi(
           'setAccountStatus').callsFake(
           status => Promise.resolve());
-      sinon.stub(element.restApiService, 'setAccountUsername').callsFake(
+      stubRestApi('setAccountUsername').callsFake(
           username => Promise.resolve());
     });
 
@@ -263,7 +262,7 @@
       element.set('_serverConfig',
           {auth: {editable_account_fields: []}});
 
-      statusStub = sinon.stub(element.restApiService,
+      statusStub = stubRestApi(
           'setAccountStatus').callsFake(
           status => Promise.resolve());
     });
diff --git a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js
index 0c785aa..c8ce574 100644
--- a/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-agreements-list/gr-agreements-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-agreements-list.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-agreements-list');
 
@@ -31,9 +32,7 @@
       name: 'Agreements 1',
     }];
 
-    stub('gr-rest-api-interface', {
-      getAccountAgreements() { return Promise.resolve(agreements); },
-    });
+    stubRestApi('getAccountAgreements').returns(Promise.resolve(agreements));
 
     element = basicFixture.instantiate();
 
diff --git a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
index aeacea4..7c11136 100644
--- a/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-cla-view/gr-cla-view_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-cla-view.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-cla-view');
 
@@ -105,11 +106,10 @@
   ];
 
   setup(done => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve(config); },
-      getAccountGroups() { return Promise.resolve(groups); },
-      getAccountAgreements() { return Promise.resolve(signedAgreements); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve(config));
+    stubRestApi('getAccountGroups').returns(Promise.resolve(groups));
+    stubRestApi('getAccountAgreements').returns(
+        Promise.resolve(signedAgreements));
     element = basicFixture.instantiate();
     element.loadData().then(() => { flush(done); });
   });
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
index 01c6861..cffd1ae 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-edit-preferences.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-edit-preferences');
 
@@ -56,11 +57,7 @@
       theme: 'DEFAULT',
     };
 
-    stub('gr-rest-api-interface', {
-      getEditPreferences() {
-        return Promise.resolve(editPreferences);
-      },
-    });
+    stubRestApi('getEditPreferences').returns(Promise.resolve(editPreferences));
 
     element = basicFixture.instantiate();
 
@@ -92,7 +89,7 @@
   });
 
   test('save changes', () => {
-    sinon.stub(element.restApiService, 'saveEditPreferences')
+    stubRestApi('saveEditPreferences')
         .returns(Promise.resolve());
     const showTabsCheckbox = valueOf('Show tabs', 'editPreferences')
         .firstElementChild;
diff --git a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
index 143d49c..bc4a4d2 100644
--- a/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
+++ b/polygerrit-ui/app/elements/settings/gr-email-editor/gr-email-editor_test.ts
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-email-editor.js';
 import {GrEmailEditor} from './gr-email-editor';
+import {spyRestApi, stubRestApi} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-email-editor');
 
@@ -31,11 +32,7 @@
       {email: 'email@three.com'},
     ];
 
-    stub('gr-rest-api-interface', {
-      getAccountEmails() {
-        return Promise.resolve(emails);
-      },
-    });
+    stubRestApi('getAccountEmails').returns(Promise.resolve(emails));
 
     element = basicFixture.instantiate();
 
@@ -113,15 +110,9 @@
     assert.equal(element._emailsToRemove[0].email, 'email@three.com');
   });
 
-  test('save changes', done => {
-    const deleteEmailStub = sinon.stub(
-      element.restApiService,
-      'deleteAccountEmail'
-    );
-    const setPreferredStub = sinon.stub(
-      element.restApiService,
-      'setPreferredAccountEmail'
-    );
+  test('save changes', async () => {
+    const deleteEmailSpy = spyRestApi('deleteAccountEmail');
+    const setPreferredSpy = spyRestApi('setPreferredAccountEmail');
 
     const rows = element
       .shadowRoot!.querySelector('table')!
@@ -142,15 +133,10 @@
     assert.equal(element._emailsToRemove[0].email, 'email@one.com');
     assert.equal(element._emails.length, 2);
 
-    // Save the changes.
-    element.save().then(() => {
-      assert.equal(deleteEmailStub.callCount, 1);
-      assert.equal(deleteEmailStub.getCall(0).args[0], 'email@one.com');
-
-      assert.isTrue(setPreferredStub.called);
-      assert.equal(setPreferredStub.getCall(0).args[0], 'email@three.com');
-
-      done();
-    });
+    await element.save();
+    assert.equal(deleteEmailSpy.callCount, 1);
+    assert.equal(deleteEmailSpy.getCall(0).args[0], 'email@one.com');
+    assert.isTrue(setPreferredSpy.called);
+    assert.equal(setPreferredSpy.getCall(0).args[0], 'email@three.com');
   });
 });
diff --git a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
index 1a84aeb4..8820748 100644
--- a/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-gpg-editor/gr-gpg-editor_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-gpg-editor.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-gpg-editor');
 
@@ -50,9 +51,7 @@
       },
     };
 
-    stub('gr-rest-api-interface', {
-      getAccountGPGKeys() { return Promise.resolve(keys); },
-    });
+    stubRestApi('getAccountGPGKeys').returns(Promise.resolve(keys));
 
     element = basicFixture.instantiate();
 
@@ -74,7 +73,7 @@
   test('remove key', done => {
     const lastKey = keys[Object.keys(keys)[1]];
 
-    const saveStub = sinon.stub(element.restApiService, 'deleteAccountGPGKey')
+    const saveStub = stubRestApi('deleteAccountGPGKey')
         .callsFake(() => Promise.resolve());
 
     assert.equal(element._keysToRemove.length, 0);
@@ -130,7 +129,7 @@
       },
     };
 
-    const addStub = sinon.stub(element.restApiService,
+    const addStub = stubRestApi(
         'addAccountGPGKey').callsFake(
         () => Promise.resolve(newKeyObject));
 
@@ -156,7 +155,7 @@
   test('add invalid key', done => {
     const newKeyString = 'not even close to valid';
 
-    const addStub = sinon.stub(element.restApiService,
+    const addStub = stubRestApi(
         'addAccountGPGKey').callsFake(
         () => Promise.reject(new Error('error')));
 
diff --git a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js
index e19345a..e048103 100644
--- a/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-group-list/gr-group-list_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-group-list.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-group-list');
 
@@ -45,9 +46,7 @@
       name: 'Group 3',
     }];
 
-    stub('gr-rest-api-interface', {
-      getAccountGroups() { return Promise.resolve(groups); },
-    });
+    stubRestApi('getAccountGroups').returns(Promise.resolve(groups));
 
     element = basicFixture.instantiate();
 
diff --git a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js
index 6f47952..e403b4f 100644
--- a/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-http-password/gr-http-password_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-http-password.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-http-password');
 
@@ -29,10 +30,8 @@
     account = {username: 'user name'};
     config = {auth: {}};
 
-    stub('gr-rest-api-interface', {
-      getAccount() { return Promise.resolve(account); },
-      getConfig() { return Promise.resolve(config); },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve(account));
+    stubRestApi('getConfig').returns(Promise.resolve(config));
 
     element = basicFixture.instantiate();
     element.loadData().then(() => { flush(done); });
@@ -42,7 +41,7 @@
     const button = element.$.generateButton;
     const nextPassword = 'the new password';
     let generateResolve;
-    const generateStub = sinon.stub(element.restApiService,
+    const generateStub = stubRestApi(
         'generateAccountHttpPassword')
         .callsFake(() => new Promise(resolve => {
           generateResolve = resolve;
diff --git a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
index 8af5bd0..867473f 100644
--- a/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-identities/gr-identities_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-identities.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-identities');
 
@@ -39,14 +40,12 @@
     },
   ];
 
-  setup(done => {
-    stub('gr-rest-api-interface', {
-      getExternalIds() { return Promise.resolve(ids); },
-    });
+  setup(async () => {
+    stubRestApi('getExternalIds').returns(Promise.resolve(ids));
 
     element = basicFixture.instantiate();
-
-    element.loadData().then(() => { flush(done); });
+    await element.loadData();
+    await flush();
   });
 
   test('renders', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
index 468ef57..ce9c106 100644
--- a/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-registration-dialog/gr-registration-dialog_test.js
@@ -16,6 +16,7 @@
  */
 import '../../../test/common-test-setup-karma.js';
 import './gr-registration-dialog.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-registration-dialog');
 
@@ -38,27 +39,21 @@
       ],
     };
 
-    stub('gr-rest-api-interface', {
-      getAccount() {
-        return Promise.resolve(account);
-      },
-      setAccountName(name) {
-        account.name = name;
-        return Promise.resolve();
-      },
-      setAccountUsername(username) {
-        account.username = username;
-        return Promise.resolve();
-      },
-      setPreferredAccountEmail(email) {
-        account.email = email;
-        return Promise.resolve();
-      },
-      getConfig() {
-        return Promise.resolve(
-            {auth: {editable_account_fields: ['USER_NAME']}});
-      },
+    stubRestApi('getAccount').returns(Promise.resolve(account));
+    stubRestApi('setAccountName').callsFake(name => {
+      account.name = name;
+      return Promise.resolve();
     });
+    stubRestApi('setAccountUsername').callsFake(username => {
+      account.username = username;
+      return Promise.resolve();
+    });
+    stubRestApi('setPreferredAccountEmail').callsFake(email => {
+      account.email = email;
+      return Promise.resolve();
+    });
+    stubRestApi('getConfig').returns(
+        Promise.resolve({auth: {editable_account_fields: ['USER_NAME']}}));
 
     element = basicFixture.instantiate();
 
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
index 4c5a2a2..98abb3fb 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view_test.js
@@ -20,6 +20,7 @@
 import './gr-settings-view.js';
 import {flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 import {GerritView} from '../../../services/router/router-model.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-settings-view');
 const blankFixture = fixtureFromElement('div');
@@ -51,7 +52,7 @@
   }
 
   function stubAddAccountEmail(statusCode) {
-    return sinon.stub(element.restApiService, 'addAccountEmail').callsFake(
+    return stubRestApi('addAccountEmail').callsFake(
         () => Promise.resolve({status: statusCode}));
   }
 
@@ -82,17 +83,10 @@
     };
     config = {auth: {editable_account_fields: []}};
 
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-      getAccount() { return Promise.resolve(account); },
-      getPreferences() { return Promise.resolve(preferences); },
-      getWatchedProjects() {
-        return Promise.resolve([]);
-      },
-      getAccountEmails() { return Promise.resolve(); },
-      getConfig() { return Promise.resolve(config); },
-      getAccountGroups() { return Promise.resolve([]); },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve(account));
+    stubRestApi('getPreferences').returns(Promise.resolve(preferences));
+    stubRestApi('getAccountEmails').returns(Promise.resolve());
+    stubRestApi('getConfig').returns(Promise.resolve(config));
     element = basicFixture.instantiate();
 
     // Allow the element to render.
@@ -179,13 +173,11 @@
     assert.isTrue(element._prefsChanged);
     assert.isFalse(element._menuChanged);
 
-    stub('gr-rest-api-interface', {
-      savePreferences(prefs) {
-        assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
-        assertMenusEqual(prefs.my, preferences.my);
-        assert.equal(prefs.publish_comments_on_push, true);
-        return Promise.resolve();
-      },
+    stubRestApi('savePreferences').callsFake(prefs => {
+      assert.equal(prefs.diff_view, 'SIDE_BY_SIDE');
+      assertMenusEqual(prefs.my, preferences.my);
+      assert.equal(prefs.publish_comments_on_push, true);
+      return Promise.resolve();
     });
 
     // Save the change.
@@ -204,11 +196,9 @@
     assert.isFalse(element._menuChanged);
     assert.isTrue(element._prefsChanged);
 
-    stub('gr-rest-api-interface', {
-      savePreferences(prefs) {
-        assert.equal(prefs.publish_comments_on_push, true);
-        return Promise.resolve();
-      },
+    stubRestApi('savePreferences').callsFake(prefs => {
+      assert.equal(prefs.publish_comments_on_push, true);
+      return Promise.resolve();
     });
 
     // Save the change.
@@ -228,11 +218,9 @@
     assert.isFalse(element._menuChanged);
     assert.isTrue(element._prefsChanged);
 
-    stub('gr-rest-api-interface', {
-      savePreferences(prefs) {
-        assert.equal(prefs.work_in_progress_by_default, true);
-        return Promise.resolve();
-      },
+    stubRestApi('savePreferences').callsFake(prefs => {
+      assert.equal(prefs.work_in_progress_by_default, true);
+      return Promise.resolve();
     });
 
     // Save the change.
@@ -263,11 +251,9 @@
     assert.isTrue(element._menuChanged);
     assert.isFalse(element._prefsChanged);
 
-    stub('gr-rest-api-interface', {
-      savePreferences(prefs) {
-        assertMenusEqual(prefs.my, element._localMenu);
-        return Promise.resolve();
-      },
+    stubRestApi('savePreferences').callsFake(prefs => {
+      assertMenusEqual(prefs.my, element._localMenu);
+      return Promise.resolve();
     });
 
     element._handleSaveMenu().then(() => {
@@ -372,9 +358,7 @@
       ],
     };
 
-    stub('gr-rest-api-interface', {
-      getDefaultPreferences() { return Promise.resolve(originalMenu); },
-    });
+    stubRestApi('getDefaultPreferences').returns(Promise.resolve(originalMenu));
 
     const updatedMenu = [
       {url: '/first/url', name: 'first name', target: '_blank'},
@@ -474,22 +458,19 @@
 
   suite('when email verification token is provided', () => {
     let resolveConfirm;
+    let confirmEmailStub;
 
     setup(() => {
       sinon.stub(element.$.emailEditor, 'loadData');
-      sinon.stub(
-          element.restApiService,
-          'confirmEmail')
-          .callsFake(
-              () => new Promise(
-                  resolve => { resolveConfirm = resolve; }));
+      confirmEmailStub = stubRestApi('confirmEmail').returns(
+          new Promise(resolve => { resolveConfirm = resolve; }));
       element.params = {view: GerritView.SETTINGS, emailToken: 'foo'};
       element.attached();
     });
 
     test('it is used to confirm email via rest API', () => {
-      assert.isTrue(element.restApiService.confirmEmail.calledOnce);
-      assert.isTrue(element.restApiService.confirmEmail.calledWith('foo'));
+      assert.isTrue(confirmEmailStub.calledOnce);
+      assert.isTrue(confirmEmailStub.calledWith('foo'));
     });
 
     test('emails are not loaded initially', () => {
diff --git a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
index 73e707f..8f99baa 100644
--- a/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-ssh-editor/gr-ssh-editor_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-ssh-editor.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-ssh-editor');
 
@@ -41,9 +42,7 @@
       valid: true,
     }];
 
-    stub('gr-rest-api-interface', {
-      getAccountSSHKeys() { return Promise.resolve(keys); },
-    });
+    stubRestApi('getAccountSSHKeys').returns(Promise.resolve(keys));
 
     element = basicFixture.instantiate();
 
@@ -65,7 +64,7 @@
   test('remove key', done => {
     const lastKey = keys[1];
 
-    const saveStub = sinon.stub(element.restApiService, 'deleteAccountSSHKey')
+    const saveStub = stubRestApi('deleteAccountSSHKey')
         .callsFake(() => Promise.resolve());
 
     assert.equal(element._keysToRemove.length, 0);
@@ -116,7 +115,7 @@
       valid: true,
     };
 
-    const addStub = sinon.stub(element.restApiService,
+    const addStub = stubRestApi(
         'addAccountSSHKey').callsFake(
         () => Promise.resolve(newKeyObject));
 
@@ -142,7 +141,7 @@
   test('add invalid key', done => {
     const newKeyString = 'not even close to valid';
 
-    const addStub = sinon.stub(element.restApiService,
+    const addStub = stubRestApi(
         'addAccountSSHKey').callsFake(
         () => Promise.reject(new Error('error')));
 
diff --git a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
index 2fd8900..aac8995 100644
--- a/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
+++ b/polygerrit-ui/app/elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-watched-projects-editor.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-watched-projects-editor');
 
@@ -44,21 +45,17 @@
       },
     ];
 
-    stub('gr-rest-api-interface', {
-      getSuggestedProjects(input) {
-        if (input.startsWith('th')) {
-          return Promise.resolve({'the project': {
-            id: 'the project',
-            state: 'ACTIVE',
-            web_links: [],
-          }});
-        } else {
-          return Promise.resolve({});
-        }
-      },
-      getWatchedProjects() {
-        return Promise.resolve(projects);
-      },
+    stubRestApi('getWatchedProjects').returns(Promise.resolve(projects));
+    stubRestApi('getSuggestedProjects').callsFake(input => {
+      if (input.startsWith('th')) {
+        return Promise.resolve({'the project': {
+          id: 'the project',
+          state: 'ACTIVE',
+          web_links: [],
+        }});
+      } else {
+        return Promise.resolve({});
+      }
     });
 
     element = basicFixture.instantiate();
diff --git a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js
index 6912776..e25bdea 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-label/gr-account-label_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-account-label.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-account-label');
 
@@ -28,10 +29,8 @@
   }
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getLoggedIn() { return Promise.resolve(false); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
     element = basicFixture.instantiate();
     element._config = {
       user: {
@@ -104,7 +103,7 @@
     });
 
     test('tap attention button', () => {
-      const apiStub = sinon.stub(element.restApiService,
+      const apiStub = stubRestApi(
           'removeFromAttentionSet')
           .callsFake(() => Promise.resolve());
       const button = element.shadowRoot.querySelector('#attentionButton');
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js
index 554953e..af485c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-account-link.js';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-account-link');
 
@@ -25,9 +26,7 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
index 3acba5b..6430290 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-list/gr-account-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-account-list.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-account-list');
 
@@ -60,9 +61,7 @@
     existingAccount1 = makeAccount();
     existingAccount2 = makeAccount();
 
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
     element.accounts = [existingAccount1, existingAccount2];
     suggestionsProvider = new MockSuggestionsProvider();
diff --git a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
index baec672..76f058a 100644
--- a/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-avatar/gr-avatar_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-avatar.js';
 import {getPluginLoader} from '../gr-js-api-interface/gr-plugin-loader.js';
+import {appContext} from '../../../services/app-context.js';
 
 const basicFixture = fixtureFromElement('gr-avatar');
 
@@ -104,7 +105,7 @@
       getPluginLoader().loadPlugins([]);
 
       return Promise.all([
-        element.restApiService.getConfig(),
+        appContext.restApiService.getConfig(),
         getPluginLoader().awaitPluginsLoaded(),
       ]).then(() => {
         assert.isFalse(element.hasAttribute('hidden'));
@@ -134,7 +135,7 @@
       getPluginLoader().loadPlugins([]);
 
       return Promise.all([
-        element.restApiService.getConfig(),
+        appContext.restApiService.getConfig(),
         getPluginLoader().awaitPluginsLoaded(),
       ]).then(() => {
         assert.isTrue(element.hasAttribute('hidden'));
@@ -167,7 +168,7 @@
       getPluginLoader().loadPlugins([]);
 
       return Promise.all([
-        element.restApiService.getConfig(),
+        appContext.restApiService.getConfig(),
         getPluginLoader().awaitPluginsLoaded(),
       ]).then(() => {
         assert.isTrue(element.hasAttribute('hidden'));
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
index c8409fa..b70a55a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread_test.ts
@@ -45,6 +45,7 @@
   pressAndReleaseKeyOn,
 } from '@polymer/iron-test-helpers/mock-interactions';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../../../test/test-utils';
 
 const basicFixture = fixtureFromElement('gr-comment-thread');
 
@@ -55,11 +56,7 @@
     let element: GrCommentThread;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getLoggedIn() {
-          return Promise.resolve(false);
-        },
-      });
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
 
       element = basicFixture.instantiate();
       element.patchNum = 3 as PatchSetNum;
@@ -239,12 +236,12 @@
 
     test('setting project name loads the project config', done => {
       const projectName = 'foo/bar/baz' as RepoName;
-      const getProjectStub = sinon
-        .stub(element.restApiService, 'getProjectConfig')
-        .returns(Promise.resolve({} as ConfigInfo));
+      const getProjectStub = stubRestApi('getProjectConfig').returns(
+        Promise.resolve({} as ConfigInfo)
+      );
       element.projectName = projectName;
       flush(() => {
-        assert.isTrue(getProjectStub.calledWithExactly(projectName));
+        assert.isTrue(getProjectStub.calledWithExactly(projectName as never));
         done();
       });
     });
@@ -309,38 +306,34 @@
   let element: GrCommentThread;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() {
-        return Promise.resolve(false);
-      },
-      saveDiffDraft() {
-        return Promise.resolve(({
-          headers: {} as Headers,
-          redirected: false,
-          status: 200,
-          statusText: '',
-          type: '' as ResponseType,
-          url: '',
-          ok: true,
-          text() {
-            return Promise.resolve(
-              ")]}'\n" +
-                JSON.stringify({
-                  id: '7afa4931_de3d65bd',
-                  path: '/path/to/file.txt',
-                  line: 5,
-                  in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
-                  updated: '2015-12-21 02:01:10.850000000',
-                  message: 'Done',
-                })
-            );
-          },
-        } as unknown) as Response);
-      },
-      deleteDiffDraft() {
-        return Promise.resolve(({ok: true} as unknown) as Response);
-      },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('saveDiffDraft').returns(
+      Promise.resolve(({
+        headers: {} as Headers,
+        redirected: false,
+        status: 200,
+        statusText: '',
+        type: '' as ResponseType,
+        url: '',
+        ok: true,
+        text() {
+          return Promise.resolve(
+            ")]}'\n" +
+              JSON.stringify({
+                id: '7afa4931_de3d65bd',
+                path: '/path/to/file.txt',
+                line: 5,
+                in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+                updated: '2015-12-21 02:01:10.850000000',
+                message: 'Done',
+              })
+          );
+        },
+      } as unknown) as Response)
+    );
+    stubRestApi('deleteDiffDraft').returns(
+      Promise.resolve(({ok: true} as unknown) as Response)
+    );
     element = withCommentFixture.instantiate();
     element.patchNum = 1 as PatchSetNum;
     element.changeNum = 1 as NumericChangeId;
@@ -930,32 +923,28 @@
   let element: GrCommentThread;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getLoggedIn() {
-        return Promise.resolve(false);
-      },
-      saveDiffDraft() {
-        return Promise.resolve(({
-          ok: true,
-          text() {
-            return Promise.resolve(
-              ")]}'\n" +
-                JSON.stringify({
-                  id: '7afa4931_de3d65bd',
-                  path: '/path/to/file.txt',
-                  line: 5,
-                  in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
-                  updated: '2015-12-21 02:01:10.850000000',
-                  message: 'Done',
-                })
-            );
-          },
-        } as unknown) as Response);
-      },
-      deleteDiffDraft() {
-        return Promise.resolve(({ok: true} as unknown) as Response);
-      },
-    });
+    stubRestApi('getLoggedIn').returns(Promise.resolve(false));
+    stubRestApi('saveDiffDraft').returns(
+      Promise.resolve(({
+        ok: true,
+        text() {
+          return Promise.resolve(
+            ")]}'\n" +
+              JSON.stringify({
+                id: '7afa4931_de3d65bd',
+                path: '/path/to/file.txt',
+                line: 5,
+                in_reply_to: 'baf0414d_60047215' as UrlEncodedCommentId,
+                updated: '2015-12-21 02:01:10.850000000',
+                message: 'Done',
+              })
+          );
+        },
+      } as unknown) as Response)
+    );
+    stubRestApi('deleteDiffDraft').returns(
+      Promise.resolve(({ok: true} as unknown) as Response)
+    );
     element = withCommentFixture.instantiate();
     element.patchNum = 1 as PatchSetNum;
     element.changeNum = 1 as NumericChangeId;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
index cc89a7e..ea46ea1 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.js
@@ -20,6 +20,7 @@
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {__testOnly_UNSAVED_MESSAGE} from './gr-comment.js';
 import {SpecialFilePath, Side} from '../../../constants/constants.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-comment');
 
@@ -39,16 +40,12 @@
     let openOverlaySpy;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getAccount() {
-          return Promise.resolve({
-            email: 'dhruvsri@google.com',
-            name: 'Dhruv Srivastava',
-            _account_id: 1083225,
-            avatars: [{url: 'abc', height: 32}],
-          });
-        },
-      });
+      stubRestApi('getAccount').returns(Promise.resolve({
+        email: 'dhruvsri@google.com',
+        name: 'Dhruv Srivastava',
+        _account_id: 1083225,
+        avatars: [{url: 'abc', height: 32}],
+      }));
       element = basicFixture.instantiate();
       element.comment = {
         author: {
@@ -274,8 +271,7 @@
     });
 
     test('delete comment', done => {
-      sinon.stub(
-          element.restApiService, 'deleteComment').returns(Promise.resolve({}));
+      const stub = stubRestApi('deleteComment').returns(Promise.resolve({}));
       sinon.spy(element.confirmDeleteOverlay, 'open');
       element.changeNum = 42;
       element.patchNum = 0xDEADBEEF;
@@ -292,7 +288,7 @@
                   .querySelector('#confirmDeleteComment');
           dialog.message = 'removal reason';
           element._handleConfirmDeleteComment();
-          assert.isTrue(element.restApiService.deleteComment.calledWith(
+          assert.isTrue(stub.calledWith(
               42, 0xDEADBEEF, 'baf0414d_60047215', 'removal reason'));
           done();
         });
@@ -374,7 +370,7 @@
       element.patchNum = 1;
       const updateRequestStub = sinon.stub(element, '_updateRequestToast');
       const diffDraftStub =
-        sinon.stub(element.restApiService, 'saveDiffDraft').returns(
+        stubRestApi('saveDiffDraft').returns(
             Promise.resolve({ok: false}));
       element._saveDraft({id: 'abc_123'});
       flush(() => {
@@ -410,7 +406,7 @@
       element.patchNum = 1;
       const updateRequestStub = sinon.stub(element, '_updateRequestToast');
       const diffDraftStub =
-        sinon.stub(element.restApiService, 'saveDiffDraft').returns(
+        stubRestApi('saveDiffDraft').returns(
             Promise.reject(new Error()));
       element._saveDraft({id: 'abc_123'});
       flush(() => {
@@ -445,29 +441,23 @@
     let element;
 
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getAccount() { return Promise.resolve(null); },
-        getConfig() { return Promise.resolve({}); },
-        saveDiffDraft() {
-          return Promise.resolve({
-            ok: true,
-            text() {
-              return Promise.resolve(
-                  ')]}\'\n{' +
-                  '"id": "baf0414d_40572e03",' +
-                  '"path": "/path/to/file",' +
-                  '"line": 5,' +
-                  '"updated": "2015-12-08 21:52:36.177000000",' +
-                  '"message": "saved!"' +
-                '}'
-              );
-            },
-          });
+      stubRestApi('getAccount').returns(Promise.resolve(null));
+      stubRestApi('getConfig').returns(Promise.resolve({}));
+      stubRestApi('saveDiffDraft').returns(Promise.resolve({
+        ok: true,
+        text() {
+          return Promise.resolve(
+              ')]}\'\n{' +
+              '"id": "baf0414d_40572e03",' +
+              '"path": "/path/to/file",' +
+              '"line": 5,' +
+              '"updated": "2015-12-08 21:52:36.177000000",' +
+              '"message": "saved!"' +
+              '}'
+          );
         },
-        removeChangeReviewer() {
-          return Promise.resolve({ok: true});
-        },
-      });
+      }));
+      stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
       stub('gr-storage', {
         getDraftComment() { return null; },
       });
@@ -802,7 +792,7 @@
     test('storage is cleared only after save success', () => {
       element._messageText = 'test';
       const eraseStub = sinon.stub(element, '_eraseDraftComment');
-      sinon.stub(element.restApiService, 'getResponseObject')
+      stubRestApi('getResponseObject')
           .returns(Promise.resolve({}));
 
       sinon.stub(element, '_saveDraft').returns(Promise.resolve({ok: false}));
@@ -1240,9 +1230,7 @@
 
     let clock;
     setup(() => {
-      stub('gr-rest-api-interface', {
-        getAccount() { return Promise.resolve(null); },
-      });
+      stubRestApi('getAccount').returns(Promise.resolve(null));
       clock = sinon.useFakeTimers();
     });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
index 0a7f6dd..9a96c2d 100644
--- a/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-date-formatter/gr-date-formatter_test.js
@@ -19,6 +19,7 @@
 import './gr-date-formatter.js';
 import {parseDate} from '../../../utils/date-util.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
 <gr-date-formatter date-str="2015-09-24 23:30:17.033000000"></gr-date-formatter>
@@ -62,10 +63,8 @@
   function stubRestAPI(preferences) {
     const loggedInPromise = Promise.resolve(preferences !== null);
     const preferencesPromise = Promise.resolve(preferences);
-    stub('gr-rest-api-interface', {
-      getLoggedIn: sinon.stub().returns(loggedInPromise),
-      getPreferences: sinon.stub().returns(preferencesPromise),
-    });
+    stubRestApi('getLoggedIn').returns(loggedInPromise);
+    stubRestApi('getPreferences').returns(preferencesPromise);
     return Promise.all([loggedInPromise, preferencesPromise]);
   }
 
diff --git a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
index 6afb299..716ef2f 100644
--- a/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-diff-preferences/gr-diff-preferences_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-diff-preferences.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-diff-preferences');
 
@@ -50,11 +51,7 @@
       ignore_whitespace: 'IGNORE_NONE',
     };
 
-    stub('gr-rest-api-interface', {
-      getDiffPreferences() {
-        return Promise.resolve(diffPreferences);
-      },
-    });
+    stubRestApi('getDiffPreferences').returns(Promise.resolve(diffPreferences));
 
     element = basicFixture.instantiate();
 
@@ -89,7 +86,7 @@
   });
 
   test('save changes', () => {
-    sinon.stub(element.restApiService, 'saveDiffPreferences')
+    stubRestApi('saveDiffPreferences')
         .returns(Promise.resolve());
     const showTrailingWhitespaceCheckbox =
         valueOf('Show trailing whitespace', 'diffPreferences')
diff --git a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
index 105cc2c..6427c6b 100644
--- a/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-download-commands/gr-download-commands_test.js
@@ -17,7 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-download-commands.js';
-import {isHidden} from '../../../test/test-utils.js';
+import {isHidden, stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-download-commands');
 
@@ -50,6 +50,7 @@
 
   suite('unauthenticated', () => {
     setup(async () => {
+      stubRestApi('getLoggedIn').returns(Promise.resolve(false));
       element = basicFixture.instantiate();
       element.schemes = SCHEMES;
       element.commands = COMMANDS;
@@ -66,16 +67,12 @@
     });
 
     test('element visibility', () => {
-      assert.isFalse(isHidden(element.shadowRoot
-          .querySelector('paper-tabs')));
-      assert.isFalse(isHidden(element.shadowRoot
-          .querySelector('.commands')));
+      assert.isFalse(isHidden(element.shadowRoot.querySelector('paper-tabs')));
+      assert.isFalse(isHidden(element.shadowRoot.querySelector('.commands')));
 
       element.schemes = [];
-      assert.isTrue(isHidden(element.shadowRoot
-          .querySelector('paper-tabs')));
-      assert.isTrue(isHidden(element.shadowRoot
-          .querySelector('.commands')));
+      assert.isTrue(isHidden(element.shadowRoot.querySelector('paper-tabs')));
+      assert.isTrue(isHidden(element.shadowRoot.querySelector('.commands')));
     });
 
     test('tab selection', () => {
@@ -87,38 +84,30 @@
       assert.equal(element.$.downloadTabs.selected, '2');
     });
 
-    test('loads scheme from preferences', () => {
-      stub('gr-rest-api-interface', {
-        getPreferences() {
-          return Promise.resolve({download_scheme: 'repo'});
-        },
-      });
+    test('loads scheme from preferences', async () => {
+      const stub = stubRestApi('getPreferences').returns(
+          Promise.resolve({download_scheme: 'repo'}));
       element._loggedIn = true;
-      assert.isTrue(element.restApiService.getPreferences.called);
-      return element.restApiService.getPreferences.lastCall.returnValue.then(
-          () => {
-            assert.equal(element.selectedScheme, 'repo');
-          });
+      await flush();
+      assert.isTrue(stub.called);
+      await stub.lastCall.returnValue;
+      assert.equal(element.selectedScheme, 'repo');
     });
 
-    test('normalize scheme from preferences', () => {
-      stub('gr-rest-api-interface', {
-        getPreferences() {
-          return Promise.resolve({download_scheme: 'REPO'});
-        },
-      });
+    test('normalize scheme from preferences', async () => {
+      const stub = stubRestApi('getPreferences').returns(
+          Promise.resolve({download_scheme: 'REPO'}));
       element._loggedIn = true;
-      return element.restApiService.getPreferences.lastCall.returnValue.then(
-          () => {
-            assert.equal(element.selectedScheme, 'repo');
-          });
+      await flush();
+      assert.isTrue(stub.called);
+      await stub.lastCall.returnValue;
+      assert.equal(element.selectedScheme, 'repo');
     });
 
     test('saves scheme to preferences', () => {
       element._loggedIn = true;
-      const savePrefsStub = sinon.stub(element.restApiService,
-          'savePreferences')
-          .callsFake(() => Promise.resolve());
+      const savePrefsStub = stubRestApi('savePreferences').returns(
+          Promise.resolve());
 
       flush();
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
index e3d7ed70..c2c8d68 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown-list/gr-dropdown-list_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-dropdown-list.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-dropdown-list');
 
@@ -24,9 +25,7 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
index 515644b..02bffe4 100644
--- a/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-dropdown/gr-dropdown_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-dropdown.js';
 import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-dropdown');
 
@@ -25,9 +26,7 @@
   let element;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
index 8d1f499..1c67e13 100644
--- a/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-hovercard-account/gr-hovercard-account_test.js
@@ -19,7 +19,7 @@
 import './gr-hovercard-account.js';
 import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 import {ReviewerState} from '../../../constants/constants.js';
-import {appContext} from '../../../services/app-context.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromTemplate(html`
 <gr-hovercard-account class="hovered"></gr-hovercard-account>
@@ -36,10 +36,8 @@
   };
 
   setup(async () => {
-    sinon.stub(appContext.restApiService, 'getAccount').returns(
-        Promise.resolve({...ACCOUNT})
-    );
-    sinon.stub(appContext.restApiService, 'getConfig').returns(
+    stubRestApi('getAccount').returns(Promise.resolve({...ACCOUNT}));
+    stubRestApi('getConfig').returns(
         Promise.resolve({change: {enable_attention_set: true}})
     );
     element = basicFixture.instantiate();
@@ -111,8 +109,7 @@
         [ReviewerState.REVIEWER]: [ACCOUNT],
       },
     };
-    sinon.stub(element.restApiService, 'removeChangeReviewer').returns(
-        Promise.resolve({ok: true}));
+    stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
     flush();
@@ -131,11 +128,10 @@
         [ReviewerState.REVIEWER]: [ACCOUNT],
       },
     };
-    const saveReviewStub = sinon.stub(element.restApiService,
+    const saveReviewStub = stubRestApi(
         'saveChangeReview').returns(
         Promise.resolve({ok: true}));
-    sinon.stub(element.restApiService, 'removeChangeReviewer').returns(
-        Promise.resolve({ok: true}));
+    stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
@@ -158,11 +154,9 @@
         [ReviewerState.REVIEWER]: [],
       },
     };
-    const saveReviewStub = sinon.stub(element.restApiService,
-        'saveChangeReview').returns(
-        Promise.resolve({ok: true}));
-    sinon.stub(element.restApiService, 'removeChangeReviewer').returns(
-        Promise.resolve({ok: true}));
+    const saveReviewStub = stubRestApi(
+        'saveChangeReview').returns(Promise.resolve({ok: true}));
+    stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
     flush();
@@ -185,8 +179,7 @@
         [ReviewerState.REVIEWER]: [],
       },
     };
-    sinon.stub(element.restApiService, 'removeChangeReviewer').returns(
-        Promise.resolve({ok: true}));
+    stubRestApi('removeChangeReviewer').returns(Promise.resolve({ok: true}));
     const reloadListener = sinon.spy();
     element._target.addEventListener('reload', reloadListener);
 
@@ -207,8 +200,7 @@
     const apiPromise = new Promise(r => {
       apiResolve = r;
     });
-    sinon.stub(element.restApiService, 'addToAttentionSet')
-        .callsFake(() => apiPromise);
+    stubRestApi('addToAttentionSet').returns(apiPromise);
     element.highlightAttention = true;
     element._target = document.createElement('div');
     flush();
@@ -240,8 +232,7 @@
     const apiPromise = new Promise(r => {
       apiResolve = r;
     });
-    sinon.stub(element.restApiService, 'removeFromAttentionSet')
-        .callsFake(() => apiPromise);
+    stubRestApi('removeFromAttentionSet').returns(apiPromise);
     element.highlightAttention = true;
     element.change = {
       attention_set: {31415926535: {}},
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
index 8f743e9..979b86e 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-api-utils.ts
@@ -16,23 +16,13 @@
  */
 
 import {getBaseUrl} from '../../../utils/url-util';
-import {RestApiService} from '../../../services/services/gr-rest-api/gr-rest-api';
 import {HttpMethod} from '../../../constants/constants';
 import {RequestPayload} from '../../../types/common';
+import {appContext} from '../../../services/app-context';
 
 export const PRELOADED_PROTOCOL = 'preloaded:';
 export const PLUGIN_LOADING_TIMEOUT_MS = 10000;
 
-let _restAPI: RestApiService | undefined;
-export function getRestAPI() {
-  if (!_restAPI) {
-    _restAPI = (document.createElement(
-      'gr-rest-api-interface'
-    ) as unknown) as RestApiService;
-  }
-  return _restAPI;
-}
-
 /**
  * Retrieves the name of the plugin base on the url.
  */
@@ -83,7 +73,7 @@
   opt_callback?: (response: unknown) => void,
   opt_payload?: RequestPayload
 ) {
-  return getRestAPI()
+  return appContext.restApiService
     .send(method, url, opt_payload)
     .then(response => {
       if (response.status < 200 || response.status >= 300) {
@@ -95,7 +85,7 @@
           }
         });
       } else {
-        return getRestAPI().getResponseObject(response);
+        return appContext.restApiService.getResponseObject(response);
       }
     })
     .then(response => {
@@ -105,9 +95,3 @@
       return response;
     });
 }
-
-// TEST only methods / properties
-
-export function testOnly_resetInternalState() {
-  _restAPI = undefined;
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
index 8f41b39..52efc56 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import '../../change/gr-reply-dialog/gr-reply-dialog.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-reply-dialog');
 
@@ -30,10 +31,8 @@
   let plugin;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getAccount() { return Promise.resolve(null); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getAccount').returns(Promise.resolve(null));
   });
 
   suite('early init', () => {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
index cf254d4..74b84bc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit.ts
@@ -24,7 +24,7 @@
   PluginOptionMap,
   PluginLoader,
 } from './gr-plugin-loader';
-import {getRestAPI, send} from './gr-api-utils';
+import {send} from './gr-api-utils';
 import {appContext} from '../../../services/app-context';
 import {PluginApi} from '../../plugins/gr-plugin-types';
 import {HttpMethod} from '../../../constants/constants';
@@ -118,7 +118,7 @@
   callback?: (response: Response) => void
 ) {
   console.warn('.delete() is deprecated! Use plugin.restApi().delete()');
-  return getRestAPI()
+  return appContext.restApiService
     .send(HttpMethod.DELETE, url)
     .then(response => {
       if (response.status !== 204) {
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
index 9312f1b..77eb457 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-gerrit_test.js
@@ -20,6 +20,7 @@
 import {getPluginLoader} from './gr-plugin-loader.js';
 import {resetPlugins} from '../../../test/test-utils.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
 
@@ -29,20 +30,12 @@
   let element;
 
   let clock;
-  let sendStub;
 
   setup(() => {
     clock = sinon.useFakeTimers();
 
-    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
-    stub('gr-rest-api-interface', {
-      getAccount() {
-        return Promise.resolve({name: 'Judy Hopps'});
-      },
-      send(...args) {
-        return sendStub(...args);
-      },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
+    stubRestApi('send').returns(Promise.resolve({status: 200}));
     element = basicFixture.instantiate();
   });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
index 1481c2d..ce57f92 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface_test.js
@@ -25,6 +25,7 @@
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {stubBaseUrl} from '../../../test/test-utils.js';
 import sinon from 'sinon/pkg/sinon-esm';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
 
@@ -46,17 +47,10 @@
   setup(() => {
     clock = sinon.useFakeTimers();
 
-    getResponseObjectStub = sinon.stub().returns(Promise.resolve());
-    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
-    stub('gr-rest-api-interface', {
-      getAccount() {
-        return Promise.resolve({name: 'Judy Hopps'});
-      },
-      getResponseObject: getResponseObjectStub,
-      send(...args) {
-        return sendStub(...args);
-      },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
+    getResponseObjectStub = stubRestApi('getResponseObject').returns(
+        Promise.resolve());
+    sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
     element = basicFixture.instantiate();
     errorStub = sinon.stub(element.reporting, 'error');
     pluginApi.install(p => { plugin = p; }, '0.1',
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
index 584cd39..66248c6 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-loader_test.js
@@ -22,6 +22,7 @@
 import {resetPlugins, stubBaseUrl} from '../../../test/test-utils.js';
 import {_testOnly_flushPreinstalls} from './gr-gerrit.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-js-api-interface');
 
@@ -31,22 +32,14 @@
   let plugin;
 
   let url;
-  let sendStub;
   let pluginLoader;
   let clock;
 
   setup(() => {
     clock = sinon.useFakeTimers();
 
-    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
-    stub('gr-rest-api-interface', {
-      getAccount() {
-        return Promise.resolve({name: 'Judy Hopps'});
-      },
-      send(...args) {
-        return sendStub(...args);
-      },
-    });
+    stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
+    stubRestApi('send').returns(Promise.resolve({status: 200}));
     pluginLoader = _testOnly_resetPluginLoader();
     sinon.stub(document.body, 'appendChild');
     basicFixture.instantiate();
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
index eb63a77..1d60288 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api.ts
@@ -14,57 +14,42 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {
-  ErrorCallback,
-  RestApiService,
-} from '../../../services/services/gr-rest-api/gr-rest-api';
+import {ErrorCallback} from '../../../services/services/gr-rest-api/gr-rest-api';
 import {HttpMethod} from '../../../constants/constants';
 import {RequestPayload} from '../../../types/common';
-
-let restApi: RestApiService | null = null;
-
-export function _testOnlyResetRestApi() {
-  restApi = null;
-}
-
-function getRestApi(): RestApiService {
-  if (!restApi) {
-    restApi = (document.createElement(
-      'gr-rest-api-interface'
-    ) as unknown) as RestApiService;
-  }
-  return restApi;
-}
+import {appContext} from '../../../services/app-context';
 
 export class GrPluginRestApi {
+  private readonly restApi = appContext.restApiService;
+
   constructor(private readonly prefix = '') {}
 
   getLoggedIn() {
-    return getRestApi().getLoggedIn();
+    return this.restApi.getLoggedIn();
   }
 
   getVersion() {
-    return getRestApi().getVersion();
+    return this.restApi.getVersion();
   }
 
   getConfig() {
-    return getRestApi().getConfig();
+    return this.restApi.getConfig();
   }
 
   invalidateReposCache() {
-    getRestApi().invalidateReposCache();
+    this.restApi.invalidateReposCache();
   }
 
   getAccount() {
-    return getRestApi().getAccount();
+    return this.restApi.getAccount();
   }
 
   getAccountCapabilities(capabilities: string[]) {
-    return getRestApi().getAccountCapabilities(capabilities);
+    return this.restApi.getAccountCapabilities(capabilities);
   }
 
   getRepos(filter: string, reposPerPage: number, offset?: number) {
-    return getRestApi().getRepos(filter, reposPerPage, offset);
+    return this.restApi.getRepos(filter, reposPerPage, offset);
   }
 
   fetch(
@@ -101,7 +86,7 @@
     errFn?: ErrorCallback,
     contentType?: string
   ): Promise<Response | void> {
-    return getRestApi().send(
+    return this.restApi.send(
       method,
       this.prefix + url,
       payload,
@@ -151,7 +136,7 @@
             }
           });
         } else {
-          return getRestApi().getResponseObject(response);
+          return this.restApi.getResponseObject(response);
         }
       }
     );
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
index 53aaa1e..d2b5658 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-rest-api_test.js
@@ -19,31 +19,20 @@
 import './gr-js-api-interface.js';
 import {GrPluginRestApi} from './gr-plugin-rest-api.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
 suite('gr-plugin-rest-api tests', () => {
   let instance;
-
   let getResponseObjectStub;
   let sendStub;
-  let restApiStub;
 
   setup(() => {
-    getResponseObjectStub = sinon.stub().returns(Promise.resolve());
-    sendStub = sinon.stub().returns(Promise.resolve({status: 200}));
-    restApiStub = {
-      getAccount: () => Promise.resolve({name: 'Judy Hopps'}),
-      getResponseObject: getResponseObjectStub,
-      send: sendStub,
-      getLoggedIn: sinon.stub(),
-      getVersion: sinon.stub(),
-      getConfig: sinon.stub(),
-    };
-    stub('gr-rest-api-interface', Object.keys(restApiStub).reduce((a, k) => {
-      a[k] = (...args) => restApiStub[k](...args);
-      return a;
-    }, {}));
+    stubRestApi('getAccount').returns(Promise.resolve({name: 'Judy Hopps'}));
+    getResponseObjectStub = stubRestApi('getResponseObject').returns(
+        Promise.resolve());
+    sendStub = stubRestApi('send').returns(Promise.resolve({status: 200}));
     pluginApi.install(p => {}, '0.1',
         'http://test.com/plugins/testplugin/static/test.js');
     instance = new GrPluginRestApi();
@@ -119,25 +108,25 @@
   });
 
   test('getLoggedIn', () => {
-    restApiStub.getLoggedIn.returns(Promise.resolve(true));
+    const stub = stubRestApi('getLoggedIn').returns(Promise.resolve(true));
     return instance.getLoggedIn().then(result => {
-      assert.isTrue(restApiStub.getLoggedIn.calledOnce);
+      assert.isTrue(stub.calledOnce);
       assert.isTrue(result);
     });
   });
 
   test('getVersion', () => {
-    restApiStub.getVersion.returns(Promise.resolve('foo bar'));
+    const stub = stubRestApi('getVersion').returns(Promise.resolve('foo bar'));
     return instance.getVersion().then(result => {
-      assert.isTrue(restApiStub.getVersion.calledOnce);
+      assert.isTrue(stub.calledOnce);
       assert.equal(result, 'foo bar');
     });
   });
 
   test('getConfig', () => {
-    restApiStub.getConfig.returns(Promise.resolve('foo bar'));
+    const stub = stubRestApi('getConfig').returns(Promise.resolve('foo bar'));
     return instance.getConfig().then(result => {
-      assert.isTrue(restApiStub.getConfig.calledOnce);
+      assert.isTrue(stub.calledOnce);
       assert.equal(result, 'foo bar');
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js
index 1229641..9e3876b 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-reporting-js-api_test.js
@@ -19,6 +19,7 @@
 import '../../change/gr-reply-dialog/gr-reply-dialog.js';
 import {_testOnly_initGerritPluginApi} from './gr-gerrit.js';
 import {appContext} from '../../../services/app-context.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const pluginApi = _testOnly_initGerritPluginApi();
 
@@ -27,10 +28,8 @@
   let plugin;
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-      getAccount() { return Promise.resolve(null); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    stubRestApi('getAccount').returns(Promise.resolve(null));
   });
 
   suite('early init', () => {
@@ -72,4 +71,4 @@
       );
     });
   });
-});
\ No newline at end of file
+});
diff --git a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
index aa71bfc..b2365aa 100644
--- a/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-label-info/gr-label-info_test.js
@@ -18,6 +18,7 @@
 import '../../../test/common-test-setup-karma.js';
 import './gr-label-info.js';
 import {isHidden} from '../../../test/test-utils.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-label-info');
 
@@ -72,8 +73,7 @@
 
     test('deletes votes', () => {
       const deleteResponse = Promise.resolve({ok: true});
-      const deleteStub = sinon.stub(
-          element.restApiService, 'deleteVote').returns(deleteResponse);
+      const deleteStub = stubRestApi('deleteVote').returns(deleteResponse);
 
       element.change.removable_reviewers = [element.account];
       element.change.labels.test.recommended = {_account_id: 1};
diff --git a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
index 8fbc639..4f12edc 100644
--- a/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-repo-branch-picker/gr-repo-branch-picker_test.js
@@ -17,6 +17,7 @@
 
 import '../../../test/common-test-setup-karma.js';
 import './gr-repo-branch-picker.js';
+import {stubRestApi} from '../../../test/test-utils.js';
 
 const basicFixture = fixtureFromElement('gr-repo-branch-picker');
 
@@ -28,8 +29,9 @@
   });
 
   suite('_getRepoSuggestions', () => {
+    let getReposStub;
     setup(() => {
-      sinon.stub(element.restApiService, 'getRepos')
+      getReposStub = stubRestApi('getRepos')
           .returns(Promise.resolve([
             {
               id: 'plugins%2Favatars-external',
@@ -47,25 +49,25 @@
           ]));
     });
 
-    test('converts to suggestion objects', () => {
+    test('converts to suggestion objects', async () => {
       const input = 'plugins/avatars';
-      return element._getRepoSuggestions(input).then(suggestions => {
-        assert.isTrue(element.restApiService.getRepos.calledWith(input));
-        const unencodedNames = [
-          'plugins/avatars-external',
-          'plugins/avatars-gravatar',
-          'plugins/avatars/external',
-          'plugins/avatars/gravatar',
-        ];
-        assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
-        assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
-      });
+      const suggestions = await element._getRepoSuggestions(input);
+      assert.isTrue(getReposStub.calledWith(input));
+      const unencodedNames = [
+        'plugins/avatars-external',
+        'plugins/avatars-gravatar',
+        'plugins/avatars/external',
+        'plugins/avatars/gravatar',
+      ];
+      assert.deepEqual(suggestions.map(s => s.name), unencodedNames);
+      assert.deepEqual(suggestions.map(s => s.value), unencodedNames);
     });
   });
 
   suite('_getRepoBranchesSuggestions', () => {
+    let getRepoBranchesStub;
     setup(() => {
-      sinon.stub(element.restApiService, 'getRepoBranches')
+      getRepoBranchesStub = stubRestApi('getRepoBranches')
           .returns(Promise.resolve([
             {ref: 'refs/heads/stable-2.10'},
             {ref: 'refs/heads/stable-2.11'},
@@ -76,48 +78,43 @@
           ]));
     });
 
-    test('converts to suggestion objects', () => {
+    test('converts to suggestion objects', async () => {
       const repo = 'gerrit';
       const branchInput = 'stable-2.1';
       element.repo = repo;
-      return element._getRepoBranchesSuggestions(branchInput)
-          .then(suggestions => {
-            assert.isTrue(element.restApiService.getRepoBranches.calledWith(
-                branchInput, repo, 15));
-            const refNames = [
-              'stable-2.10',
-              'stable-2.11',
-              'stable-2.12',
-              'stable-2.13',
-              'stable-2.14',
-              'stable-2.15',
-            ];
-            assert.deepEqual(suggestions.map(s => s.name), refNames);
-            assert.deepEqual(suggestions.map(s => s.value), refNames);
-          });
+      const suggestions =
+          await element._getRepoBranchesSuggestions(branchInput);
+      assert.isTrue(getRepoBranchesStub.calledWith(branchInput, repo, 15));
+      const refNames = [
+        'stable-2.10',
+        'stable-2.11',
+        'stable-2.12',
+        'stable-2.13',
+        'stable-2.14',
+        'stable-2.15',
+      ];
+      assert.deepEqual(suggestions.map(s => s.name), refNames);
+      assert.deepEqual(suggestions.map(s => s.value), refNames);
     });
 
-    test('filters out ref prefix', () => {
+    test('filters out ref prefix', async () => {
       const repo = 'gerrit';
       const branchInput = 'refs/heads/stable-2.1';
       element.repo = repo;
       return element._getRepoBranchesSuggestions(branchInput)
           .then(suggestions => {
-            assert.isTrue(element.restApiService.getRepoBranches.calledWith(
+            assert.isTrue(getRepoBranchesStub.calledWith(
                 'stable-2.1', repo, 15));
           });
     });
 
-    test('does not query when repo is unset', () => element
-        ._getRepoBranchesSuggestions('')
-        .then(() => {
-          assert.isFalse(element.restApiService.getRepoBranches.called);
-          element.repo = 'gerrit';
-          return element._getRepoBranchesSuggestions('');
-        })
-        .then(() => {
-          assert.isTrue(element.restApiService.getRepoBranches.called);
-        }));
+    test('does not query when repo is unset', async () => {
+      await element._getRepoBranchesSuggestions('');
+      assert.isFalse(getRepoBranchesStub.called);
+      element.repo = 'gerrit';
+      await element._getRepoBranchesSuggestions('');
+      assert.isTrue(getRepoBranchesStub.called);
+    });
   });
 });
 
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
index 9e77784..d12eaca 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.ts
@@ -25,6 +25,8 @@
   FetchParams,
   FetchPromisesCache,
   GrRestApiHelper,
+  parsePrefixedJSON,
+  readResponsePayload,
   SendJSONRequest,
   SendRequest,
   SiteBasedCache,
@@ -149,13 +151,16 @@
 } from '../../../services/services/gr-rest-api/gr-rest-api';
 import {
   CommentSide,
+  createDefaultDiffPrefs,
+  createDefaultEditPrefs,
+  createDefaultPreferences,
   DiffViewMode,
   HttpMethod,
   ReviewerState,
 } from '../../../constants/constants';
 import {firePageError, fireServerError} from '../../../utils/event-util';
 
-const JSON_PREFIX = ")]}'";
+export const JSON_PREFIX = ")]}'";
 const MAX_PROJECT_RESULTS = 25;
 // This value is somewhat arbitrary and not based on research or calculations.
 const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
@@ -688,27 +693,7 @@
           reportUrlAsIs: true,
         }) as Promise<DiffPreferencesInfo | undefined>;
       }
-      const anonymousResult: DiffPreferencesInfo = {
-        auto_hide_diff_table_header: true,
-        context: 10,
-        cursor_blink_rate: 0,
-        font_size: 12,
-        ignore_whitespace: 'IGNORE_NONE',
-        intraline_difference: true,
-        line_length: 100,
-        line_wrapping: false,
-        show_line_endings: true,
-        show_tabs: true,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-        tab_size: 8,
-        theme: 'DEFAULT',
-      };
-      // These defaults should match the defaults in
-      // java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
-      // NOTE: There are some settings that don't apply to PolyGerrit
-      // (Render mode being at least one of them).
-      return Promise.resolve(anonymousResult);
+      return Promise.resolve(createDefaultDiffPrefs());
     });
   }
 
@@ -720,27 +705,7 @@
           reportUrlAsIs: true,
         }) as Promise<EditPreferencesInfo | undefined>;
       }
-      const result: EditPreferencesInfo = {
-        auto_close_brackets: false,
-        cursor_blink_rate: 0,
-        hide_line_numbers: false,
-        hide_top_menu: false,
-        indent_unit: 2,
-        indent_with_tabs: false,
-        key_map_type: 'DEFAULT',
-        line_length: 100,
-        line_wrapping: false,
-        match_brackets: true,
-        show_base: false,
-        show_tabs: true,
-        show_whitespace_errors: true,
-        syntax_highlighting: true,
-        tab_size: 8,
-        theme: 'DEFAULT',
-      };
-      // These defaults should match the defaults in
-      // java/com/google/gerrit/extensions/client/EditPreferencesInfo.java
-      return Promise.resolve(result);
+      return Promise.resolve(createDefaultEditPrefs());
     });
   }
 
@@ -1041,19 +1006,7 @@
           return prefInfo;
         });
       }
-
-      // TODO(TS): Many properties are omitted here, but they are required.
-      // Add default values for missed properties
-      const anonymousPrefs = {
-        changes_per_page: 25,
-        default_diff_view: this._isNarrowScreen()
-          ? DiffViewMode.UNIFIED
-          : DiffViewMode.SIDE_BY_SIDE,
-        diff_view: DiffViewMode.SIDE_BY_SIDE,
-        size_bar_in_change_table: true,
-      } as PreferencesInfo;
-
-      return anonymousPrefs;
+      return createDefaultPreferences();
     });
   }
 
@@ -1296,7 +1249,7 @@
         };
         return this._restApiHelper.fetchRawJSON(req).then(response => {
           if (response?.status === 304) {
-            return (this._restApiHelper.parsePrefixedJSON(
+            return (parsePrefixedJSON(
               // urlWithParams already cached
               this._etags.getCachedPayload(urlWithParams)!
             ) as unknown) as ChangeInfo;
@@ -1315,20 +1268,18 @@
             return Promise.resolve(null);
           }
 
-          return this._restApiHelper
-            .readResponsePayload(response)
-            .then(payload => {
-              if (!payload) {
-                return null;
-              }
-              this._etags.collect(urlWithParams, response, payload.raw);
-              // TODO(TS): Why it is always change info?
-              this._maybeInsertInLookup(
-                (payload.parsed as unknown) as ChangeInfo
-              );
+          return readResponsePayload(response).then(payload => {
+            if (!payload) {
+              return null;
+            }
+            this._etags.collect(urlWithParams, response, payload.raw);
+            // TODO(TS): Why it is always change info?
+            this._maybeInsertInLookup(
+              (payload.parsed as unknown) as ChangeInfo
+            );
 
-              return (payload.parsed as unknown) as ChangeInfo;
-            });
+            return (payload.parsed as unknown) as ChangeInfo;
+          });
         });
       }
     );
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
index 717fe54..42030c7 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.js
@@ -23,6 +23,11 @@
 import {appContext} from '../../../services/app-context.js';
 import {createChange} from '../../../test/test-data-generators.js';
 import {CURRENT} from '../../../utils/patch-set-util.js';
+import {
+  parsePrefixedJSON,
+  readResponsePayload,
+} from './gr-rest-apis/gr-rest-api-helper.js';
+import {JSON_PREFIX} from './gr-rest-api-interface.js';
 
 const basicFixture = fixtureFromElement('gr-rest-api-interface');
 
@@ -353,19 +358,6 @@
         });
       });
 
-  test('getPreferences returns correctly on small screens not logged in',
-      () => {
-        const testJSON = {diff_view: 'SIDE_BY_SIDE'};
-        const loggedIn = false;
-        const smallScreen = true;
-
-        preferenceSetup(testJSON, loggedIn, smallScreen);
-        return element.getPreferences().then(obj => {
-          assert.equal(obj.default_diff_view, 'UNIFIED_DIFF');
-          assert.equal(obj.diff_view, 'SIDE_BY_SIDE');
-        });
-      });
-
   test('getPreferences returns correctly on larger screens logged in',
       () => {
         const testJSON = {diff_view: 'UNIFIED_DIFF'};
@@ -932,22 +924,18 @@
       });
     });
 
-    test('_getChangeDetail populates _projectLookup', () => {
+    test('_getChangeDetail populates _projectLookup', async () => {
       sinon.stub(element, 'getChangeActionURL')
           .returns(Promise.resolve(''));
       sinon.stub(element._restApiHelper, 'fetchRawJSON')
-          .returns(Promise.resolve({ok: true}));
-
-      const mockResponse = {_number: 1, project: 'test'};
-      sinon.stub(element._restApiHelper, 'readResponsePayload')
           .returns(Promise.resolve({
-            parsed: mockResponse,
-            raw: JSON.stringify(mockResponse),
+            ok: true,
+            status: 200,
+            text: () => Promise.resolve(`)]}'{"_number":1,"project":"test"}`),
           }));
-      return element._getChangeDetail(1, '516714').then(() => {
-        assert.equal(Object.keys(element._projectLookup).length, 1);
-        assert.equal(element._projectLookup[1], 'test');
-      });
+      await element._getChangeDetail(1, '516714');
+      assert.equal(Object.keys(element._projectLookup).length, 1);
+      assert.equal(element._projectLookup[1], 'test');
     });
 
     suite('_getChangeDetail ETag cache', () => {
@@ -1106,21 +1094,19 @@
   });
 
   suite('reading responses', () => {
-    test('_readResponsePayload', () => {
+    test('_readResponsePayload', async () => {
       const mockObject = {foo: 'bar', baz: 'foo'};
-      const serial = element.JSON_PREFIX + JSON.stringify(mockObject);
+      const serial = JSON_PREFIX + JSON.stringify(mockObject);
       const mockResponse = {text: () => Promise.resolve(serial)};
-      return element._restApiHelper.readResponsePayload(mockResponse)
-          .then(payload => {
-            assert.deepEqual(payload.parsed, mockObject);
-            assert.equal(payload.raw, serial);
-          });
+      const payload = await readResponsePayload(mockResponse);
+      assert.deepEqual(payload.parsed, mockObject);
+      assert.equal(payload.raw, serial);
     });
 
     test('_parsePrefixedJSON', () => {
       const obj = {x: 3, y: {z: 4}, w: 23};
-      const serial = element.JSON_PREFIX + JSON.stringify(obj);
-      const result = element._restApiHelper.parsePrefixedJSON(serial);
+      const serial = JSON_PREFIX + JSON.stringify(obj);
+      const result = parsePrefixedJSON(serial);
       assert.deepEqual(result, obj);
     });
   });
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
index 616ec02..dcb2ef0 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper.ts
@@ -47,6 +47,29 @@
   raw: string;
 }
 
+export function readResponsePayload(
+  response: Response
+): Promise<ResponsePayload> {
+  return response.text().then(text => {
+    let result;
+    try {
+      result = parsePrefixedJSON(text);
+    } catch (_) {
+      result = null;
+    }
+    // TODO(TS): readResponsePayload can assign null to the parsed property if
+    // it can't parse input data. However polygerrit assumes in many places
+    // that the parsed property can't be null. We should update
+    // readResponsePayload method and reject a promise instead of assigning
+    // null to the parsed property
+    return {parsed: result!, raw: text};
+  });
+}
+
+export function parsePrefixedJSON(jsonWithPrefix: string): ParsedJSON {
+  return JSON.parse(jsonWithPrefix.substring(JSON_PREFIX.length)) as ParsedJSON;
+}
+
 /**
  * Wrapper around Map for caching server responses. Site-based so that
  * changes to CANONICAL_PATH will result in a different cache going into
@@ -390,30 +413,7 @@
   }
 
   getResponseObject(response: Response): Promise<ParsedJSON> {
-    return this.readResponsePayload(response).then(payload => payload.parsed);
-  }
-
-  readResponsePayload(response: Response): Promise<ResponsePayload> {
-    return response.text().then(text => {
-      let result;
-      try {
-        result = this.parsePrefixedJSON(text);
-      } catch (_) {
-        result = null;
-      }
-      // TODO(TS): readResponsePayload can assign null to the parsed property if
-      // it can't parse input data. However polygerrit assumes in many places
-      // that the parsed property can't be null. We should update
-      // readResponsePayload method and reject a promise instead of assigning
-      // null to the parsed property
-      return {parsed: result!, raw: text};
-    });
-  }
-
-  parsePrefixedJSON(jsonWithPrefix: string): ParsedJSON {
-    return JSON.parse(
-      jsonWithPrefix.substring(JSON_PREFIX.length)
-    ) as ParsedJSON;
+    return readResponsePayload(response).then(payload => payload.parsed);
   }
 
   addAcceptJsonHeader(req: FetchJSONRequest) {
diff --git a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
index ac48ea0..85c92d3 100644
--- a/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-email-suggestions-provider/gr-email-suggestions-provider_test.js
@@ -16,12 +16,11 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GrEmailSuggestionsProvider} from './gr-email-suggestions-provider.js';
 import {appContext} from '../../services/app-context.js';
+import {stubRestApi} from '../../test/test-utils.js';
 
 suite('GrEmailSuggestionsProvider tests', () => {
-  let restAPI;
   let provider;
   const account1 = {
     name: 'Some name',
@@ -33,17 +32,14 @@
   };
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
-    restAPI = appContext.restApiService;
-    provider = new GrEmailSuggestionsProvider(restAPI);
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    provider = new GrEmailSuggestionsProvider(appContext.restApiService);
   });
 
   test('getSuggestions', done => {
     const getSuggestedAccountsStub =
-        sinon.stub(restAPI, 'getSuggestedAccounts')
-            .returns(Promise.resolve([account1, account2]));
+        stubRestApi('getSuggestedAccounts').returns(
+            Promise.resolve([account1, account2]));
 
     provider.getSuggestions('Some input').then(res => {
       assert.deepEqual(res, [account1, account2]);
diff --git a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
index f03195d..3ce9d9d 100644
--- a/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-group-suggestions-provider/gr-group-suggestions-provider_test.js
@@ -16,12 +16,11 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GrGroupSuggestionsProvider} from './gr-group-suggestions-provider.js';
 import {appContext} from '../../services/app-context.js';
+import {stubRestApi} from '../../test/test-utils.js';
 
 suite('GrGroupSuggestionsProvider tests', () => {
-  let restAPI;
   let provider;
   const group1 = {
     name: 'Some name',
@@ -34,16 +33,13 @@
   };
 
   setup(() => {
-    stub('gr-rest-api-interface', {
-      getConfig() { return Promise.resolve({}); },
-    });
-    restAPI = appContext.restApiService;
-    provider = new GrGroupSuggestionsProvider(restAPI);
+    stubRestApi('getConfig').returns(Promise.resolve({}));
+    provider = new GrGroupSuggestionsProvider(appContext.restApiService);
   });
 
   test('getSuggestions', done => {
     const getSuggestedAccountsStub =
-        sinon.stub(restAPI, 'getSuggestedGroups')
+        stubRestApi('getSuggestedGroups')
             .returns(Promise.resolve({
               'Some name': {id: 1},
               'Other name': {id: 3, url: 'abcd'},
diff --git a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
index 2aea699..c7de24a 100644
--- a/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
+++ b/polygerrit-ui/app/scripts/gr-reviewer-suggestions-provider/gr-reviewer-suggestions-provider_test.js
@@ -16,9 +16,9 @@
  */
 
 import '../../test/common-test-setup-karma.js';
-import '../../elements/shared/gr-rest-api-interface/gr-rest-api-interface.js';
 import {GrReviewerSuggestionsProvider, SUGGESTIONS_PROVIDERS_USERS_TYPES} from './gr-reviewer-suggestions-provider.js';
 import {appContext} from '../../services/app-context.js';
+import {stubRestApi} from '../../test/test-utils.js';
 
 suite('GrReviewerSuggestionsProvider tests', () => {
   let _nextAccountId = 0;
@@ -47,7 +47,6 @@
   let suggestion1;
   let suggestion2;
   let suggestion3;
-  let restAPI;
   let provider;
 
   let redundantSuggestion1;
@@ -68,12 +67,8 @@
       },
     };
 
-    stub('gr-rest-api-interface', {
-      getLoggedIn() { return Promise.resolve(true); },
-      getConfig() { return Promise.resolve({}); },
-    });
+    stubRestApi('getConfig').returns(Promise.resolve({}));
 
-    restAPI = appContext.restApiService;
     change = {
       _number: 42,
       owner,
@@ -88,21 +83,23 @@
 
   suite('allowAnyUser set to false', () => {
     setup(done => {
-      provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+      provider = GrReviewerSuggestionsProvider.create(
+          appContext.restApiService, change._number,
           SUGGESTIONS_PROVIDERS_USERS_TYPES.REVIEWER);
       provider.init().then(done);
     });
     suite('stubbed values for _getReviewerSuggestions', () => {
+      let getChangeSuggestedReviewersStub;
       setup(() => {
-        stub('gr-rest-api-interface', {
-          getChangeSuggestedReviewers() {
-            redundantSuggestion1 = {account: existingReviewer1};
-            redundantSuggestion2 = {account: existingReviewer2};
-            redundantSuggestion3 = {account: owner};
-            return Promise.resolve([redundantSuggestion1, redundantSuggestion2,
-              redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
-          },
-        });
+        getChangeSuggestedReviewersStub =
+            stubRestApi('getChangeSuggestedReviewers').callsFake(() => {
+              redundantSuggestion1 = {account: existingReviewer1};
+              redundantSuggestion2 = {account: existingReviewer2};
+              redundantSuggestion3 = {account: owner};
+              return Promise.resolve([
+                redundantSuggestion1, redundantSuggestion2,
+                redundantSuggestion3, suggestion1, suggestion2, suggestion3]);
+            });
       });
 
       test('makeSuggestionItem formats account or group accordingly', () => {
@@ -182,26 +179,22 @@
       });
 
       test('getSuggestions short circuits when logged out', () => {
-        // API call is already stubbed.
-        const xhrSpy = restAPI.getChangeSuggestedReviewers;
         provider._loggedIn = false;
         return provider.getSuggestions('').then(() => {
-          assert.isFalse(xhrSpy.called);
+          assert.isFalse(getChangeSuggestedReviewersStub.called);
           provider._loggedIn = true;
           return provider.getSuggestions('').then(() => {
-            assert.isTrue(xhrSpy.called);
+            assert.isTrue(getChangeSuggestedReviewersStub.called);
           });
         });
       });
     });
 
     test('getChangeSuggestedReviewers is used', done => {
-      const suggestReviewerStub =
-          sinon.stub(restAPI, 'getChangeSuggestedReviewers')
-              .returns(Promise.resolve([]));
-      const suggestAccountStub =
-          sinon.stub(restAPI, 'getSuggestedAccounts')
-              .returns(Promise.resolve([]));
+      const suggestReviewerStub = stubRestApi('getChangeSuggestedReviewers')
+          .returns(Promise.resolve([]));
+      const suggestAccountStub = stubRestApi('getSuggestedAccounts')
+          .returns(Promise.resolve([]));
 
       provider.getSuggestions('').then(() => {
         assert.isTrue(suggestReviewerStub.calledOnce);
@@ -214,18 +207,17 @@
 
   suite('allowAnyUser set to true', () => {
     setup(done => {
-      provider = GrReviewerSuggestionsProvider.create(restAPI, change._number,
+      provider = GrReviewerSuggestionsProvider.create(
+          appContext.restApiService, change._number,
           SUGGESTIONS_PROVIDERS_USERS_TYPES.ANY);
       provider.init().then(done);
     });
 
     test('getSuggestedAccounts is used', done => {
-      const suggestReviewerStub =
-          sinon.stub(restAPI, 'getChangeSuggestedReviewers')
-              .returns(Promise.resolve([]));
-      const suggestAccountStub =
-          sinon.stub(restAPI, 'getSuggestedAccounts')
-              .returns(Promise.resolve([]));
+      const suggestReviewerStub = stubRestApi('getChangeSuggestedReviewers')
+          .returns(Promise.resolve([]));
+      const suggestAccountStub = stubRestApi('getSuggestedAccounts')
+          .returns(Promise.resolve([]));
 
       provider.getSuggestions('').then(() => {
         assert.isFalse(suggestReviewerStub.called);
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 8c1c4ee..beba88e 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -23,7 +23,6 @@
 import './test-router';
 import {_testOnlyInitAppContext} from './test-app-context-init';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
-import {_testOnlyResetRestApi} from '../elements/shared/gr-js-api-interface/gr-plugin-rest-api';
 import {_testOnlyResetGrRestApiSharedObjects} from '../elements/shared/gr-rest-api-interface/gr-rest-api-interface';
 import {
   cleanupTestUtils,
@@ -122,7 +121,6 @@
   // to reset this behavior if you need to test something specific.
   pl.loadPlugins([]);
   _testOnlyResetGrRestApiSharedObjects();
-  _testOnlyResetRestApi();
 });
 
 // For karma always set our implementation
diff --git a/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
new file mode 100644
index 0000000..b025fec
--- /dev/null
+++ b/polygerrit-ui/app/test/mocks/gr-rest-api_mock.ts
@@ -0,0 +1,531 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 {RestApiService} from '../../services/services/gr-rest-api/gr-rest-api';
+import {
+  AccountDetailInfo,
+  AccountExternalIdInfo,
+  AccountInfo,
+  ServerInfo,
+  ProjectInfo,
+  AccountCapabilityInfo,
+  SuggestedReviewerInfo,
+  GroupNameToGroupInfoMap,
+  ParsedJSON,
+  EditPreferencesInfo,
+  SshKeyInfo,
+  RepoName,
+  GpgKeyInfo,
+  PreferencesInfo,
+  EmailInfo,
+  ProjectAccessInfo,
+  CapabilityInfoMap,
+  ChangeInfo,
+  ProjectInfoWithName,
+  GroupInfo,
+  BranchInfo,
+  ConfigInfo,
+  EditInfo,
+  DashboardInfo,
+  ProjectAccessInfoMap,
+  IncludedInInfo,
+  CommentInfo,
+  PathToCommentsInfoMap,
+  PluginInfo,
+  DocResult,
+  ContributorAgreementInfo,
+  Password,
+  ProjectWatchInfo,
+  NameToProjectInfoMap,
+  GroupAuditEventInfo,
+  Base64FileContent,
+  TagInfo,
+  RelatedChangesInfo,
+  SubmittedTogetherInfo,
+  FilePathToDiffInfoMap,
+  BlameInfo,
+  ImagesForDiff,
+  ActionNameToActionInfoMap,
+  Hashtag,
+  FileNameToFileInfoMap,
+  TopMenuEntryInfo,
+  MergeableInfo,
+  CommitInfo,
+  GroupId,
+  GroupName,
+  UrlEncodedRepoName,
+} from '../../types/common';
+import {DiffInfo, DiffPreferencesInfo} from '../../types/diff';
+import {ParsedChangeInfo} from '../../elements/shared/gr-rest-api-interface/gr-reviewer-updates-parser';
+import {readResponsePayload} from '../../elements/shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
+import {
+  createAccountDetailWithId,
+  createChange,
+  createCommit,
+  createConfig,
+  createPreferences,
+  createServerInfo,
+} from '../test-data-generators';
+import {
+  createDefaultDiffPrefs,
+  createDefaultEditPrefs,
+} from '../../constants/constants';
+
+export const grRestApiMock: RestApiService = {
+  addAccountEmail(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  addAccountGPGKey(): Promise<Record<string, GpgKeyInfo>> {
+    return Promise.resolve({});
+  },
+  addAccountSSHKey(): Promise<SshKeyInfo> {
+    throw new Error('addAccountSSHKey() not implemented by RestApiMock.');
+  },
+  addToAttentionSet(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  applyFixSuggestion(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  awaitPendingDiffDrafts(): Promise<void> {
+    return Promise.resolve();
+  },
+  confirmEmail(): Promise<string | null> {
+    return Promise.resolve('');
+  },
+  createChange(): Promise<ChangeInfo | undefined> {
+    throw new Error('createChange() not implemented by RestApiMock.');
+  },
+  createGroup(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  createRepo(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  createRepoBranch(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  createRepoTag(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteAccountEmail(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteAccountGPGKey(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteAccountIdentity(): Promise<unknown> {
+    return Promise.resolve(new Response());
+  },
+  deleteAccountSSHKey(): void {},
+  deleteAssignee(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteChangeCommitMessage(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteComment(): Promise<CommentInfo> {
+    throw new Error('deleteComment() not implemented by RestApiMock.');
+  },
+  deleteDiffDraft(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteDraftComments(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteFileInChangeEdit(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  deleteGroupMember(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteIncludedGroup(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteRepoBranches(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteRepoTags(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteVote(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  deleteWatchedProjects(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  executeChangeAction(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  generateAccountHttpPassword(): Promise<Password> {
+    return Promise.resolve('asdf');
+  },
+  getAccount(): Promise<AccountDetailInfo | undefined> {
+    return Promise.resolve(createAccountDetailWithId(1));
+  },
+  getAccountAgreements(): Promise<ContributorAgreementInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getAccountCapabilities(): Promise<AccountCapabilityInfo | undefined> {
+    return Promise.resolve({});
+  },
+  getAccountDetails(): Promise<AccountDetailInfo | undefined> {
+    return Promise.resolve(createAccountDetailWithId(1));
+  },
+  getAccountEmails(): Promise<EmailInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getAccountGPGKeys(): Promise<Record<string, GpgKeyInfo>> {
+    return Promise.resolve({});
+  },
+  getAccountGroups(): Promise<GroupInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getAccountSSHKeys(): Promise<SshKeyInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getAccountStatus(): Promise<string | undefined> {
+    return Promise.resolve('');
+  },
+  getAvatarChangeUrl(): Promise<string | undefined> {
+    return Promise.resolve('');
+  },
+  getBlame(): Promise<BlameInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getCapabilities(): Promise<CapabilityInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getChange(): Promise<ChangeInfo | null> {
+    throw new Error('getChange() not implemented by RestApiMock.');
+  },
+  getChangeActionURL(): Promise<string> {
+    return Promise.resolve('');
+  },
+  getChangeCherryPicks(): Promise<ChangeInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getChangeCommitInfo(): Promise<CommitInfo | undefined> {
+    return Promise.resolve(createCommit());
+  },
+  getChangeConflicts(): Promise<ChangeInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getChangeDetail(): Promise<ParsedChangeInfo | null | undefined> {
+    return Promise.resolve(createChange() as ParsedChangeInfo);
+  },
+  getChangeEdit(): Promise<false | EditInfo | undefined> {
+    return Promise.resolve(false);
+  },
+  getChangeFiles(): Promise<FileNameToFileInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getChangeIncludedIn(): Promise<IncludedInInfo | undefined> {
+    throw new Error('getChangeIncludedIn() not implemented by RestApiMock.');
+  },
+  getChangeOrEditFiles(): Promise<FileNameToFileInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getChangeRevisionActions(): Promise<ActionNameToActionInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getChangeSuggestedCCs(): Promise<SuggestedReviewerInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getChangeSuggestedReviewers(): Promise<SuggestedReviewerInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getChanges() {
+    return Promise.resolve([]);
+  },
+  getChangesSubmittedTogether(): Promise<SubmittedTogetherInfo | undefined> {
+    throw new Error('getChangesSubmittedTogether() not implemented by mock.');
+  },
+  getChangesWithSameTopic(): Promise<ChangeInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getConfig(): Promise<ServerInfo | undefined> {
+    return Promise.resolve(createServerInfo());
+  },
+  getDashboard(): Promise<DashboardInfo | undefined> {
+    throw new Error('getDashboard() not implemented by RestApiMock.');
+  },
+  getDefaultPreferences(): Promise<PreferencesInfo | undefined> {
+    throw new Error('getDefaultPreferences() not implemented by RestApiMock.');
+  },
+  getDiff(): Promise<DiffInfo | undefined> {
+    throw new Error('getDiff() not implemented by RestApiMock.');
+  },
+  getDiffChangeDetail(): Promise<ChangeInfo | undefined | null> {
+    throw new Error('getDiffChangeDetail() not implemented by RestApiMock.');
+  },
+  getDiffComments() {
+    throw new Error('getDiffComments() not implemented by RestApiMock.');
+  },
+  getDiffDrafts() {
+    throw new Error('getDiffDrafts() not implemented by RestApiMock.');
+  },
+  getDiffPreferences(): Promise<DiffPreferencesInfo | undefined> {
+    return Promise.resolve(createDefaultDiffPrefs());
+  },
+  getDiffRobotComments() {
+    throw new Error('getDiffRobotComments() not implemented by RestApiMock.');
+  },
+  getDocumentationSearches(): Promise<DocResult[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getEditPreferences(): Promise<EditPreferencesInfo | undefined> {
+    return Promise.resolve(createDefaultEditPrefs());
+  },
+  getExternalIds(): Promise<AccountExternalIdInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getFileContent(): Promise<Response | Base64FileContent | undefined> {
+    return Promise.resolve(new Response());
+  },
+  getFromProjectLookup(): Promise<RepoName | undefined> {
+    throw new Error('getFromProjectLookup() not implemented by RestApiMock.');
+  },
+  getGroupAuditLog(): Promise<GroupAuditEventInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getGroupConfig(id: GroupId | GroupName): Promise<GroupInfo | undefined> {
+    return Promise.resolve({
+      id: id as GroupId,
+    });
+  },
+  getGroupMembers(): Promise<AccountInfo[]> {
+    return Promise.resolve([]);
+  },
+  getGroups(): Promise<GroupNameToGroupInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getImagesForDiff(): Promise<ImagesForDiff> {
+    throw new Error('getImagesForDiff() not implemented by RestApiMock.');
+  },
+  getIncludedGroup(): Promise<GroupInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getIsAdmin(): Promise<boolean | undefined> {
+    return Promise.resolve(false);
+  },
+  getIsGroupOwner(): Promise<boolean> {
+    return Promise.resolve(false);
+  },
+  getLoggedIn(): Promise<boolean> {
+    return Promise.resolve(true);
+  },
+  getMergeable(): Promise<MergeableInfo | undefined> {
+    throw new Error('getMergeable() not implemented by RestApiMock.');
+  },
+  getPlugins(): Promise<{[p: string]: PluginInfo} | undefined> {
+    return Promise.resolve({});
+  },
+  getPortedComments(): Promise<PathToCommentsInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getPortedDrafts(): Promise<PathToCommentsInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getPreferences(): Promise<PreferencesInfo | undefined> {
+    return Promise.resolve(createPreferences());
+  },
+  getProjectConfig(): Promise<ConfigInfo | undefined> {
+    return Promise.resolve(createConfig());
+  },
+  getRelatedChanges(): Promise<RelatedChangesInfo | undefined> {
+    return Promise.resolve({changes: []});
+  },
+  getRepo(repo: RepoName): Promise<ProjectInfo | undefined> {
+    return Promise.resolve({
+      id: (repo as string) as UrlEncodedRepoName,
+      name: repo,
+    });
+  },
+  getRepoAccess(): Promise<ProjectAccessInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getRepoAccessRights(): Promise<ProjectAccessInfo | undefined> {
+    return Promise.resolve(undefined);
+  },
+  getRepoBranches(): Promise<BranchInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getRepoDashboards(): Promise<DashboardInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getRepoTags(): Promise<TagInfo[]> {
+    return Promise.resolve([]);
+  },
+  getRepos(): Promise<ProjectInfoWithName[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getResponseObject(response: Response): Promise<ParsedJSON> {
+    return readResponsePayload(response).then(payload => payload.parsed);
+  },
+  getReviewedFiles(): Promise<string[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getRobotCommentFixPreview(): Promise<FilePathToDiffInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getSuggestedAccounts(): Promise<AccountInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getSuggestedGroups(): Promise<GroupNameToGroupInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getSuggestedProjects(): Promise<NameToProjectInfoMap | undefined> {
+    return Promise.resolve({});
+  },
+  getTopMenus(): Promise<TopMenuEntryInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  getVersion(): Promise<string | undefined> {
+    return Promise.resolve('');
+  },
+  getWatchedProjects(): Promise<ProjectWatchInfo[] | undefined> {
+    return Promise.resolve([]);
+  },
+  hasPendingDiffDrafts(): number {
+    return 0;
+  },
+  invalidateAccountsCache(): void {},
+  invalidateGroupsCache(): void {},
+  invalidateReposCache(): void {},
+  probePath(): Promise<boolean> {
+    return Promise.resolve(true);
+  },
+  putChangeCommitMessage(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  queryChangeFiles(): Promise<string[] | undefined> {
+    return Promise.resolve([]);
+  },
+  removeChangeReviewer(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  removeFromAttentionSet(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  renameFileInChangeEdit(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  restoreFileInChangeEdit(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  runRepoGC(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveAccountAgreement(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveChangeEdit(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveChangeReview() {
+    return Promise.resolve(new Response());
+  },
+  saveChangeReviewed(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  saveChangeStarred(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveDiffDraft(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveDiffPreferences(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveEditPreferences(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveFileReviewed(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveFileUploadChangeEdit(): Promise<Response | undefined> {
+    return Promise.resolve(new Response());
+  },
+  saveGroupDescription(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveGroupMember(): Promise<AccountInfo> {
+    return Promise.resolve({});
+  },
+  saveGroupName(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveGroupOptions(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveGroupOwner(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveIncludedGroup(): Promise<GroupInfo | undefined> {
+    throw new Error('saveIncludedGroup() not implemented by RestApiMock.');
+  },
+  savePreferences(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveRepoConfig(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  saveWatchedProjects(): Promise<ProjectWatchInfo[]> {
+    return Promise.resolve([]);
+  },
+  send() {
+    return Promise.resolve(new Response());
+  },
+  setAccountDisplayName(): Promise<void> {
+    return Promise.resolve();
+  },
+  setAccountName(): Promise<void> {
+    return Promise.resolve();
+  },
+  setAccountStatus(): Promise<void> {
+    return Promise.resolve();
+  },
+  setAccountUsername(): Promise<void> {
+    return Promise.resolve();
+  },
+  setAssignee(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  setChangeHashtag(): Promise<Hashtag[]> {
+    return Promise.resolve([]);
+  },
+  setChangeTopic(): Promise<string> {
+    return Promise.resolve('');
+  },
+  setDescription(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  setInProjectLookup(): void {},
+  setPreferredAccountEmail(): Promise<void> {
+    return Promise.resolve();
+  },
+  setRepoAccessRights(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+  setRepoAccessRightsForReview(): Promise<ChangeInfo> {
+    throw new Error('setRepoAccessRightsForReview() not implemented by mock.');
+  },
+  setRepoHead(): Promise<Response> {
+    return Promise.resolve(new Response());
+  },
+};
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 7f19903..57afd8a 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -19,6 +19,7 @@
 import {initAppContext} from '../services/app-context-init';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
 import {AppContext, appContext} from '../services/app-context';
+import {grRestApiMock} from './mocks/gr-rest-api_mock';
 
 export function _testOnlyInitAppContext() {
   initAppContext();
@@ -34,4 +35,5 @@
     });
   }
   setMock('reportingService', grReportingMock);
+  setMock('restApiService', grRestApiMock);
 }
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 444d0bf..8137712 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -404,6 +404,7 @@
   };
 }
 
+// TODO: Maybe reconcile with createDefaultPreferences() in constants.ts.
 export function createPreferences(): PreferencesInfo {
   return {
     changes_per_page: 10,
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 97c220d..2115f90 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -16,12 +16,13 @@
  */
 import '../types/globals';
 import {_testOnly_resetPluginLoader} from '../elements/shared/gr-js-api-interface/gr-plugin-loader';
-import {testOnly_resetInternalState} from '../elements/shared/gr-js-api-interface/gr-api-utils';
 import {_testOnly_resetEndpoints} from '../elements/shared/gr-js-api-interface/gr-plugin-endpoints';
 import {
   _testOnly_getShortcutManagerInstance,
   Shortcut,
 } from '../mixins/keyboard-shortcut-mixin/keyboard-shortcut-mixin';
+import {appContext} from '../services/app-context';
+import {RestApiService} from '../services/services/gr-rest-api/gr-rest-api';
 
 export interface MockPromise extends Promise<unknown> {
   resolve: (value?: unknown) => void;
@@ -87,7 +88,6 @@
 // Provide reset plugins function to clear installed plugins between tests.
 // No gr-app found (running tests)
 export const resetPlugins = () => {
-  testOnly_resetInternalState();
   _testOnly_resetEndpoints();
   const pl = _testOnly_resetPluginLoader();
   pl.loadPlugins([]);
@@ -135,6 +135,14 @@
   registerTestCleanup(() => (window.CANONICAL_PATH = originalCanonicalPath));
 }
 
+export function stubRestApi<K extends keyof RestApiService>(method: K) {
+  return sinon.stub(appContext.restApiService, method);
+}
+
+export function spyRestApi<K extends keyof RestApiService>(method: K) {
+  return sinon.spy(appContext.restApiService, method);
+}
+
 /**
  * Forcing an opacity of 0 onto the ironOverlayBackdrop is required, because
  * otherwise the backdrop stays around in the DOM for too long waiting for
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 5574add..beb86f4 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -1721,8 +1721,8 @@
   flushCaches?: boolean;
   killTask?: boolean;
   maintainServer?: boolean;
-  priority: UserPriority;
-  queryLimit: QueryLimitInfo;
+  priority?: UserPriority;
+  queryLimit?: QueryLimitInfo;
   runAs?: boolean;
   runGC?: boolean;
   streamEvents?: boolean;