diff --git a/lib/manager/gradle-wrapper/__snapshots__/artifacts.spec.ts.snap b/lib/manager/gradle-wrapper/__snapshots__/artifacts.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..a95465409dc1a15a38ab0a5eb2c18354a23f3451
--- /dev/null
+++ b/lib/manager/gradle-wrapper/__snapshots__/artifacts.spec.ts.snap
@@ -0,0 +1,67 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`manager/gradle-wrapper/artifacts gradlew failed 1`] = `
+Array [
+  Object {
+    "cmd": "<gradlew> wrapper --gradle-version 5.6.4",
+    "options": Object {
+      "cwd": "/root/project/lib/manager/gradle-wrapper/__fixtures__/testFiles",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`manager/gradle-wrapper/artifacts replaces existing value 1`] = `
+Array [
+  Object {
+    "cmd": "<gradlew> wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip",
+    "options": Object {
+      "cwd": "/root/project/lib/manager/gradle-wrapper/__fixtures__/testFiles",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+]
+`;
+
+exports[`manager/gradle-wrapper/artifacts updates distributionSha256Sum 1`] = `
+Array [
+  Object {
+    "cmd": "<gradlew> wrapper --gradle-distribution-url https://services.gradle.org/distributions/gradle-6.3-bin.zip --gradle-distribution-sha256-sum 038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768",
+    "options": Object {
+      "cwd": "/root/project/lib/manager/gradle-wrapper/__fixtures__/testFiles",
+      "encoding": "utf-8",
+      "env": Object {
+        "HOME": "/home/user",
+        "HTTPS_PROXY": "https://example.com",
+        "HTTP_PROXY": "http://example.com",
+        "LANG": "en_US.UTF-8",
+        "LC_ALL": "en_US",
+        "NO_PROXY": "localhost",
+        "PATH": "/tmp/path",
+      },
+      "timeout": 900000,
+    },
+  },
+]
+`;
diff --git a/lib/manager/gradle-wrapper/artifacts-real.spec.ts b/lib/manager/gradle-wrapper/artifacts-real.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5dafe01c6a41de2d784ed9e330929a9211cae12f
--- /dev/null
+++ b/lib/manager/gradle-wrapper/artifacts-real.spec.ts
@@ -0,0 +1,289 @@
+import { resolve } from 'path';
+import { readFile, readFileSync } from 'fs-extra';
+import Git from 'simple-git/promise';
+import * as httpMock from '../../../test/httpMock';
+import { getName, partial, platform } from '../../../test/util';
+import { setUtilConfig } from '../../util';
+import * as runCache from '../../util/cache/run';
+import { ifSystemSupportsGradle } from '../gradle/__testutil__/gradle';
+import * as dcUpdate from '.';
+
+const fixtures = resolve(__dirname, './__fixtures__');
+const config = {
+  localDir: resolve(fixtures, './testFiles'),
+  toVersion: '5.6.4',
+};
+
+function readString(...paths: string[]): Promise<string> {
+  return readFile(resolve(fixtures, ...paths), 'utf8');
+}
+
+function readBinSync(...paths: string[]): Buffer {
+  return readFileSync(resolve(fixtures, ...paths));
+}
+
+function compareFile(file: string, path: string) {
+  expect(readBinSync(`./testFiles/${file}`)).toEqual(
+    readBinSync(`./${path}/${file}`)
+  );
+}
+
+describe(getName(__filename), () => {
+  ifSystemSupportsGradle(6).describe('real tests', () => {
+    jest.setTimeout(60 * 1000);
+
+    beforeEach(async () => {
+      jest.resetAllMocks();
+      await setUtilConfig(config);
+      httpMock.setup();
+      runCache.clear();
+    });
+
+    afterEach(async () => {
+      await Git(fixtures).checkout(['HEAD', '--', '.']);
+      httpMock.reset();
+    });
+
+    it('replaces existing value', async () => {
+      platform.getRepoStatus.mockResolvedValue({
+        modified: [
+          'gradle/wrapper/gradle-wrapper.properties',
+          'gradle/wrapper/gradle-wrapper.jar',
+          'gradlew',
+          'gradlew.bat',
+        ],
+      } as Git.StatusResult);
+
+      const res = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: await readString(
+          `./expectedFiles/gradle/wrapper/gradle-wrapper.properties`
+        ),
+        config: { ...config, toVersion: '6.3' },
+      });
+
+      expect(res).toEqual(
+        [
+          'gradle/wrapper/gradle-wrapper.properties',
+          'gradle/wrapper/gradle-wrapper.jar',
+          'gradlew',
+          'gradlew.bat',
+        ].map((fileProjectPath) => {
+          return {
+            file: {
+              name: fileProjectPath,
+              contents: readBinSync(`./testFiles/${fileProjectPath}`),
+            },
+          };
+        })
+      );
+
+      [
+        'gradle/wrapper/gradle-wrapper.properties',
+        'gradle/wrapper/gradle-wrapper.jar',
+        'gradlew',
+        'gradlew.bat',
+      ].forEach((file) => {
+        compareFile(file, 'expectedFiles');
+      });
+    });
+
+    it('updates from version', async () => {
+      platform.getRepoStatus.mockResolvedValueOnce(
+        partial<Git.StatusResult>({
+          modified: ['gradle/wrapper/gradle-wrapper.properties'],
+        })
+      );
+
+      const result = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: ``,
+        config: { ...config, toVersion: '6.3' },
+      });
+
+      expect(result).toHaveLength(1);
+      expect(result[0].artifactError).toBeUndefined();
+
+      compareFile('gradle/wrapper/gradle-wrapper.properties', 'expectedFiles');
+    });
+
+    it('up to date', async () => {
+      platform.getRepoStatus.mockResolvedValue({
+        modified: [],
+      } as Git.StatusResult);
+
+      const res = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: await readString(
+          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
+        ),
+        config,
+      });
+
+      expect(res).toEqual([]);
+
+      // 5.6.4 => 5.6.4 (updates execs)
+      // 6.3 => (5.6.4) (downgrades execs)
+      // looks like a bug in Gradle
+      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
+        compareFile(file, 'testFiles-copy');
+      });
+    });
+
+    it('getRepoStatus fails', async () => {
+      platform.getRepoStatus.mockImplementation(() => {
+        throw new Error('failed');
+      });
+
+      const res = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: await readString(
+          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
+        ),
+        config,
+      });
+
+      expect(res[0].artifactError.lockFile).toEqual(
+        'gradle/wrapper/gradle-wrapper.properties'
+      );
+      expect(res[0].artifactError.stderr).toEqual('failed');
+
+      // 5.6.4 => 5.6.4 (updates execs) - unexpected behavior (looks like a bug in Gradle)
+      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
+        compareFile(file, 'testFiles-copy');
+      });
+    });
+
+    it('gradlew failed', async () => {
+      const cfg = { ...config, localDir: resolve(fixtures, './wrongCmd') };
+
+      await setUtilConfig(cfg);
+      const res = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: await readString(
+          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
+        ),
+        config: cfg,
+      });
+
+      expect(res[0].artifactError.lockFile).toEqual(
+        'gradle/wrapper/gradle-wrapper.properties'
+      );
+      expect(res[0].artifactError.stderr).not.toBeNull();
+      expect(res[0].artifactError.stderr).not.toEqual('');
+
+      // 5.6.4 => 5.6.4 (updates execs) - unexpected behavior (looks like a bug in Gradle)
+      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
+        compareFile(file, 'testFiles-copy');
+      });
+    });
+
+    it('gradlew not found', async () => {
+      const res = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: undefined,
+        config: {
+          localDir: 'some-dir',
+        },
+      });
+
+      expect(res).toBeNull();
+    });
+
+    it('updates distributionSha256Sum', async () => {
+      httpMock
+        .scope('https://services.gradle.org')
+        .get('/distributions/gradle-6.3-bin.zip.sha256')
+        .reply(
+          200,
+          '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768'
+        );
+
+      platform.getRepoStatus.mockResolvedValueOnce(
+        partial<Git.StatusResult>({
+          modified: ['gradle/wrapper/gradle-wrapper.properties'],
+        })
+      );
+
+      const newContent = await readString(`./gradle-wrapper-sum.properties`);
+
+      const result = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: newContent.replace(
+          '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768',
+          '1f3067073041bc44554d0efe5d402a33bc3d3c93cc39ab684f308586d732a80d'
+        ),
+        config: {
+          ...config,
+          toVersion: '6.3',
+          currentValue: '5.6.4',
+        },
+      });
+
+      expect(result).toHaveLength(1);
+      expect(result[0].artifactError).toBeUndefined();
+
+      expect(
+        await readString(
+          config.localDir,
+          `./gradle/wrapper/gradle-wrapper.properties`
+        )
+      ).toEqual(newContent);
+
+      expect(httpMock.getTrace()).toEqual([
+        {
+          headers: {
+            'accept-encoding': 'gzip, deflate',
+            host: 'services.gradle.org',
+            'user-agent': 'https://github.com/renovatebot/renovate',
+          },
+          method: 'GET',
+          url:
+            'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
+        },
+      ]);
+    });
+
+    it('distributionSha256Sum 404', async () => {
+      httpMock
+        .scope('https://services.gradle.org')
+        .get('/distributions/gradle-6.3-bin.zip.sha256')
+        .reply(404);
+
+      const result = await dcUpdate.updateArtifacts({
+        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
+        updatedDeps: [],
+        newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`,
+        config,
+      });
+
+      expect(result).toEqual([
+        {
+          artifactError: {
+            lockFile: 'gradle/wrapper/gradle-wrapper.properties',
+            stderr: 'Response code 404 (Not Found)',
+          },
+        },
+      ]);
+      expect(httpMock.getTrace()).toEqual([
+        {
+          headers: {
+            'accept-encoding': 'gzip, deflate',
+            host: 'services.gradle.org',
+            'user-agent': 'https://github.com/renovatebot/renovate',
+          },
+          method: 'GET',
+          url:
+            'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
+        },
+      ]);
+    });
+  });
+});
diff --git a/lib/manager/gradle-wrapper/artifacts.spec.ts b/lib/manager/gradle-wrapper/artifacts.spec.ts
index c6994b2843d54b9fd2a069115022fd0431a54cbc..8c863ac49ca9f3bc5857dca45d738ac8939b6762 100644
--- a/lib/manager/gradle-wrapper/artifacts.spec.ts
+++ b/lib/manager/gradle-wrapper/artifacts.spec.ts
@@ -1,297 +1,204 @@
+import { exec as _exec } from 'child_process';
 import { resolve } from 'path';
-import { readFile, readFileSync } from 'fs-extra';
+import { readFile } from 'fs-extra';
 import Git from 'simple-git/promise';
+import { envMock, mockExecAll } from '../../../test/execUtil';
 import * as httpMock from '../../../test/httpMock';
 import {
-  bufferSerializer,
+  addReplacingSerializer,
+  env,
+  fs,
   getName,
   partial,
   platform,
 } from '../../../test/util';
 import { setUtilConfig } from '../../util';
-import * as runCache from '../../util/cache/run';
-import { ifSystemSupportsGradle } from '../gradle/__testutil__/gradle';
+import { BinarySource } from '../../util/exec/common';
+import { resetPrefetchedImages } from '../../util/exec/docker';
 import * as dcUpdate from '.';
 
+jest.mock('child_process');
+jest.mock('../../util/fs');
+jest.mock('../../util/exec/env');
+
+const exec: jest.Mock<typeof _exec> = _exec as any;
 const fixtures = resolve(__dirname, './__fixtures__');
 const config = {
   localDir: resolve(fixtures, './testFiles'),
   toVersion: '5.6.4',
 };
+const dockerConfig = { ...config, binarySource: BinarySource.Docker };
 
-jest.mock('../../platform');
-
-expect.addSnapshotSerializer(bufferSerializer());
+addReplacingSerializer('gradlew.bat', '<gradlew>');
+addReplacingSerializer('./gradlew', '<gradlew>');
 
 function readString(...paths: string[]): Promise<string> {
   return readFile(resolve(fixtures, ...paths), 'utf8');
 }
 
-function readBinSync(...paths: string[]): Buffer {
-  return readFileSync(resolve(fixtures, ...paths));
-}
-
-function compareFile(file: string, path: string) {
-  expect(readBinSync(`./testFiles/${file}`)).toEqual(
-    readBinSync(`./${path}/${file}`)
-  );
-}
-
 describe(getName(__filename), () => {
-  ifSystemSupportsGradle(6).describe('real tests', () => {
-    jest.setTimeout(60 * 1000);
-
-    beforeEach(async () => {
-      jest.resetAllMocks();
-      await setUtilConfig(config);
-      httpMock.setup();
-      runCache.clear();
+  beforeEach(async () => {
+    jest.resetAllMocks();
+    httpMock.setup();
+
+    env.getChildProcessEnv.mockReturnValue({
+      ...envMock.basic,
+      LANG: 'en_US.UTF-8',
+      LC_ALL: 'en_US',
     });
 
-    afterEach(async () => {
-      await Git(fixtures)?.checkout(['HEAD', '--', '.']);
-      httpMock.reset();
-    });
-
-    it('replaces existing value', async () => {
-      platform.getRepoStatus.mockResolvedValue({
-        modified: [
-          'gradle/wrapper/gradle-wrapper.properties',
-          'gradle/wrapper/gradle-wrapper.jar',
-          'gradlew',
-          'gradlew.bat',
-        ],
-      } as Git.StatusResult);
+    await setUtilConfig(config);
+    resetPrefetchedImages();
 
-      const res = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: await readString(
-          `./expectedFiles/gradle/wrapper/gradle-wrapper.properties`
-        ),
-        config: { ...config, toVersion: '6.3' },
-      });
+    fs.readLocalFile.mockResolvedValue('test');
+  });
 
-      expect(res).toEqual(
-        [
-          'gradle/wrapper/gradle-wrapper.properties',
-          'gradle/wrapper/gradle-wrapper.jar',
-          'gradlew',
-          'gradlew.bat',
-        ].map((fileProjectPath) => {
-          return {
-            file: {
-              name: fileProjectPath,
-              contents: readBinSync(`./testFiles/${fileProjectPath}`),
-            },
-          };
-        })
-      );
+  afterEach(() => {
+    httpMock.reset();
+  });
 
-      [
+  it('replaces existing value', async () => {
+    platform.getRepoStatus.mockResolvedValue({
+      modified: [
         'gradle/wrapper/gradle-wrapper.properties',
-        'gradle/wrapper/gradle-wrapper.jar',
         'gradlew',
         'gradlew.bat',
-      ].forEach((file) => {
-        compareFile(file, 'expectedFiles');
-      });
+      ],
+    } as Git.StatusResult);
+
+    const execSnapshots = mockExecAll(exec);
+
+    const res = await dcUpdate.updateArtifacts({
+      packageFileName: 'gradle-wrapper.properties',
+      updatedDeps: [],
+      newPackageFileContent: await readString(
+        `./expectedFiles/gradle/wrapper/gradle-wrapper.properties`
+      ),
+      config: { ...config, toVersion: '6.3' },
     });
 
-    it('updates from version', async () => {
-      platform.getRepoStatus.mockResolvedValueOnce(
-        partial<Git.StatusResult>({
-          modified: ['gradle/wrapper/gradle-wrapper.properties'],
-        })
-      );
-
-      const result = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: ``,
-        config: { ...config, toVersion: '6.3' },
-      });
-
-      expect(result).toHaveLength(1);
-      expect(result[0].artifactError).toBeUndefined();
-
-      compareFile('gradle/wrapper/gradle-wrapper.properties', 'expectedFiles');
-    });
-
-    it('up to date', async () => {
-      platform.getRepoStatus.mockResolvedValue({
-        modified: [],
-      } as Git.StatusResult);
-
-      const res = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: await readString(
-          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
-        ),
-        config,
-      });
-
-      expect(res).toEqual([]);
+    expect(res).toEqual(
+      [
+        'gradle/wrapper/gradle-wrapper.properties',
+        'gradlew',
+        'gradlew.bat',
+      ].map((fileProjectPath) => {
+        return {
+          file: {
+            name: fileProjectPath,
+            contents: 'test',
+          },
+        };
+      })
+    );
+    expect(execSnapshots).toMatchSnapshot();
+  });
 
-      // 5.6.4 => 5.6.4 (updates execs)
-      // 6.3 => (5.6.4) (downgrades execs)
-      // looks like a bug in Gradle
-      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
-        compareFile(file, 'testFiles-copy');
-      });
+  it('gradlew not found', async () => {
+    const res = await dcUpdate.updateArtifacts({
+      packageFileName: 'gradle-wrapper.properties',
+      updatedDeps: [],
+      newPackageFileContent: undefined,
+      config: {
+        localDir: 'some-dir',
+      },
     });
 
-    it('getRepoStatus fails', async () => {
-      platform.getRepoStatus.mockImplementation(() => {
-        throw new Error('failed');
-      });
-
-      const res = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: await readString(
-          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
-        ),
-        config,
-      });
-
-      expect(res[0].artifactError.lockFile).toEqual(
-        'gradle/wrapper/gradle-wrapper.properties'
-      );
-      expect(res[0].artifactError.stderr).toEqual('failed');
+    expect(res).toBeNull();
+  });
 
-      // 5.6.4 => 5.6.4 (updates execs) - unexpected behavior (looks like a bug in Gradle)
-      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
-        compareFile(file, 'testFiles-copy');
-      });
+  it('gradlew failed', async () => {
+    const execSnapshots = mockExecAll(exec, new Error('failed'));
+    platform.getRepoStatus.mockResolvedValueOnce(
+      partial<Git.StatusResult>({
+        modified: [],
+      })
+    );
+    const res = await dcUpdate.updateArtifacts({
+      packageFileName: 'gradle-wrapper.properties',
+      updatedDeps: [],
+      newPackageFileContent: '',
+      config,
     });
 
-    it('gradlew failed', async () => {
-      const cfg = { ...config, localDir: resolve(fixtures, './wrongCmd') };
-
-      await setUtilConfig(cfg);
-      const res = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: await readString(
-          `./testFiles/gradle/wrapper/gradle-wrapper.properties`
-        ),
-        config: cfg,
-      });
+    expect(execSnapshots).toMatchSnapshot();
+    expect(res).toEqual([]);
+  });
 
-      expect(res[0].artifactError.lockFile).toEqual(
-        'gradle-wrapper.properties'
+  it.only('updates distributionSha256Sum', async () => {
+    httpMock
+      .scope('https://services.gradle.org')
+      .get('/distributions/gradle-6.3-bin.zip.sha256')
+      .reply(
+        200,
+        '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768'
       );
-      expect(res[0].artifactError.stderr).not.toBeNull();
-      expect(res[0].artifactError.stderr).not.toEqual('');
 
-      // 5.6.4 => 5.6.4 (updates execs) - unexpected behavior (looks like a bug in Gradle)
-      ['gradle/wrapper/gradle-wrapper.properties'].forEach((file) => {
-        compareFile(file, 'testFiles-copy');
-      });
-    });
+    platform.getRepoStatus.mockResolvedValueOnce(
+      partial<Git.StatusResult>({
+        modified: ['gradle/wrapper/gradle-wrapper.properties'],
+      })
+    );
 
-    it('gradlew not found', async () => {
-      const res = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: undefined,
-        config: {
-          localDir: 'some-dir',
-        },
-      });
+    const execSnapshots = mockExecAll(exec);
 
-      expect(res).toBeNull();
+    const result = await dcUpdate.updateArtifacts({
+      packageFileName: 'gradle-wrapper.properties',
+      updatedDeps: [],
+      newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`,
+      config: dockerConfig,
     });
 
-    it('updates distributionSha256Sum', async () => {
-      httpMock
-        .scope('https://services.gradle.org')
-        .get('/distributions/gradle-6.3-bin.zip.sha256')
-        .reply(
-          200,
-          '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768'
-        );
+    expect(result).toHaveLength(1);
+    expect(result[0].artifactError).toBeUndefined();
 
-      platform.getRepoStatus.mockResolvedValueOnce(
-        partial<Git.StatusResult>({
-          modified: ['gradle/wrapper/gradle-wrapper.properties'],
-        })
-      );
-      const newContent = await readString(`./gradle-wrapper-sum.properties`);
-
-      const result = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: newContent.replace(
-          '038794feef1f4745c6347107b6726279d1c824f3fc634b60f86ace1e9fbd1768',
-          '1f3067073041bc44554d0efe5d402a33bc3d3c93cc39ab684f308586d732a80d'
-        ),
-        config: {
-          ...config,
-          toVersion: '6.3',
-          currentValue: '5.6.4',
+    expect(execSnapshots).toMatchSnapshot();
+    expect(httpMock.getTrace()).toEqual([
+      {
+        headers: {
+          'accept-encoding': 'gzip, deflate',
+          host: 'services.gradle.org',
+          'user-agent': 'https://github.com/renovatebot/renovate',
         },
-      });
-
-      expect(result).toHaveLength(1);
-      expect(result[0].artifactError).toBeUndefined();
-
-      expect(
-        await readString(
-          config.localDir,
-          `./gradle/wrapper/gradle-wrapper.properties`
-        )
-      ).toEqual(newContent);
+        method: 'GET',
+        url:
+          'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
+      },
+    ]);
+  });
 
-      expect(httpMock.getTrace()).toEqual([
-        {
-          headers: {
-            'accept-encoding': 'gzip, deflate',
-            host: 'services.gradle.org',
-            'user-agent': 'https://github.com/renovatebot/renovate',
-          },
-          method: 'GET',
-          url:
-            'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
-        },
-      ]);
+  it('distributionSha256Sum 404', async () => {
+    httpMock
+      .scope('https://services.gradle.org')
+      .get('/distributions/gradle-6.3-bin.zip.sha256')
+      .reply(404);
+
+    const result = await dcUpdate.updateArtifacts({
+      packageFileName: 'gradle-wrapper.properties',
+      updatedDeps: [],
+      newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`,
+      config,
     });
 
-    it('distributionSha256Sum 404', async () => {
-      httpMock
-        .scope('https://services.gradle.org')
-        .get('/distributions/gradle-6.3-bin.zip.sha256')
-        .reply(404);
-
-      const result = await dcUpdate.updateArtifacts({
-        packageFileName: 'gradle/wrapper/gradle-wrapper.properties',
-        updatedDeps: [],
-        newPackageFileContent: `distributionSha256Sum=336b6898b491f6334502d8074a6b8c2d73ed83b92123106bd4bf837f04111043\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-6.3-bin.zip`,
-        config,
-      });
-
-      expect(result).toEqual([
-        {
-          artifactError: {
-            lockFile: 'gradle/wrapper/gradle-wrapper.properties',
-            stderr: 'Response code 404 (Not Found)',
-          },
+    expect(result).toEqual([
+      {
+        artifactError: {
+          lockFile: 'gradle-wrapper.properties',
+          stderr: 'Response code 404 (Not Found)',
         },
-      ]);
-      expect(httpMock.getTrace()).toEqual([
-        {
-          headers: {
-            'accept-encoding': 'gzip, deflate',
-            host: 'services.gradle.org',
-            'user-agent': 'https://github.com/renovatebot/renovate',
-          },
-          method: 'GET',
-          url:
-            'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
+      },
+    ]);
+    expect(httpMock.getTrace()).toEqual([
+      {
+        headers: {
+          'accept-encoding': 'gzip, deflate',
+          host: 'services.gradle.org',
+          'user-agent': 'https://github.com/renovatebot/renovate',
         },
-      ]);
-    });
+        method: 'GET',
+        url:
+          'https://services.gradle.org/distributions/gradle-6.3-bin.zip.sha256',
+      },
+    ]);
   });
 });
diff --git a/lib/manager/gradle-wrapper/artifacts.ts b/lib/manager/gradle-wrapper/artifacts.ts
index f7d75d262b0bfca0cc51d7866d869e5cf63d03cc..e396226e5c2b82e10526b8404c8d4e74877ff868 100644
--- a/lib/manager/gradle-wrapper/artifacts.ts
+++ b/lib/manager/gradle-wrapper/artifacts.ts
@@ -1,4 +1,3 @@
-/* istanbul ignore file */
 import { resolve } from 'path';
 import * as fs from 'fs-extra';
 import Git from 'simple-git/promise';
diff --git a/package.json b/package.json
index ccb040dd29350fc46ff48eb320c014be03d42901..260603a8cc2687c3e1844017a460092a0031fdf5 100644
--- a/package.json
+++ b/package.json
@@ -184,6 +184,7 @@
     "@babel/plugin-syntax-dynamic-import": "7.8.3",
     "@babel/preset-env": "7.9.6",
     "@babel/preset-typescript": "7.9.0",
+    "@jest/globals": "26.0.1",
     "@jest/reporters": "26.0.1",
     "@jest/test-result": "26.0.1",
     "@semantic-release/exec": "5.0.0",
diff --git a/test/execUtil.ts b/test/execUtil.ts
index babd4063a1f7c9462a4875a5a3cc3fb4fe523010..479f33559765b33855e645886f2ceb9438201a24 100644
--- a/test/execUtil.ts
+++ b/test/execUtil.ts
@@ -1,4 +1,7 @@
 import { exec as _exec } from 'child_process';
+import is from '@sindresorhus/is';
+import traverse from 'traverse';
+import { toUnix } from 'upath';
 import { ExecOptions } from '../lib/util/exec';
 
 type CallOptions = ExecOptions | null | undefined;
@@ -18,15 +21,15 @@ export function execSnapshot(cmd: string, options?: CallOptions): ExecSnapshot {
     options,
   };
 
-  const str = JSON.stringify(snapshot, (k, v) => (v === undefined ? null : v));
+  const cwd = toUnix(process.cwd());
 
-  const cwd = process.cwd().replace(/\\(\w)/g, '/$1');
-  return JSON.parse(
-    str
-      .replace(/\\(\w)/g, '/$1')
-      .split(cwd)
-      .join('/root/project')
-  );
+  // eslint-disable-next-line array-callback-return
+  return traverse(snapshot).map(function fixup(v) {
+    if (is.string(v)) {
+      const val = v.replace(/\\(\w)/g, '/$1').replace(cwd, '/root/project');
+      this.update(val);
+    }
+  });
 }
 
 const defaultExecResult = { stdout: '', stderr: '' };
diff --git a/test/util.ts b/test/util.ts
index 34e742350300090ca56d4fe3765202a05e49c930..c94ec68c2f3e0702543c39c1e79fa83b5b338a06 100644
--- a/test/util.ts
+++ b/test/util.ts
@@ -1,9 +1,12 @@
 import crypto from 'crypto';
+import { expect, jest } from '@jest/globals';
 import * as upath from 'upath';
 import { RenovateConfig as _RenovateConfig } from '../lib/config';
 import { getConfig } from '../lib/config/defaults';
 import { platform as _platform } from '../lib/platform';
+import * as _env from '../lib/util/exec/env';
 import * as _fs from '../lib/util/fs';
+import * as _hostRules from '../lib/util/host-rules';
 
 /**
  * Simple wrapper for getting mocked version of a module
@@ -26,7 +29,7 @@ export function mocked<T>(module: T): jest.Mocked<T> {
  */
 export function mockPartial(moduleName: string, overrides?: object): unknown {
   const absolutePath = upath.join(module.parent.filename, '../', moduleName);
-  const originalModule = jest.requireActual(absolutePath);
+  const originalModule = jest.requireActual(absolutePath) as any;
   return {
     __esModule: true,
     ...originalModule,
@@ -44,6 +47,8 @@ export function partial<T>(obj: Partial<T>): T {
 
 export const fs = mocked(_fs);
 export const platform = mocked(_platform);
+export const env = mocked(_env);
+export const hostRules = mocked(_hostRules);
 
 // Required because of isolatedModules
 export type RenovateConfig = _RenovateConfig;
@@ -75,14 +80,22 @@ export const replacingSerializer = (
   },
 });
 
+export function addReplacingSerializer(from: string, to: string): void {
+  expect.addSnapshotSerializer(replacingSerializer(from, to));
+}
+
 function toHash(buf: Buffer): string {
   return crypto.createHash('sha256').update(buf).digest('hex');
 }
 
-export const bufferSerializer = (): jest.SnapshotSerializerPlugin => ({
+const bufferSerializer: jest.SnapshotSerializerPlugin = {
   test: (value) => Buffer.isBuffer(value),
   serialize: (val, config, indent, depth, refs, printer) => {
     const replaced = toHash(val);
     return printer(replaced, config, indent, depth, refs);
   },
-});
+};
+
+export function addBufferSerializer(): void {
+  expect.addSnapshotSerializer(bufferSerializer);
+}
diff --git a/yarn.lock b/yarn.lock
index c58b15251e25dc5c7dc40ba0402b151aaaaaa57a..c0bf83756e74c53f8832896181ad8786da04e659 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -946,7 +946,7 @@
     jest-mock "^26.0.1"
     jest-util "^26.0.1"
 
-"@jest/globals@^26.0.1":
+"@jest/globals@26.0.1", "@jest/globals@^26.0.1":
   version "26.0.1"
   resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.0.1.tgz#3f67b508a7ce62b6e6efc536f3d18ec9deb19a9c"
   integrity sha512-iuucxOYB7BRCvT+TYBzUqUNuxFX1hqaR6G6IcGgEqkJ5x4htNKo1r7jk1ji9Zj8ZMiMw0oB5NaA7k5Tx6MVssA==