Repository: beeware/briefcase
Pull request #599 (2021-07-12 07:58:42)

Replace use of str(pathlib.Path(...)) in codebase
<!--- What problem does this change solve? -->
I removed `str()` functions from `str(pathlib.Path(...))` or replaced with `os.fsdecode`. `str()` for Pathlib objects was introduced for python 3.5 compatibility.
<!--- If this PR relates to an issue, include Refs #XXX or Fixes #XXX -->

Refs #592 

## PR Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
- [x] All new features have been tested
- [x] All new features have been documented
- [x] I have read the **CONTRIBUTING.md** file
- [x] I will abide by the code of conduct



Patch:
diff --git a/changes/592.misc.rst b/changes/592.misc.rst
new file mode 100644
index 00000000..8ad29e55
--- /dev/null
+++ b/changes/592.misc.rst
@@ -0,0 +1,2 @@
+str() functions from str(pathlib.Path(...)) are removed or replaced with os.fsdecode.
+str() functions were introduced for compatibility with Python 3.5.
\ No newline at end of file
diff --git a/src/briefcase/commands/create.py b/src/briefcase/commands/create.py
index 9a947716..41f33cf2 100644
--- a/src/briefcase/commands/create.py
+++ b/src/briefcase/commands/create.py
@@ -1,3 +1,4 @@
+import os
 import shutil
 import subprocess
 import sys
@@ -296,7 +297,7 @@ class CreateCommand(BaseCommand):
             self.cookiecutter(
                 str(cached_template),
                 no_input=True,
-                output_dir=str(output_path),
+                output_dir=os.fsdecode(output_path),
                 checkout=app.template_branch,
                 extra_context=extra_context
             )
@@ -369,9 +370,10 @@ class CreateCommand(BaseCommand):
             print("Unpacking support package...")
             support_path = self.support_path(app)
             support_path.mkdir(parents=True, exist_ok=True)
+            # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
             self.shutil.unpack_archive(
-                str(support_filename),
-                extract_dir=str(support_path)
+                os.fsdecode(support_filename),
+                extract_dir=os.fsdecode(support_path)
             )
         except (shutil.ReadError, EOFError):
             print()
@@ -415,7 +417,7 @@ class CreateCommand(BaseCommand):
                 # Remove existing versions of the code
                 if target.exists():
                     if target.is_dir():
-                        self.shutil.rmtree(str(target))
+                        self.shutil.rmtree(target)
                     else:
                         target.unlink()
 
@@ -423,9 +425,9 @@ class CreateCommand(BaseCommand):
                 if not original.exists():
                     raise MissingAppSources(src)
                 elif original.is_dir():
-                    self.shutil.copytree(str(original), str(target))
+                    self.shutil.copytree(original, target)
                 else:
-                    self.shutil.copy(str(original), str(target))
+                    self.shutil.copy(original, target)
         else:
             print("No sources defined for {app.app_name}.".format(app=app))
 
@@ -542,7 +544,7 @@ class CreateCommand(BaseCommand):
                 # Make sure the target directory exists
                 target.parent.mkdir(parents=True, exist_ok=True)
                 # Copy the source image to the target location
-                self.shutil.copy(str(full_source), str(target))
+                self.shutil.copy(full_source, target)
             else:
                 print(
                     "Unable to find {source_filename} for {full_role}; using default".format(
@@ -637,7 +639,7 @@ class CreateCommand(BaseCommand):
             print("[{app.app_name}] Removing old application bundle...".format(
                 app=app
             ))
-            self.shutil.rmtree(str(bundle_path))
+            self.shutil.rmtree(bundle_path)
 
         print()
         print('[{app.app_name}] Generating application template...'.format(
diff --git a/src/briefcase/commands/new.py b/src/briefcase/commands/new.py
index e6d9b9c3..ca18e136 100644
--- a/src/briefcase/commands/new.py
+++ b/src/briefcase/commands/new.py
@@ -1,3 +1,4 @@
+import os
 import re
 import subprocess
 from email.utils import parseaddr
@@ -484,7 +485,7 @@ What GUI toolkit do you want to use for this project?""",
             self.cookiecutter(
                 str(cached_template),
                 no_input=True,
-                output_dir=str(self.base_path),
+                output_dir=os.fsdecode(self.base_path),
                 checkout="v0.3",
                 extra_context=context
             )
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index b51a7a25..82e9e31d 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -1,3 +1,4 @@
+import os
 import re
 import shutil
 import subprocess
@@ -84,7 +85,7 @@ class AndroidSDK:
     def env(self):
         return {
             **self.command.os.environ,
-            "ANDROID_SDK_ROOT": str(self.root_path),
+            "ANDROID_SDK_ROOT": os.fsdecode(self.root_path),
             "JAVA_HOME": str(self.jdk.java_home),
         }
 
@@ -175,7 +176,7 @@ class AndroidSDK:
         """
         return self.sdkmanager_path.exists() and (
             self.command.host_os == "Windows"
-            or self.command.os.access(str(self.sdkmanager_path), self.command.os.X_OK)
+            or self.command.os.access(self.sdkmanager_path, self.command.os.X_OK)
         )
 
     @property
@@ -199,7 +200,8 @@ class AndroidSDK:
 
         try:
             print("Install Android SDK...")
-            self.command.shutil.unpack_archive(str(sdk_zip_path), extract_dir=str(self.root_path))
+            # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
+            self.command.shutil.unpack_archive(os.fsdecode(sdk_zip_path), extract_dir=os.fsdecode(self.root_path))
         except (shutil.ReadError, EOFError):
             raise BriefcaseCommandError(
                 """\
@@ -218,7 +220,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
         # On non-Windows, we manually fix permissions.
         if self.command.host_os != "Windows":
             for binpath in (self.root_path / "tools" / "bin").glob("*"):
-                if not self.command.os.access(str(binpath), self.command.os.X_OK):
+                if not self.command.os.access(binpath, self.command.os.X_OK):
                     binpath.chmod(0o755)
 
         # Licences must be accepted.
@@ -230,7 +232,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
             # Using subprocess.run() with no I/O redirection so the user sees
             # the full output and can send input.
             self.command.subprocess.run(
-                [str(self.sdkmanager_path), "--update"], env=self.env, check=True,
+                [os.fsdecode(self.sdkmanager_path), "--update"], env=self.env, check=True,
             )
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(
@@ -272,7 +274,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
             # Using subprocess.run() with no I/O redirection so the user sees
             # the full output and can send input.
             self.command.subprocess.run(
-                [str(self.sdkmanager_path), "--licenses"], env=self.env, check=True,
+                [os.fsdecode(self.sdkmanager_path), "--licenses"], env=self.env, check=True,
             )
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(
@@ -307,7 +309,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
             # displaying it only if an exception occurs.
             self.command.subprocess.run(
                 [
-                    str(self.sdkmanager_path),
+                    os.fsdecode(self.sdkmanager_path),
                     "platforms;android-28",
                     "system-images;android-28;default;x86",
                     "emulator",
@@ -329,7 +331,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
             # Capture `stderr` so that if the process exits with failure, the
             # stderr data is in `e.output`.
             output = self.command.subprocess.check_output(
-                [str(self.emulator_path), "-list-avds"],
+                [os.fsdecode(self.emulator_path), "-list-avds"],
                 universal_newlines=True,
                 stderr=subprocess.STDOUT,
             ).strip()
@@ -349,7 +351,7 @@ Delete {sdk_zip_path} and run briefcase again.""".format(
             # Capture `stderr` so that if the process exits with failure, the
             # stderr data is in `e.output`.
             output = self.command.subprocess.check_output(
-                [str(self.adb_path), "devices", "-l"],
+                [os.fsdecode(self.adb_path), "devices", "-l"],
                 universal_newlines=True,
                 stderr=subprocess.STDOUT,
             ).strip()
@@ -611,7 +613,7 @@ An emulator named '{avd}' already exists.
             print()
             self.command.subprocess.check_output(
                 [
-                    str(self.avdmanager_path),
+                    os.fsdecode(self.avdmanager_path),
                     "--verbose",
                     "create", "avd",
                     "--name", avd,
@@ -648,9 +650,10 @@ An emulator named '{avd}' already exists.
 
             # Unpack skin archive
             try:
+                # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
                 self.command.shutil.unpack_archive(
-                    str(skin_tgz_path),
-                    extract_dir=str(skin_path)
+                    os.fsdecode(skin_tgz_path),
+                    extract_dir=os.fsdecode(skin_path)
                 )
             except (shutil.ReadError, EOFError):
                 raise BriefcaseCommandError(
@@ -694,7 +697,7 @@ In future, you can specify this device by running:
             print("Starting emulator {avd}...".format(avd=avd))
             emulator_popen = self.command.subprocess.Popen(
                 [
-                    str(self.emulator_path),
+                    os.fsdecode(self.emulator_path),
                     '@' + avd,
                     '-dns-server', '8.8.8.8'
                 ],
@@ -850,11 +853,11 @@ class ADB:
             # stderr data is in `e.output`.
             return self.command.subprocess.check_output(
                 [
-                    str(self.android_sdk.adb_path),
+                    os.fsdecode(self.android_sdk.adb_path),
                     "-s",
                     self.device,
                 ]
-                + list(arguments),
+                + [(os.fsdecode(arg) if isinstance(arg, Path) else arg) for arg in arguments],
                 universal_newlines=True,
                 stderr=subprocess.STDOUT,
             )
@@ -872,7 +875,7 @@ class ADB:
         Returns `None` on success; raises an exception on failure.
         """
         try:
-            self.run("install", str(apk_path))
+            self.run("install", apk_path)
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(
                 "Unable to install APK {apk_path} on {device}".format(
@@ -976,7 +979,7 @@ class ADB:
             # the full output and can send input.
             self.command.subprocess.run(
                 [
-                    str(self.android_sdk.adb_path),
+                    os.fsdecode(self.android_sdk.adb_path),
                     "-s",
                     self.device,
                     "logcat",
diff --git a/src/briefcase/integrations/docker.py b/src/briefcase/integrations/docker.py
index 55642419..a8edc99c 100644
--- a/src/briefcase/integrations/docker.py
+++ b/src/briefcase/integrations/docker.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 import sys
 from pathlib import Path
@@ -228,7 +229,7 @@ class Docker:
                     ),
                     "--build-arg", "HOST_UID={uid}".format(uid=self.command.os.getuid()),
                     "--build-arg", "HOST_GID={gid}".format(gid=self.command.os.getgid()),
-                    Path(str(self.command.base_path), *self.app.sources[0].split('/')[:-1])
+                    Path(self.command.base_path, *self.app.sources[0].split('/')[:-1])
                 ],
                 check=True,
             )
@@ -276,14 +277,14 @@ class Docker:
                         command=self.command
                     )
                 )
-            elif str(self.command.platform_path) in arg:
+            elif os.fsdecode(self.command.platform_path) in arg:
                 docker_args.append(
-                    arg.replace(str(self.command.platform_path), '/app')
+                    arg.replace(os.fsdecode(self.command.platform_path), '/app')
                 )
-            elif str(self.command.dot_briefcase_path) in arg:
+            elif os.fsdecode(self.command.dot_briefcase_path) in arg:
                 docker_args.append(
                     arg.replace(
-                        str(self.command.dot_briefcase_path), '/home/brutus/.briefcase'
+                        os.fsdecode(self.command.dot_briefcase_path), '/home/brutus/.briefcase'
                     )
                 )
             else:
diff --git a/src/briefcase/integrations/java.py b/src/briefcase/integrations/java.py
index 72c77e71..288b18d7 100644
--- a/src/briefcase/integrations/java.py
+++ b/src/briefcase/integrations/java.py
@@ -1,3 +1,4 @@
+import os
 import shutil
 import subprocess
 from pathlib import Path
@@ -91,7 +92,7 @@ class JDK:
                 # This verifies that we have a JDK, not a just a JRE.
                 output = command.subprocess.check_output(
                     [
-                        str(Path(java_home) / 'bin' / 'javac'),
+                        os.fsdecode(Path(java_home) / 'bin' / 'javac'),
                         '-version',
                     ],
                     universal_newlines=True,
@@ -254,9 +255,10 @@ class JDK:
 
         try:
             print("Installing AdoptOpenJDK...")
+            # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
             self.command.shutil.unpack_archive(
-                str(jdk_zip_path),
-                extract_dir=str(self.command.tools_path)
+                os.fsdecode(jdk_zip_path),
+                extract_dir=os.fsdecode(self.command.tools_path)
             )
         except (shutil.ReadError, EOFError):
             raise BriefcaseCommandError(
diff --git a/src/briefcase/integrations/linuxdeploy.py b/src/briefcase/integrations/linuxdeploy.py
index f8f09165..88903053 100644
--- a/src/briefcase/integrations/linuxdeploy.py
+++ b/src/briefcase/integrations/linuxdeploy.py
@@ -61,7 +61,7 @@ class LinuxDeploy:
                 url=self.linuxdeploy_download_url,
                 download_path=self.command.tools_path
             )
-            self.command.os.chmod(str(linuxdeploy_appimage_path), 0o755)
+            self.command.os.chmod(linuxdeploy_appimage_path, 0o755)
         except requests_exceptions.ConnectionError:
             raise NetworkFailure('downloading linuxdeploy AppImage')
 
diff --git a/src/briefcase/integrations/wix.py b/src/briefcase/integrations/wix.py
index f61e4c05..9dde1375 100644
--- a/src/briefcase/integrations/wix.py
+++ b/src/briefcase/integrations/wix.py
@@ -1,3 +1,4 @@
+import os
 import shutil
 from pathlib import Path
 
@@ -133,9 +134,10 @@ WiX Toolset. Current value: {wix_home!r}
 
         try:
             print("Installing WiX...")
+            # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
             self.command.shutil.unpack_archive(
-                str(wix_zip_path),
-                extract_dir=str(self.wix_home)
+                os.fsdecode(wix_zip_path),
+                extract_dir=os.fsdecode(self.wix_home)
             )
         except (shutil.ReadError, EOFError):
             raise BriefcaseCommandError("""
diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py
index 3edd0055..f7c9ab0f 100644
--- a/src/briefcase/platforms/android/gradle.py
+++ b/src/briefcase/platforms/android/gradle.py
@@ -110,11 +110,11 @@ class GradleBuildCommand(GradleMixin, BuildCommand):
                 # Windows needs the full path to `gradlew`; macOS & Linux can find it
                 # via `./gradlew`. For simplicity of implementation, we always provide
                 # the full path.
-                [str(self.gradlew_path(app)), "assembleDebug"],
+                [self.gradlew_path(app), "assembleDebug"],
                 env=self.android_sdk.env,
                 # Set working directory so gradle can use the app bundle path as its
                 # project root, i.e., to avoid 'Task assembleDebug not found'.
-                cwd=str(self.bundle_path(app)),
+                cwd=self.bundle_path(app),
                 check=True
             )
         except subprocess.CalledProcessError:
@@ -224,11 +224,11 @@ class GradlePackageCommand(GradleMixin, PackageCommand):
                 # Windows needs the full path to `gradlew`; macOS & Linux can find it
                 # via `./gradlew`. For simplicity of implementation, we always provide
                 # the full path.
-                [str(self.gradlew_path(app)), "bundleRelease"],
+                [self.gradlew_path(app), "bundleRelease"],
                 env=self.android_sdk.env,
                 # Set working directory so gradle can use the app bundle path as its
                 # project root, i.e., to avoid 'Task bundleRelease not found'.
-                cwd=str(self.bundle_path(app)),
+                cwd=self.bundle_path(app),
                 check=True
             )
         except subprocess.CalledProcessError:
diff --git a/src/briefcase/platforms/linux/appimage.py b/src/briefcase/platforms/linux/appimage.py
index 639719cd..6aa3efc2 100644
--- a/src/briefcase/platforms/linux/appimage.py
+++ b/src/briefcase/platforms/linux/appimage.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from contextlib import contextmanager
 
@@ -190,10 +191,10 @@ class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
             with self.dockerize(app) as docker:
                 docker.run(
                     [
-                        str(self.linuxdeploy.appimage_path),
+                        self.linuxdeploy.appimage_path,
                         "--appimage-extract-and-run",
                         "--appdir={appdir_path}".format(appdir_path=self.appdir_path(app)),
-                        "-d", str(
+                        "-d", os.fsdecode(
                             self.appdir_path(app) / "{app.bundle}.{app.app_name}.desktop".format(
                                 app=app,
                             )
@@ -202,11 +203,11 @@ class LinuxAppImageBuildCommand(LinuxAppImageMixin, BuildCommand):
                     ] + deploy_deps_args,
                     env=env,
                     check=True,
-                    cwd=str(self.platform_path)
+                    cwd=self.platform_path
                 )
 
             # Make the binary executable.
-            self.os.chmod(str(self.binary_path(app)), 0o755)
+            self.os.chmod(self.binary_path(app), 0o755)
         except subprocess.CalledProcessError:
             print()
             raise BriefcaseCommandError(
@@ -242,7 +243,7 @@ class LinuxAppImageRunCommand(LinuxAppImageMixin, RunCommand):
             print()
             self.subprocess.run(
                 [
-                    str(self.binary_path(app)),
+                    os.fsdecode(self.binary_path(app)),
                 ],
                 check=True,
             )
diff --git a/src/briefcase/platforms/macOS/__init__.py b/src/briefcase/platforms/macOS/__init__.py
index c075c04d..2e901c18 100644
--- a/src/briefcase/platforms/macOS/__init__.py
+++ b/src/briefcase/platforms/macOS/__init__.py
@@ -1,11 +1,14 @@
-import subprocess
 import itertools
+import os
+import subprocess
 
 from briefcase.config import BaseConfig
 from briefcase.console import select_option
 from briefcase.exceptions import BriefcaseCommandError
-from briefcase.integrations.xcode import get_identities
-from briefcase.integrations.xcode import verify_command_line_tools_install
+from briefcase.integrations.xcode import (
+    get_identities,
+    verify_command_line_tools_install
+)
 
 try:
     import dmgbuild
@@ -39,7 +42,7 @@ class macOSRunMixin:
                 [
                     'open',
                     '-n',  # Force a new app to be launched
-                    str(self.binary_path(app)),
+                    os.fsdecode(self.binary_path(app)),
                 ],
                 check=True,
             )
@@ -74,7 +77,7 @@ class macOSRunMixin:
                     'senderImagePath=="{sender}"'
                     ' OR (processImagePath=="{sender}"'
                     ' AND senderImagePath=="/usr/lib/libffi.dylib")'.format(
-                        sender=str(self.binary_path(app) / "Contents" / "MacOS" / app.formal_name)
+                        sender=os.fsdecode(self.binary_path(app) / "Contents" / "MacOS" / app.formal_name)
                     )
                 ],
                 check=True,
@@ -176,8 +179,8 @@ class macOSPackageMixin:
                 [
                     'codesign',
                     '--sign', identity,
-                    '--entitlements', str(entitlements),
-                    '--deep', str(path),
+                    '--entitlements', os.fsdecode(entitlements),
+                    '--deep', os.fsdecode(path),
                     '--force',
                     '--options', 'runtime',
                 ],
@@ -243,7 +246,7 @@ class macOSPackageMixin:
             print('[{app.app_name}] Building DMG...'.format(app=app))
 
             dmg_settings = {
-                'files': [str(self.binary_path(app))],
+                'files': [os.fsdecode(self.binary_path(app))],
                 'symlinks': {'Applications': '/Applications'},
                 'icon_locations': {
                     '{app.formal_name}.app'.format(app=app): (75, 75),
@@ -278,14 +281,14 @@ class macOSPackageMixin:
                     icon_filename = None
 
             if icon_filename:
-                dmg_settings['icon'] = str(icon_filename)
+                dmg_settings['icon'] = os.fsdecode(icon_filename)
 
             try:
                 image_filename = self.base_path / '{image}.png'.format(
                     image=app.installer_background
                 )
                 if image_filename.exists():
-                    dmg_settings['background'] = str(image_filename)
+                    dmg_settings['background'] = os.fsdecode(image_filename)
                 else:
                     print("Can't find {filename}.png for DMG background".format(
                         filename=app.installer_background
@@ -295,7 +298,7 @@ class macOSPackageMixin:
                 pass
 
             self.dmgbuild.build_dmg(
-                filename=str(self.distribution_path(app, packaging_format=packaging_format)),
+                filename=os.fsdecode(self.distribution_path(app, packaging_format=packaging_format)),
                 volume_name='{app.formal_name} {app.version}'.format(app=app),
                 settings=dmg_settings
             )
diff --git a/src/briefcase/platforms/macOS/app.py b/src/briefcase/platforms/macOS/app.py
index 946e9f99..5264e5d0 100644
--- a/src/briefcase/platforms/macOS/app.py
+++ b/src/briefcase/platforms/macOS/app.py
@@ -1,3 +1,4 @@
+import os
 import tempfile
 from pathlib import Path
 
@@ -45,14 +46,16 @@ class macOSAppCreateCommand(macOSAppMixin, CreateCommand):
         super().install_app_support_package(app)
 
         # keep only Python lib from support package
-        lib_path = str(self.support_path(app).parent / 'Support' / 'Python' / 'Resources' / 'lib')
+        lib_path = self.support_path(app).parent / 'Support' / 'Python' / 'Resources' / 'lib'
 
         with tempfile.TemporaryDirectory() as tmpdir:
-            self.shutil.move(lib_path, tmpdir)
-            self.shutil.rmtree(str(self.support_path(app)))
+            # TODO: Py3.8 compatibility; os.fsdecode not required in Py3.9
+            self.shutil.move(os.fsdecode(lib_path), os.fsdecode(tmpdir))
+            self.shutil.rmtree(self.support_path(app))
 
-            self.os.makedirs(str(Path(lib_path).parent))
-            self.shutil.move(str(Path(tmpdir) / 'lib'), lib_path)
+            self.os.makedirs(Path(lib_path).parent)
+            # TODO: Py3.8 compatibility; os.fsdecode not required in Py3.9
+            self.shutil.move(os.fsdecode(Path(tmpdir) / 'lib'), os.fsdecode(lib_path))
 
 
 class macOSAppUpdateCommand(macOSAppMixin, UpdateCommand):
diff --git a/src/briefcase/platforms/windows/msi.py b/src/briefcase/platforms/windows/msi.py
index e3ae176c..06a76c78 100644
--- a/src/briefcase/platforms/windows/msi.py
+++ b/src/briefcase/platforms/windows/msi.py
@@ -1,3 +1,4 @@
+import os
 import struct
 import subprocess
 import sys
@@ -143,7 +144,7 @@ class WindowsMSIRunCommand(WindowsMSIMixin, RunCommand):
             print()
             self.subprocess.run(
                 [
-                    str(self.binary_path(app) / 'src' / 'python' / 'pythonw.exe'),
+                    os.fsdecode(self.binary_path(app) / 'src' / 'python' / 'pythonw.exe'),
                     "-m", app.module_name
                 ],
                 check=True,
@@ -172,7 +173,7 @@ class WindowsMSIPackageCommand(WindowsMSIMixin, PackageCommand):
             print("Compiling application manifest...")
             self.subprocess.run(
                 [
-                    str(self.wix.heat_exe),
+                    self.wix.heat_exe,
                     "dir",
                     "src",
                     "-nologo",  # Don't display startup text
@@ -187,7 +188,7 @@ class WindowsMSIPackageCommand(WindowsMSIMixin, PackageCommand):
                     "-out", "{app.app_name}-manifest.wxs".format(app=app),
                 ],
                 check=True,
-                cwd=str(self.bundle_path(app))
+                cwd=self.bundle_path(app)
             )
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(
@@ -199,7 +200,7 @@ class WindowsMSIPackageCommand(WindowsMSIMixin, PackageCommand):
             print("Compiling application installer...")
             self.subprocess.run(
                 [
-                    str(self.wix.candle_exe),
+                    self.wix.candle_exe,
                     "-nologo",  # Don't display startup text
                     "-ext", "WixUtilExtension",
                     "-ext", "WixUIExtension",
@@ -208,7 +209,7 @@ class WindowsMSIPackageCommand(WindowsMSIMixin, PackageCommand):
                     "{app.app_name}-manifest.wxs".format(app=app),
                 ],
                 check=True,
-                cwd=str(self.bundle_path(app))
+                cwd=self.bundle_path(app)
             )
         except subprocess.CalledProcessError:
             raise BriefcaseCommandError(
@@ -220,16 +221,16 @@ class WindowsMSIPackageCommand(WindowsMSIMixin, PackageCommand):
             print("Linking application installer...")
             self.subprocess.run(
                 [
-                    str(self.wix.light_exe),
+                    self.wix.light_exe,
                     "-nologo",  # Don't display startup text
                     "-ext", "WixUtilExtension",
                     "-ext", "WixUIExtension",
-                    "-o", str(self.distribution_path(app, packaging_format='msi')),
+                    "-o", self.distribution_path(app, packaging_format='msi'),
                     "{app.app_name}.wixobj".format(app=app),
                     "{app.app_name}-manifest.wixobj".format(app=app),
                 ],
                 check=True,
-                cwd=str(self.bundle_path(app))
+                cwd=self.bundle_path(app)
             )
         except subprocess.CalledProcessError:
             print()


Test patch:
diff --git a/tests/commands/base/test_app_module_path.py b/tests/commands/base/test_app_module_path.py
index 591bc1b4..5daaeb1f 100644
--- a/tests/commands/base/test_app_module_path.py
+++ b/tests/commands/base/test_app_module_path.py
@@ -7,28 +7,28 @@ def test_single_source(base_command, my_app):
     "If an app provides a single source location and it matches, it is selected as the dist-info location"
     my_app.sources = ['src/my_app']
 
-    assert str(base_command.app_module_path(my_app)) == str(base_command.base_path / 'src' / 'my_app')
+    assert base_command.app_module_path(my_app) == base_command.base_path / 'src' / 'my_app'
 
 
 def test_no_prefix(base_command, my_app):
     "If an app provides a source location without a prefix and it matches, it is selected as the dist-info location"
     my_app.sources = ['my_app']
 
-    assert str(base_command.app_module_path(my_app)) == str(base_command.base_path / 'my_app')
+    assert base_command.app_module_path(my_app) == base_command.base_path / 'my_app'
 
 
 def test_long_prefix(base_command, my_app):
     "If an app provides a source location with a long prefix and it matches, it is selected as the dist-info location"
     my_app.sources = ['path/to/src/my_app']
 
-    assert str(base_command.app_module_path(my_app)) == str(base_command.base_path / 'path' / 'to' / 'src' / 'my_app')
+    assert base_command.app_module_path(my_app) == base_command.base_path / 'path' / 'to' / 'src' / 'my_app'
 
 
 def test_matching_source(base_command, my_app):
     "If an app provides a single matching source location, it is selected as the dist-info location"
     my_app.sources = ['src/other', 'src/my_app', 'src/extra']
 
-    assert str(base_command.app_module_path(my_app)) == str(base_command.base_path / 'src' / 'my_app')
+    assert base_command.app_module_path(my_app) == base_command.base_path / 'src' / 'my_app'
 
 
 def test_multiple_match(base_command, my_app):
diff --git a/tests/commands/base/test_parse_config.py b/tests/commands/base/test_parse_config.py
index 2775d0f9..add3ac78 100644
--- a/tests/commands/base/test_parse_config.py
+++ b/tests/commands/base/test_parse_config.py
@@ -6,7 +6,7 @@ from briefcase.exceptions import BriefcaseConfigError
 
 def test_missing_config(base_command):
     "If the configuration file doesn't exit, raise an error"
-    filename = str(base_command.base_path / 'does_not_exist.toml')
+    filename = base_command.base_path / 'does_not_exist.toml'
     with pytest.raises(BriefcaseConfigError, match="configuration file not found"):
         base_command.parse_config(filename)
 
@@ -14,7 +14,7 @@ def test_missing_config(base_command):
 def test_incomplete_global_config(base_command):
     "If the global configuration is missing a required argument, an error is raised"
     # Provide a configuration that is missing `bundle`, a required attribute
-    filename = str(base_command.base_path / 'pyproject.toml')
+    filename = base_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
@@ -34,7 +34,7 @@ def test_incomplete_global_config(base_command):
 def test_incomplete_config(base_command):
     "If the configuration is missing a required argument, an error is raised"
     # Provide a configuration that is missing `bundle`, a required attribute
-    filename = str(base_command.base_path / 'pyproject.toml')
+    filename = base_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
@@ -55,7 +55,7 @@ def test_incomplete_config(base_command):
 
 def test_parse_config(base_command):
     "A well formed configuration file can be augmented by the command line"
-    filename = str(base_command.base_path / 'pyproject.toml')
+    filename = base_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
@@ -104,7 +104,7 @@ def test_parse_config(base_command):
 
 def test_parse_config_custom_config_classes_missing_global_arg(other_command):
     "A command that defines custom config classes can enforce global arguments"
-    filename = str(other_command.base_path / 'pyproject.toml')
+    filename = other_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
@@ -130,7 +130,7 @@ def test_parse_config_custom_config_classes_missing_global_arg(other_command):
 
 def test_parse_config_custom_config_classes_missing_app_arg(other_command):
     "A command that defines custom config classes can enforce app arguments"
-    filename = str(other_command.base_path / 'pyproject.toml')
+    filename = other_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
@@ -149,7 +149,7 @@ def test_parse_config_custom_config_classes_missing_app_arg(other_command):
 
 def test_parse_config_custom_config_classes(other_command):
     "A well formed configuration file can be augmented by the command line"
-    filename = str(other_command.base_path / 'pyproject.toml')
+    filename = other_command.base_path / 'pyproject.toml'
     with open(filename, 'w') as config_file:
         config_file.write("""
         [tool.briefcase]
diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py
index 9323adee..a59c74bf 100644
--- a/tests/commands/create/test_generate_app_template.py
+++ b/tests/commands/create/test_generate_app_template.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from datetime import date
 from pathlib import Path
@@ -62,7 +63,7 @@ def test_default_template(create_command, myapp):
         'https://github.com/beeware/briefcase-tester-dummy-template.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': '3.X',
@@ -87,7 +88,7 @@ def test_explicit_branch(create_command, myapp):
         'https://github.com/beeware/briefcase-tester-dummy-template.git',
         no_input=True,
         checkout=branch,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': branch,
@@ -115,7 +116,7 @@ def test_platform_exists(create_command, myapp):
         'https://github.com/beeware/briefcase-tester-dummy-template.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': '3.X',
@@ -142,7 +143,7 @@ def test_explicit_repo_template(create_command, myapp):
         'https://example.com/magic/special-template.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://example.com/magic/special-template.git',
             'template_branch': '3.X',
@@ -165,7 +166,7 @@ def test_explicit_local_template(create_command, myapp):
         '/path/to/special-template',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': '/path/to/special-template',
             'template_branch': '3.X',
@@ -200,7 +201,7 @@ def test_offline_repo_template(create_command, myapp):
         'https://github.com/beeware/briefcase-tester-dummy-template.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': '3.X',
@@ -231,7 +232,7 @@ def test_invalid_repo_template(create_command, myapp):
         'https://example.com/somewhere/not-a-repo.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://example.com/somewhere/not-a-repo.git',
             'template_branch': '3.X',
@@ -263,7 +264,7 @@ def test_missing_branch_template(create_command, myapp):
         'https://example.com/somewhere/missing-branch.git',
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://example.com/somewhere/missing-branch.git',
             'template_branch': '3.X',
@@ -298,10 +299,10 @@ def test_cached_template(create_command, myapp):
 
     # Cookiecutter was invoked with the path to the *cached* template name
     create_command.cookiecutter.assert_called_once_with(
-        str(Path.home() / '.cookiecutters' / 'briefcase-tester-dummy-template'),
+        os.fsdecode(Path.home() / '.cookiecutters' / 'briefcase-tester-dummy-template'),
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': '3.X',
@@ -342,10 +343,10 @@ def test_cached_template_offline(create_command, myapp, capsys):
 
     # Cookiecutter was invoked with the path to the *cached* template name
     create_command.cookiecutter.assert_called_once_with(
-        str(Path.home() / '.cookiecutters' / 'briefcase-tester-dummy-template'),
+        os.fsdecode(Path.home() / '.cookiecutters' / 'briefcase-tester-dummy-template'),
         no_input=True,
         checkout=create_command.python_version_tag,
-        output_dir=str(create_command.platform_path),
+        output_dir=os.fsdecode(create_command.platform_path),
         extra_context=full_context({
             'template': 'https://github.com/beeware/briefcase-tester-dummy-template.git',
             'template_branch': '3.X',
diff --git a/tests/commands/create/test_install_app_support_package.py b/tests/commands/create/test_install_app_support_package.py
index 145d5af1..1a491c33 100644
--- a/tests/commands/create/test_install_app_support_package.py
+++ b/tests/commands/create/test_install_app_support_package.py
@@ -1,3 +1,4 @@
+import os
 import zipfile
 from unittest import mock
 
@@ -12,7 +13,7 @@ def test_install_app_support_package(create_command, myapp, tmp_path, support_pa
     "A support package can be downloaded and unpacked where it is needed"
     # Write a temporary support zip file
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -39,7 +40,7 @@ def test_install_pinned_app_support_package(create_command, myapp, tmp_path, sup
 
     # Write a temporary support zip file
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -62,12 +63,12 @@ def test_install_pinned_app_support_package(create_command, myapp, tmp_path, sup
 def test_install_custom_app_support_package_file(create_command, myapp, tmp_path, support_path):
     "A custom support package can be specified as a local file"
     # Provide an app-specific override of the package URL
-    myapp.support_package = str(tmp_path / 'custom' / 'support.zip')
+    myapp.support_package = os.fsdecode(tmp_path / 'custom' / 'support.zip')
 
     # Write a temporary support zip file
     support_file = tmp_path / 'custom' / 'support.zip'
     support_file.parent.mkdir(parents=True)
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -92,7 +93,7 @@ def test_install_custom_app_support_package_url(create_command, myapp, tmp_path,
 
     # Write a temporary support zip file
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -122,7 +123,7 @@ def test_install_pinned_custom_app_support_package_url(create_command, myapp, tm
 
     # Write a temporary support zip file
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -152,7 +153,7 @@ def test_install_pinned_custom_app_support_package_url_with_args(create_command,
 
     # Write a temporary support zip file
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
 
     # Modify download_url to return the temp zipfile
@@ -187,7 +188,7 @@ def test_invalid_support_package(create_command, myapp, tmp_path, support_path):
     "If the support package isn't a valid zipfile, an error is raised"
     # Create a support package that isn't a zipfile
     support_file = tmp_path / 'out.zip'
-    with open(str(support_file), 'w') as bad_support_zip:
+    with open(support_file, 'w') as bad_support_zip:
         bad_support_zip.write("This isn't a zip file")
 
     # Make the download URL return the temp file
diff --git a/tests/commands/create/test_install_image.py b/tests/commands/create/test_install_image.py
index b1f1b639..928e63c3 100644
--- a/tests/commands/create/test_install_image.py
+++ b/tests/commands/create/test_install_image.py
@@ -62,8 +62,8 @@ def test_no_requested_size(create_command, tmp_path, capsys):
 
     # The file was copied into position
     create_command.shutil.copy.assert_called_with(
-        str(create_command.base_path / 'input' / 'original.png'),
-        str(out_path),
+        create_command.base_path / 'input' / 'original.png',
+        out_path,
     )
 
 
@@ -116,8 +116,8 @@ def test_requested_size(create_command, tmp_path, capsys):
 
     # The file was copied into position
     create_command.shutil.copy.assert_called_with(
-        str(create_command.base_path / 'input' / 'original-3742.png'),
-        str(out_path),
+        create_command.base_path / 'input' / 'original-3742.png',
+        out_path,
     )
 
 
@@ -172,8 +172,8 @@ def test_variant_with_no_requested_size(create_command, tmp_path, capsys):
 
     # The file was copied into position
     create_command.shutil.copy.assert_called_with(
-        str(create_command.base_path / 'input' / 'original.png'),
-        str(out_path),
+        create_command.base_path / 'input' / 'original.png',
+        out_path,
     )
 
 
@@ -264,8 +264,8 @@ def test_variant_with_size(create_command, tmp_path, capsys):
 
     # The file was copied into position
     create_command.shutil.copy.assert_called_with(
-        str(create_command.base_path / 'input' / 'original-3742.png'),
-        str(out_path),
+        create_command.base_path / 'input' / 'original-3742.png',
+        out_path,
     )
 
 
@@ -332,6 +332,6 @@ def test_unsized_variant(create_command, tmp_path, capsys):
 
     # The file was copied into position
     create_command.shutil.copy.assert_called_with(
-        str(create_command.base_path / 'input' / 'original.png'),
-        str(out_path),
+        create_command.base_path / 'input' / 'original.png',
+        out_path,
     )
diff --git a/tests/commands/new/test_new_app.py b/tests/commands/new/test_new_app.py
index a704d80e..b61ced07 100644
--- a/tests/commands/new/test_new_app.py
+++ b/tests/commands/new/test_new_app.py
@@ -1,3 +1,4 @@
+import os
 from unittest import mock
 
 import pytest
@@ -38,7 +39,7 @@ def test_new_app(new_command, tmp_path):
     new_command.cookiecutter.assert_called_with(
         '~/.cookiecutters/briefcase-template',
         no_input=True,
-        output_dir=str(tmp_path),
+        output_dir=os.fsdecode(tmp_path),
         checkout="v0.3",
         extra_context=app_context
     )
@@ -71,7 +72,7 @@ def test_new_app_with_template(new_command, tmp_path):
     new_command.cookiecutter.assert_called_with(
         'https://example.com/other.git',
         no_input=True,
-        output_dir=str(tmp_path),
+        output_dir=os.fsdecode(tmp_path),
         checkout="v0.3",
         extra_context=app_context
     )
diff --git a/tests/integrations/android_sdk/ADB/test_command.py b/tests/integrations/android_sdk/ADB/test_command.py
index e050797d..536c6e23 100644
--- a/tests/integrations/android_sdk/ADB/test_command.py
+++ b/tests/integrations/android_sdk/ADB/test_command.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from pathlib import Path
 
@@ -17,7 +18,7 @@ def test_simple_command(mock_sdk, tmp_path):
     # Check that adb was invoked with the expected commands
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(tmp_path / "sdk" / "platform-tools" / "adb"),
+            os.fsdecode(tmp_path / "sdk" / "platform-tools" / "adb"),
             "-s",
             "exampleDevice",
             "example",
@@ -59,7 +60,7 @@ def test_error_handling(mock_sdk, tmp_path, name, exception):
     # Check that adb was invoked as expected
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(tmp_path / "sdk" / "platform-tools" / "adb"),
+            os.fsdecode(tmp_path / "sdk" / "platform-tools" / "adb"),
             "-s",
             "exampleDevice",
             "example",
diff --git a/tests/integrations/android_sdk/ADB/test_logcat.py b/tests/integrations/android_sdk/ADB/test_logcat.py
index 7057c1e5..88796425 100644
--- a/tests/integrations/android_sdk/ADB/test_logcat.py
+++ b/tests/integrations/android_sdk/ADB/test_logcat.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from unittest.mock import MagicMock
 
@@ -18,7 +19,7 @@ def test_logcat(mock_sdk):
     # Validate call parameters.
     mock_sdk.command.subprocess.run.assert_called_once_with(
         [
-            str(mock_sdk.adb_path),
+            os.fsdecode(mock_sdk.adb_path),
             "-s",
             "exampleDevice",
             "logcat",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py
index 1df6ece5..6e73bcc9 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_create_emulator.py
@@ -1,4 +1,6 @@
+import os
 import subprocess
+import sys
 from unittest.mock import MagicMock
 
 import pytest
@@ -6,6 +8,7 @@ from requests import exceptions as requests_exceptions
 
 from briefcase.exceptions import BriefcaseCommandError, NetworkFailure
 from briefcase.integrations.android_sdk import AndroidSDK
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -38,9 +41,13 @@ def test_create_emulator(mock_sdk, tmp_path):
         'new-emulator'
     ]
 
-    # Mock the result of the download of a skin
-    skin_tgz_path = MagicMock()
-    skin_tgz_path.__str__.return_value = '/path/to/skin.tgz'
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        skin_tgz_path = FsPathMock("/path/to/skin.tgz")
+    else:
+        skin_tgz_path = MagicMock()
+        skin_tgz_path.__fspath__.return_value = "/path/to/skin.tgz"
     mock_sdk.command.download_url.return_value = skin_tgz_path
 
     # Mock the initial output of an AVD config file.
@@ -58,7 +65,7 @@ def test_create_emulator(mock_sdk, tmp_path):
     # avdmanager was invoked
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(mock_sdk.avdmanager_path),
+            os.fsdecode(mock_sdk.avdmanager_path),
             "--verbose",
             "create", "avd",
             "--name", "new-emulator",
@@ -79,10 +86,11 @@ def test_create_emulator(mock_sdk, tmp_path):
         download_path=mock_sdk.root_path,
     )
 
-    # Skin is unpacked
+    # Skin is unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_sdk.command.shutil.unpack_archive.assert_called_once_with(
-        str(skin_tgz_path),
-        extract_dir=str(mock_sdk.root_path / "skins" / "pixel_3a")
+        os.fsdecode(skin_tgz_path),
+        extract_dir=os.fsdecode(mock_sdk.root_path / "skins" / "pixel_3a")
     )
 
     # Original file was deleted.
@@ -121,7 +129,7 @@ def test_create_preexisting_skins(mock_sdk, tmp_path):
     # avdmanager was invoked
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(mock_sdk.avdmanager_path),
+            os.fsdecode(mock_sdk.avdmanager_path),
             "--verbose",
             "create", "avd",
             "--name", "new-emulator",
@@ -161,7 +169,7 @@ def test_create_failure(mock_sdk):
     # but avdmanager was invoked
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(mock_sdk.avdmanager_path),
+            os.fsdecode(mock_sdk.avdmanager_path),
             "--verbose",
             "create", "avd",
             "--name", "new-emulator",
@@ -190,7 +198,7 @@ def test_download_failure(mock_sdk, tmp_path):
     # avdmanager was invoked
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(mock_sdk.avdmanager_path),
+            os.fsdecode(mock_sdk.avdmanager_path),
             "--verbose",
             "create", "avd",
             "--name", "new-emulator",
@@ -221,8 +229,13 @@ def test_unpack_failure(mock_sdk, tmp_path):
     mock_sdk.command.input.return_value = 'new-emulator'
 
     # Mock the result of the download of a skin
-    skin_tgz_path = MagicMock()
-    skin_tgz_path.__str__.return_value = '/path/to/skin.tgz'
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        skin_tgz_path = FsPathMock("/path/to/skin.tgz")
+    else:
+        skin_tgz_path = MagicMock()
+        skin_tgz_path.__fspath__.return_value = "/path/to/skin.tgz"
     mock_sdk.command.download_url.return_value = skin_tgz_path
 
     # Mock a failure unpacking the skin
@@ -235,7 +248,7 @@ def test_unpack_failure(mock_sdk, tmp_path):
     # avdmanager was invoked
     mock_sdk.command.subprocess.check_output.assert_called_once_with(
         [
-            str(mock_sdk.avdmanager_path),
+            os.fsdecode(mock_sdk.avdmanager_path),
             "--verbose",
             "create", "avd",
             "--name", "new-emulator",
@@ -256,10 +269,11 @@ def test_unpack_failure(mock_sdk, tmp_path):
         download_path=mock_sdk.root_path,
     )
 
-    # An attempt to unpack the skin was made
+    # An attempt to unpack the skin was made.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_sdk.command.shutil.unpack_archive.assert_called_once_with(
-        str(skin_tgz_path),
-        extract_dir=str(mock_sdk.root_path / "skins" / "pixel_3a")
+        os.fsdecode(skin_tgz_path),
+        extract_dir=os.fsdecode(mock_sdk.root_path / "skins" / "pixel_3a")
     )
 
     # Original file wasn't deleted.
@@ -280,6 +294,12 @@ def test_default_name(mock_sdk, tmp_path):
     with avd_config_path.open('w') as f:
         f.write('hw.device.name=pixel\n')
 
+    # Consider to remove if block when we drop py3.7 support.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        skin_tgz_path = FsPathMock("")
+        mock_sdk.command.download_url.return_value = skin_tgz_path
+
     # Create the emulator
     avd = mock_sdk.create_emulator()
 
@@ -307,6 +327,12 @@ def test_default_name_with_collisions(mock_sdk, tmp_path):
     with avd_config_path.open('w') as f:
         f.write('hw.device.name=pixel\n')
 
+    # Consider to remove if block when we drop py3.7 support.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        skin_tgz_path = FsPathMock("")
+        mock_sdk.command.download_url.return_value = skin_tgz_path
+
     # Create the emulator
     avd = mock_sdk.create_emulator()
 
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_properties.py b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
index f7e5d1ff..51a4873a 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_properties.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
@@ -1,3 +1,4 @@
+import os
 from pathlib import Path
 
 import pytest
@@ -93,8 +94,8 @@ def test_avd_path(mock_sdk, tmp_path):
 def test_simple_env(mock_sdk, tmp_path):
     "The SDK Environment can be constructed"
     assert mock_sdk.env == {
-        'JAVA_HOME': str(Path('/path/to/jdk')),
-        'ANDROID_SDK_ROOT': str(tmp_path / 'sdk')
+        'JAVA_HOME': os.fsdecode(Path('/path/to/jdk')),
+        'ANDROID_SDK_ROOT': os.fsdecode(tmp_path / 'sdk')
     }
 
 
@@ -108,8 +109,8 @@ def test_override_env(mock_sdk, tmp_path):
 
     assert mock_sdk.env == {
         'other': 'stuff',
-        'JAVA_HOME': str(Path('/path/to/jdk')),
-        'ANDROID_SDK_ROOT': str(tmp_path / 'sdk')
+        'JAVA_HOME': os.fsdecode(Path('/path/to/jdk')),
+        'ANDROID_SDK_ROOT': os.fsdecode(tmp_path / 'sdk')
     }
 
 
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
index 3887be6f..6ff1f8e1 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_start_emulator.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from unittest.mock import MagicMock, call
 
@@ -104,7 +105,7 @@ def test_start_emulator(mock_sdk):
     # The process was started.
     mock_sdk.command.subprocess.Popen.assert_called_with(
         [
-            str(mock_sdk.emulator_path),
+            os.fsdecode(mock_sdk.emulator_path),
             '@idleEmulator',
             '-dns-server', '8.8.8.8',
         ],
@@ -192,7 +193,7 @@ def test_emulator_fail_to_start(mock_sdk):
     # The process was started.
     mock_sdk.command.subprocess.Popen.assert_called_with(
         [
-            str(mock_sdk.emulator_path),
+            os.fsdecode(mock_sdk.emulator_path),
             '@idleEmulator',
             '-dns-server', '8.8.8.8',
         ],
@@ -274,7 +275,7 @@ def test_emulator_fail_to_boot(mock_sdk):
     # The process was started.
     mock_sdk.command.subprocess.Popen.assert_called_with(
         [
-            str(mock_sdk.emulator_path),
+            os.fsdecode(mock_sdk.emulator_path),
             '@idleEmulator',
             '-dns-server', '8.8.8.8',
         ],
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
index 14f7ffd5..bb473cb1 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_upgrade.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 
 import pytest
@@ -10,7 +11,7 @@ def test_upgrade(mock_sdk):
     mock_sdk.upgrade()
 
     mock_sdk.command.subprocess.run.assert_called_once_with(
-        [str(mock_sdk.sdkmanager_path), "--update"],
+        [os.fsdecode(mock_sdk.sdkmanager_path), "--update"],
         env=mock_sdk.env,
         check=True,
     )
@@ -23,7 +24,7 @@ def test_upgrade_failure(mock_sdk):
         mock_sdk.upgrade()
 
     mock_sdk.command.subprocess.run.assert_called_once_with(
-        [str(mock_sdk.sdkmanager_path), "--update"],
+        [os.fsdecode(mock_sdk.sdkmanager_path), "--update"],
         env=mock_sdk.env,
         check=True,
     )
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
index c3706c95..14b8c3e8 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
@@ -12,6 +12,7 @@ from briefcase.exceptions import (
     NetworkFailure
 )
 from briefcase.integrations.android_sdk import AndroidSDK
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -98,7 +99,7 @@ def test_user_provided_sdk(mock_command, tmp_path):
 
     # Set the environment to specify ANDROID_SDK_ROOT
     mock_command.os.environ = {
-        'ANDROID_SDK_ROOT': str(existing_android_sdk_root_path)
+        'ANDROID_SDK_ROOT': os.fsdecode(existing_android_sdk_root_path)
     }
 
     # Expect verify() to succeed
@@ -111,8 +112,7 @@ def test_user_provided_sdk(mock_command, tmp_path):
     mock_command.shutil.unpack_archive.assert_not_called()
 
     # The returned SDK has the expected root path.
-    # FIXME: The conversion to str is needed for Python 3.5 compatibility.
-    assert str(sdk.root_path) == str(existing_android_sdk_root_path)
+    assert sdk.root_path == existing_android_sdk_root_path
 
 
 def test_invalid_user_provided_sdk(mock_command, tmp_path):
@@ -130,7 +130,7 @@ def test_invalid_user_provided_sdk(mock_command, tmp_path):
 
     # Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
     mock_command.os.environ = {
-        'ANDROID_SDK_ROOT': str(tmp_path / "other_sdk")
+        'ANDROID_SDK_ROOT': os.fsdecode(tmp_path / "other_sdk")
     }
 
     # Expect verify() to succeed
@@ -154,9 +154,14 @@ def test_download_sdk(mock_command, tmp_path, host_os):
     # Mock-out `host_os` so we only do our permission check on non-Windows.
     mock_command.host_os = host_os
 
-    # The download will produce a cached file
-    cache_file = MagicMock()
-    cache_file.__str__.return_value = "/path/to/download.zip"
+    # The download will produce a cached file.
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        cache_file = FsPathMock("/path/to/download.zip")
+    else:
+        cache_file = MagicMock()
+        cache_file.__fspath__.return_value = "/path/to/download.zip"
     mock_command.download_url.return_value = cache_file
 
     # Create a file that would have been created by unpacking the archive
@@ -178,17 +183,18 @@ def test_download_sdk(mock_command, tmp_path, host_os):
         url=url,
         download_path=mock_command.tools_path,
     )
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_once_with(
         "/path/to/download.zip",
-        extract_dir=str(android_sdk_root_path)
+        extract_dir=os.fsdecode(android_sdk_root_path)
     )
 
-    # The cached file will be deleeted
+    # The cached file will be deleted
     cache_file.unlink.assert_called_once_with()
 
     # On non-Windows, ensure the unpacked binary was made executable
     if host_os != 'Windows':
-        assert os.access(str(example_tool), os.X_OK)
+        assert os.access(example_tool, os.X_OK)
 
     # The license has been accepted
     assert (android_sdk_root_path / "licenses" / "android-sdk-license").exists()
@@ -221,8 +227,13 @@ def test_download_sdk_if_sdkmanager_not_executable(mock_command, tmp_path):
     (android_sdk_root_path / "tools" / "bin" / "sdkmanager").touch(mode=0o644)
 
     # The download will produce a cached file
-    cache_file = MagicMock()
-    cache_file.__str__.return_value = "/path/to/download.zip"
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        cache_file = FsPathMock("/path/to/download.zip")
+    else:
+        cache_file = MagicMock()
+        cache_file.__fspath__.return_value = "/path/to/download.zip"
     mock_command.download_url.return_value = cache_file
 
     # Set up a side effect for accepting the license
@@ -236,9 +247,10 @@ def test_download_sdk_if_sdkmanager_not_executable(mock_command, tmp_path):
         url="https://dl.google.com/android/repository/sdk-tools-unknown-4333796.zip",
         download_path=mock_command.tools_path,
     )
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_once_with(
         "/path/to/download.zip",
-        extract_dir=str(android_sdk_root_path)
+        extract_dir=os.fsdecode(android_sdk_root_path)
     )
 
     # The cached file will be deleted
@@ -272,8 +284,13 @@ def test_detects_bad_zipfile(mock_command, tmp_path):
     android_sdk_root_path = tmp_path / "tools" / "android_sdk"
 
     # The download will produce a cached file
-    cache_file = MagicMock()
-    cache_file.__str__.return_value = "/path/to/download.zip"
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        cache_file = FsPathMock("/path/to/download.zip")
+    else:
+        cache_file = MagicMock()
+        cache_file.__fspath__.return_value = "/path/to/download.zip"
     mock_command.download_url.return_value = cache_file
 
     # But the unpack will fail.
@@ -287,7 +304,8 @@ def test_detects_bad_zipfile(mock_command, tmp_path):
         url="https://dl.google.com/android/repository/sdk-tools-unknown-4333796.zip",
         download_path=mock_command.tools_path,
     )
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_once_with(
         "/path/to/download.zip",
-        extract_dir=str(android_sdk_root_path)
+        extract_dir=os.fsdecode(android_sdk_root_path)
     )
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
index 03a9cbb4..69bc2aec 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_emulator.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 
 import pytest
@@ -23,7 +24,7 @@ def test_installs_android_emulator(mock_sdk):
 
     mock_sdk.command.subprocess.run.assert_called_once_with(
         [
-            str(mock_sdk.sdkmanager_path),
+            os.fsdecode(mock_sdk.sdkmanager_path),
             "platforms;android-28",
             "system-images;android-28;default;x86",
             "emulator",
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py b/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
index 5e22f9d3..cf33a835 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify_license.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 
 import pytest
@@ -31,7 +32,7 @@ def test_verify_license_prompts_for_licenses_and_exits_if_you_agree(mock_sdk):
     mock_sdk.command.subprocess.run.side_effect = accept_license
     mock_sdk.verify_license()
     mock_sdk.command.subprocess.run.assert_called_once_with(
-        [str(mock_sdk.sdkmanager_path), "--licenses"],
+        [os.fsdecode(mock_sdk.sdkmanager_path), "--licenses"],
         env=mock_sdk.env,
         check=True,
     )
@@ -45,7 +46,7 @@ def test_verify_license_handles_sdkmanager_crash(mock_sdk):
         mock_sdk.verify_license()
 
     mock_sdk.command.subprocess.run.assert_called_once_with(
-        [str(mock_sdk.sdkmanager_path), "--licenses"],
+        [os.fsdecode(mock_sdk.sdkmanager_path), "--licenses"],
         env=mock_sdk.env,
         check=True,
     )
@@ -60,7 +61,7 @@ def test_verify_license_insists_on_agreement(mock_sdk):
         mock_sdk.verify_license()
 
     mock_sdk.command.subprocess.run.assert_called_once_with(
-        [str(mock_sdk.sdkmanager_path), "--licenses"],
+        [os.fsdecode(mock_sdk.sdkmanager_path), "--licenses"],
         env=mock_sdk.env,
         check=True,
     )
diff --git a/tests/integrations/docker/test_Docker__prepare.py b/tests/integrations/docker/test_Docker__prepare.py
index f837cc80..919bb614 100644
--- a/tests/integrations/docker/test_Docker__prepare.py
+++ b/tests/integrations/docker/test_Docker__prepare.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 
 import pytest
@@ -15,12 +16,12 @@ def test_prepare(mock_docker, tmp_path):
             'docker',
             'build',
             '--tag', 'briefcase/com.example.myapp:py3.X',
-            '--file', str(tmp_path / 'bundle' / 'Dockerfile'),
+            '--file', os.fsdecode(tmp_path / 'bundle' / 'Dockerfile'),
             '--build-arg', "PY_VERSION=3.X",
             '--build-arg', "SYSTEM_REQUIRES=things==1.2 stuff>=3.4",
             '--build-arg', "HOST_UID=37",
             '--build-arg', "HOST_GID=42",
-            str(tmp_path / "base" / "path" / "to" / "src")
+            os.fsdecode(tmp_path / "base" / "path" / "to" / "src")
         ],
         check=True,
     )
@@ -41,12 +42,12 @@ def test_prepare_failure(mock_docker, tmp_path):
             'docker',
             'build',
             '--tag', 'briefcase/com.example.myapp:py3.X',
-            '--file', str(tmp_path / 'bundle' / 'Dockerfile'),
+            '--file', os.fsdecode(tmp_path / 'bundle' / 'Dockerfile'),
             '--build-arg', "PY_VERSION=3.X",
             '--build-arg', "SYSTEM_REQUIRES=things==1.2 stuff>=3.4",
             '--build-arg', "HOST_UID=37",
             '--build-arg', "HOST_GID=42",
-            str(tmp_path / "base" / "path" / "to" / "src")
+            os.fsdecode(tmp_path / "base" / "path" / "to" / "src")
         ],
         check=True,
     )
diff --git a/tests/integrations/docker/test_Docker__run.py b/tests/integrations/docker/test_Docker__run.py
index 2973088e..aedbcb2d 100644
--- a/tests/integrations/docker/test_Docker__run.py
+++ b/tests/integrations/docker/test_Docker__run.py
@@ -1,3 +1,4 @@
+import os
 import sys
 
 import pytest
@@ -67,9 +68,9 @@ def test_simple_call_with_path_arg(mock_docker, tmp_path, capsys):
             ),
             'briefcase/com.example.myapp:py3.X',
             'hello',
-            str(tmp_path / 'location')
+            os.fsdecode(tmp_path / 'location')
         ],
-        cwd=str(tmp_path / 'cwd')
+        cwd=os.fsdecode(tmp_path / 'cwd')
     )
     assert capsys.readouterr().out == ""
 
diff --git a/tests/integrations/java/test_JDK__verify.py b/tests/integrations/java/test_JDK__verify.py
index e9dc0cd8..ffe28551 100644
--- a/tests/integrations/java/test_JDK__verify.py
+++ b/tests/integrations/java/test_JDK__verify.py
@@ -1,5 +1,7 @@
+import os
 import shutil
 import subprocess
+import sys
 from pathlib import Path
 from unittest import mock
 
@@ -12,6 +14,7 @@ from briefcase.exceptions import (
     NetworkFailure
 )
 from briefcase.integrations.java import JDK
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -51,7 +54,7 @@ def test_macos_tool_java_home(test_command, capsys):
         ),
         # Second is a call to verify a valid Java version
         mock.call(
-            [str(Path('/path/to/java/bin/javac')), '-version'],
+            [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
             universal_newlines=True,
             stderr=subprocess.STDOUT,
         ),
@@ -116,7 +119,7 @@ def test_macos_provided_overrides_tool_java_home(test_command, capsys):
 
     # A single call to check output
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/java/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -143,7 +146,7 @@ def test_valid_provided_java_home(test_command, capsys):
 
     # A single call to check output
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/java/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -173,7 +176,7 @@ def test_invalid_jdk_version(test_command, tmp_path, capsys):
 
     # A single call was made to check javac
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/java/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -203,7 +206,7 @@ def test_no_javac(test_command, tmp_path, capsys):
 
     # A single call was made to check javac
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/nowhere/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/nowhere/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -235,7 +238,7 @@ def test_javac_error(test_command, tmp_path, capsys):
 
     # A single call was made to check javac
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/java/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -265,7 +268,7 @@ def test_unparseable_javac_version(test_command, tmp_path, capsys):
 
     # A single call was made to check javac
     test_command.subprocess.check_output.assert_called_once_with(
-        [str(Path('/path/to/java/bin/javac')), '-version'],
+        [os.fsdecode(Path('/path/to/java/bin/javac')), '-version'],
         universal_newlines=True,
         stderr=subprocess.STDOUT,
     ),
@@ -308,8 +311,13 @@ def test_successful_jdk_download(test_command, tmp_path, capsys, host_os, jdk_ur
     test_command.os.environ.get = mock.MagicMock(return_value='/does/not/exist')
 
     # Mock the cached download path
-    archive = mock.MagicMock()
-    archive.__str__.return_value = '/path/to/download.zip'
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        archive = FsPathMock("/path/to/download.zip")
+    else:
+        archive = mock.MagicMock()
+        archive.__fspath__.return_value = "/path/to/download.zip"
     test_command.download_url.return_value = archive
 
     # Create a directory to make it look like Java was downloaded and unpacked.
@@ -331,9 +339,10 @@ def test_successful_jdk_download(test_command, tmp_path, capsys, host_os, jdk_ur
         download_path=tmp_path / "tools",
     )
     # The archive was unpacked
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     test_command.shutil.unpack_archive.assert_called_with(
-        '/path/to/download.zip',
-        extract_dir=str(tmp_path / "tools")
+        "/path/to/download.zip",
+        extract_dir=os.fsdecode(tmp_path / "tools")
     )
     # The original archive was deleted
     archive.unlink.assert_called_once_with()
@@ -380,8 +389,13 @@ def test_invalid_jdk_archive(test_command, tmp_path):
     test_command.host_os = 'Linux'
 
     # Mock the cached download path
-    archive = mock.MagicMock()
-    archive.__str__.return_value = '/path/to/download.zip'
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        archive = FsPathMock("/path/to/download.zip")
+    else:
+        archive = mock.MagicMock()
+        archive.__fspath__.return_value = "/path/to/download.zip"
     test_command.download_url.return_value = archive
 
     # Mock an unpack failure due to an invalid archive
@@ -396,10 +410,11 @@ def test_invalid_jdk_archive(test_command, tmp_path):
             "jdk8u242-b08/OpenJDK8U-jdk_x64_linux_hotspot_8u242b08.tar.gz",
         download_path=tmp_path / "tools",
     )
-    # An attempt was made to unpack the archive
+    # An attempt was made to unpack the archive.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     test_command.shutil.unpack_archive.assert_called_with(
-        '/path/to/download.zip',
-        extract_dir=str(tmp_path / "tools")
+        "/path/to/download.zip",
+        extract_dir=os.fsdecode(tmp_path / "tools")
     )
     # The original archive was not deleted
     assert archive.unlink.call_count == 0
diff --git a/tests/integrations/java/test_JDK_upgrade.py b/tests/integrations/java/test_JDK_upgrade.py
index 4313390f..fe0c3ca1 100644
--- a/tests/integrations/java/test_JDK_upgrade.py
+++ b/tests/integrations/java/test_JDK_upgrade.py
@@ -1,4 +1,7 @@
+import os
 import shutil
+import sys
+
 from unittest.mock import MagicMock
 
 import pytest
@@ -11,6 +14,7 @@ from briefcase.exceptions import (
     NonManagedToolError
 )
 from briefcase.integrations.java import JDK
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -56,12 +60,17 @@ def test_existing_install(test_command, tmp_path):
 
     # We actually need to delete the original java install
     def rmtree(path):
-        shutil.rmtree(str(path))
+        shutil.rmtree(path)
     test_command.shutil.rmtree.side_effect = rmtree
 
-    # Mock the cached download path
-    archive = MagicMock()
-    archive.__str__.return_value = '/path/to/download.zip'
+    # Mock the cached download path.
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        archive = FsPathMock("/path/to/download.zip")
+    else:
+        archive = MagicMock()
+        archive.__fspath__.return_value = "/path/to/download.zip"
     test_command.download_url.return_value = archive
 
     # Create a directory to make it look like Java was downloaded and unpacked.
@@ -83,10 +92,11 @@ def test_existing_install(test_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The archive was unpacked
+    # The archive was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     test_command.shutil.unpack_archive.assert_called_with(
-        '/path/to/download.zip',
-        extract_dir=str(tmp_path / "tools")
+        "/path/to/download.zip",
+        extract_dir=os.fsdecode(tmp_path / "tools")
     )
     # The original archive was deleted
     archive.unlink.assert_called_once_with()
@@ -103,12 +113,17 @@ def test_macOS_existing_install(test_command, tmp_path):
 
     # We actually need to delete the original java install
     def rmtree(path):
-        shutil.rmtree(str(path))
+        shutil.rmtree(path)
     test_command.shutil.rmtree.side_effect = rmtree
 
-    # Mock the cached download path
-    archive = MagicMock()
-    archive.__str__.return_value = '/path/to/download.zip'
+    # Mock the cached download path.
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        archive = FsPathMock("/path/to/download.zip")
+    else:
+        archive = MagicMock()
+        archive.__fspath__.return_value = "/path/to/download.zip"
     test_command.download_url.return_value = archive
 
     # Create a directory to make it look like Java was downloaded and unpacked.
@@ -130,10 +145,11 @@ def test_macOS_existing_install(test_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The archive was unpacked
+    # The archive was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     test_command.shutil.unpack_archive.assert_called_with(
-        '/path/to/download.zip',
-        extract_dir=str(tmp_path / "tools")
+        "/path/to/download.zip",
+        extract_dir=os.fsdecode(tmp_path / "tools")
     )
     # The original archive was deleted
     archive.unlink.assert_called_once_with()
@@ -147,7 +163,7 @@ def test_download_fail(test_command, tmp_path):
 
     # We actually need to delete the original java install
     def rmtree(path):
-        shutil.rmtree(str(path))
+        shutil.rmtree(path)
     test_command.shutil.rmtree.side_effect = rmtree
 
     # Mock a failure on download
@@ -182,12 +198,17 @@ def test_unpack_fail(test_command, tmp_path):
 
     # We actually need to delete the original java install
     def rmtree(path):
-        shutil.rmtree(str(path))
+        shutil.rmtree(path)
     test_command.shutil.rmtree.side_effect = rmtree
 
     # Mock the cached download path
-    archive = MagicMock()
-    archive.__str__.return_value = '/path/to/download.zip'
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        archive = FsPathMock("/path/to/download.zip")
+    else:
+        archive = MagicMock()
+        archive.__fspath__.return_value = "/path/to/download.zip"
     test_command.download_url.return_value = archive
 
     # Mock an unpack failure due to an invalid archive
@@ -210,10 +231,11 @@ def test_unpack_fail(test_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The archive was unpacked
+    # The archive was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     test_command.shutil.unpack_archive.assert_called_with(
-        '/path/to/download.zip',
-        extract_dir=str(tmp_path / "tools")
+        "/path/to/download.zip",
+        extract_dir=os.fsdecode(tmp_path / "tools")
     )
     # The original archive was not deleted
     assert archive.unlink.call_count == 0
diff --git a/tests/integrations/subprocess/test_Subprocess__Popen.py b/tests/integrations/subprocess/test_Subprocess__Popen.py
index cc2855f3..7f5d13a7 100644
--- a/tests/integrations/subprocess/test_Subprocess__Popen.py
+++ b/tests/integrations/subprocess/test_Subprocess__Popen.py
@@ -1,3 +1,5 @@
+import os
+
 
 def test_simple_call(mock_sub, capsys):
     "A simple call will be invoked"
@@ -26,8 +28,8 @@ def test_simple_call_with_path_arg(mock_sub, capsys, tmp_path):
     mock_sub.Popen(['hello', tmp_path / 'location'], cwd=tmp_path / 'cwd')
 
     mock_sub._subprocess.Popen.assert_called_with(
-        ['hello', str(tmp_path / 'location')],
-        cwd=str(tmp_path / 'cwd')
+        ['hello', os.fsdecode(tmp_path / 'location')],
+        cwd=os.fsdecode(tmp_path / 'cwd')
     )
     assert capsys.readouterr().out == ""
 
diff --git a/tests/integrations/subprocess/test_Subprocess__check_output.py b/tests/integrations/subprocess/test_Subprocess__check_output.py
index 8cf69ce0..992e99bf 100644
--- a/tests/integrations/subprocess/test_Subprocess__check_output.py
+++ b/tests/integrations/subprocess/test_Subprocess__check_output.py
@@ -1,3 +1,5 @@
+import os
+
 
 def test_simple_call(mock_sub, capsys):
     "A simple call will be invoked"
@@ -26,8 +28,8 @@ def test_simple_call_with_path_arg(mock_sub, capsys, tmp_path):
     mock_sub.check_output(['hello', tmp_path / 'location'], cwd=tmp_path / 'cwd')
 
     mock_sub._subprocess.check_output.assert_called_with(
-        ['hello', str(tmp_path / 'location')],
-        cwd=str(tmp_path / 'cwd')
+        ['hello', os.fsdecode(tmp_path / 'location')],
+        cwd=os.fsdecode(tmp_path / 'cwd')
     )
     assert capsys.readouterr().out == ""
 
diff --git a/tests/integrations/subprocess/test_Subprocess__run.py b/tests/integrations/subprocess/test_Subprocess__run.py
index de6b865e..e551b64d 100644
--- a/tests/integrations/subprocess/test_Subprocess__run.py
+++ b/tests/integrations/subprocess/test_Subprocess__run.py
@@ -1,3 +1,5 @@
+import os
+
 
 def test_simple_call(mock_sub, capsys):
     "A simple call will be invoked"
@@ -26,8 +28,8 @@ def test_simple_call_with_path_arg(mock_sub, capsys, tmp_path):
     mock_sub.run(['hello', tmp_path / 'location'], cwd=tmp_path / 'cwd')
 
     mock_sub._subprocess.run.assert_called_with(
-        ['hello', str(tmp_path / 'location')],
-        cwd=str(tmp_path / 'cwd')
+        ['hello', os.fsdecode(tmp_path / 'location')],
+        cwd=os.fsdecode(tmp_path / 'cwd')
     )
     assert capsys.readouterr().out == ""
 
diff --git a/tests/integrations/wix/test_WiX__upgrade.py b/tests/integrations/wix/test_WiX__upgrade.py
index 568ffe1c..40681c82 100644
--- a/tests/integrations/wix/test_WiX__upgrade.py
+++ b/tests/integrations/wix/test_WiX__upgrade.py
@@ -1,3 +1,5 @@
+import os
+import sys
 from unittest.mock import MagicMock
 
 import pytest
@@ -10,6 +12,7 @@ from briefcase.exceptions import (
     NonManagedToolError
 )
 from briefcase.integrations.wix import WIX_DOWNLOAD_URL, WiX
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -59,9 +62,14 @@ def test_existing_wix_install(mock_command, tmp_path):
     # Mock the download
     wix_path = tmp_path / 'tools' / 'wix'
 
-    wix_zip_path = tmp_path / 'tools' / 'wix.zip'
-    wix_zip = MagicMock()
-    wix_zip.__str__.return_value = str(wix_zip_path)
+    wix_zip_path = os.fsdecode(tmp_path / 'tools' / 'wix.zip')
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        wix_zip = FsPathMock(wix_zip_path)
+    else:
+        wix_zip = MagicMock()
+        wix_zip.__fspath__.return_value = wix_zip_path
 
     mock_command.download_url.return_value = wix_zip
 
@@ -81,9 +89,10 @@ def test_existing_wix_install(mock_command, tmp_path):
     )
 
     # The download was unpacked
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_with(
-        str(wix_zip_path),
-        extract_dir=str(wix_path)
+        os.fsdecode(wix_zip_path),
+        extract_dir=os.fsdecode(wix_path)
     )
 
     # The zip file was removed
@@ -129,9 +138,14 @@ def test_unpack_fail(mock_command, tmp_path):
     (wix_path / 'candle.exe').touch()
 
     # Mock the download
-    wix_zip_path = tmp_path / 'tools' / 'wix.zip'
-    wix_zip = MagicMock()
-    wix_zip.__str__.return_value = str(wix_zip_path)
+    wix_zip_path = os.fsdecode(tmp_path / 'tools' / 'wix.zip')
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        wix_zip = FsPathMock(wix_zip_path)
+    else:
+        wix_zip = MagicMock()
+        wix_zip.__fspath__.return_value = wix_zip_path
 
     mock_command.download_url.return_value = wix_zip
 
@@ -152,10 +166,11 @@ def test_unpack_fail(mock_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The download was unpacked
+    # The download was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_with(
-        str(wix_zip_path),
-        extract_dir=str(wix_path)
+        os.fsdecode(wix_zip_path),
+        extract_dir=os.fsdecode(wix_path)
     )
 
     # The zip file was not removed
diff --git a/tests/integrations/wix/test_WiX__verify.py b/tests/integrations/wix/test_WiX__verify.py
index 299edf37..bf8407b1 100644
--- a/tests/integrations/wix/test_WiX__verify.py
+++ b/tests/integrations/wix/test_WiX__verify.py
@@ -1,3 +1,5 @@
+import os
+import sys
 from unittest.mock import MagicMock
 
 import pytest
@@ -9,6 +11,7 @@ from briefcase.exceptions import (
     NetworkFailure
 )
 from briefcase.integrations.wix import WIX_DOWNLOAD_URL, WiX
+from tests.utils import FsPathMock
 
 
 @pytest.fixture
@@ -34,7 +37,7 @@ def test_valid_wix_envvar(mock_command, tmp_path):
     "If the WiX envvar points to a valid WiX install, the validator succeeds"
     # Mock the environment for a WiX install
     wix_path = tmp_path / 'wix'
-    mock_command.os.environ.get.return_value = str(wix_path)
+    mock_command.os.environ.get.return_value = os.fsdecode(wix_path)
 
     # Mock the interesting parts of a WiX install
     (wix_path / 'bin').mkdir(parents=True)
@@ -49,16 +52,16 @@ def test_valid_wix_envvar(mock_command, tmp_path):
     mock_command.os.environ.get.assert_called_with('WIX')
 
     # The returned paths are as expected (and are the full paths)
-    assert str(wix.heat_exe) == str(tmp_path / 'wix' / 'bin' / 'heat.exe')
-    assert str(wix.light_exe) == str(tmp_path / 'wix' / 'bin' / 'light.exe')
-    assert str(wix.candle_exe) == str(tmp_path / 'wix' / 'bin' / 'candle.exe')
+    assert wix.heat_exe == tmp_path / 'wix' / 'bin' / 'heat.exe'
+    assert wix.light_exe == tmp_path / 'wix' / 'bin' / 'light.exe'
+    assert wix.candle_exe == tmp_path / 'wix' / 'bin' / 'candle.exe'
 
 
 def test_invalid_wix_envvar(mock_command, tmp_path):
     "If the WiX envvar points to an invalid WiX install, the validator fails"
     # Mock the environment for a WiX install
     wix_path = tmp_path / 'wix'
-    mock_command.os.environ.get.return_value = str(wix_path)
+    mock_command.os.environ.get.return_value = os.fsdecode(wix_path)
 
     # Don't create the actual install, and then attempt to validate
     with pytest.raises(BriefcaseCommandError, match="does not point to an install"):
@@ -99,9 +102,14 @@ def test_download_wix(mock_command, tmp_path):
     # Mock the download
     wix_path = tmp_path / 'tools' / 'wix'
 
-    wix_zip_path = tmp_path / 'tools' / 'wix.zip'
-    wix_zip = MagicMock()
-    wix_zip.__str__.return_value = str(wix_zip_path)
+    wix_zip_path = os.fsdecode(tmp_path / 'tools' / 'wix.zip')
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        wix_zip = FsPathMock(wix_zip_path)
+    else:
+        wix_zip = MagicMock()
+        wix_zip.__fspath__.return_value = wix_zip_path
 
     mock_command.download_url.return_value = wix_zip
 
@@ -117,10 +125,11 @@ def test_download_wix(mock_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The download was unpacked
+    # The download was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_with(
-        str(wix_zip_path),
-        extract_dir=str(wix_path)
+        os.fsdecode(wix_zip_path),
+        extract_dir=os.fsdecode(wix_path)
     )
 
     # The zip file was removed
@@ -181,9 +190,14 @@ def test_unpack_fail(mock_command, tmp_path):
     # Mock the download
     wix_path = tmp_path / 'tools' / 'wix'
 
-    wix_zip_path = tmp_path / 'tools' / 'wix.zip'
-    wix_zip = MagicMock()
-    wix_zip.__str__.return_value = str(wix_zip_path)
+    wix_zip_path = os.fsdecode(tmp_path / 'tools' / 'wix.zip')
+    # Consider to remove if block when we drop py3.7 support, only keep statements from else.
+    # MagicMock below py3.8 doesn't has __fspath__ attribute.
+    if sys.version_info < (3, 8):
+        wix_zip = FsPathMock(wix_zip_path)
+    else:
+        wix_zip = MagicMock()
+        wix_zip.__fspath__.return_value = wix_zip_path
 
     mock_command.download_url.return_value = wix_zip
 
@@ -204,10 +218,11 @@ def test_unpack_fail(mock_command, tmp_path):
         download_path=tmp_path / 'tools',
     )
 
-    # The download was unpacked
+    # The download was unpacked.
+    # TODO: Py3.6 compatibility; os.fsdecode not required in Py3.7
     mock_command.shutil.unpack_archive.assert_called_with(
-        str(wix_zip_path),
-        extract_dir=str(wix_path)
+        os.fsdecode(wix_zip_path),
+        extract_dir=os.fsdecode(wix_path)
     )
 
     # The zip file was not removed
diff --git a/tests/integrations/xcode/test_ensure_xcode_is_installed.py b/tests/integrations/xcode/test_ensure_xcode_is_installed.py
index b4d0272c..b1b2665c 100644
--- a/tests/integrations/xcode/test_ensure_xcode_is_installed.py
+++ b/tests/integrations/xcode/test_ensure_xcode_is_installed.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from unittest import mock
 
@@ -12,7 +13,7 @@ def xcode(tmp_path):
     "Create a dummy location for Xcode"
     xcode_location = tmp_path / 'Xcode.app'
     xcode_location.mkdir(parents=True, exist_ok=True)
-    return str(xcode_location)
+    return os.fsdecode(xcode_location)
 
 
 def test_not_installed(tmp_path):
@@ -27,7 +28,7 @@ def test_not_installed(tmp_path):
     with pytest.raises(BriefcaseCommandError):
         ensure_xcode_is_installed(
             command,
-            xcode_location=str(tmp_path / 'Xcode.app'),
+            xcode_location=tmp_path / 'Xcode.app',
         )
 
     # xcode-select was not invoked
diff --git a/tests/integrations/xcode/test_get_simulators.py b/tests/integrations/xcode/test_get_simulators.py
index fa8e47ae..98614ef9 100644
--- a/tests/integrations/xcode/test_get_simulators.py
+++ b/tests/integrations/xcode/test_get_simulators.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from pathlib import Path
 from unittest import mock
@@ -13,7 +14,7 @@ def simulator(tmp_path):
     "Create a dummy location for the simulator"
     simulator_location = tmp_path / 'CoreSimulator.framework'
     simulator_location.mkdir(parents=True, exist_ok=True)
-    return str(simulator_location)
+    return os.fsdecode(simulator_location)
 
 
 def simctl_result(name):
@@ -31,7 +32,7 @@ def test_simulator_is_missing(tmp_path):
     simulators = get_simulators(
         command,
         'iOS',
-        simulator_location=str(tmp_path / 'CoreSimulator.framework'),
+        simulator_location=tmp_path / 'CoreSimulator.framework',
     )
 
     # The prompt was displayed
diff --git a/tests/platforms/android/gradle/test_build.py b/tests/platforms/android/gradle/test_build.py
index cdaa9409..95b8bbd3 100644
--- a/tests/platforms/android/gradle/test_build.py
+++ b/tests/platforms/android/gradle/test_build.py
@@ -35,10 +35,10 @@ def test_execute_gradle(build_command, first_app_config, host_os, gradlew_name):
     build_command.build_app(first_app_config)
     build_command.subprocess.run.assert_called_once_with(
         [
-            str(build_command.bundle_path(first_app_config) / gradlew_name),
+            build_command.bundle_path(first_app_config) / gradlew_name,
             "assembleDebug",
         ],
-        cwd=str(build_command.bundle_path(first_app_config)),
+        cwd=build_command.bundle_path(first_app_config),
         env=build_command.android_sdk.env,
         check=True,
     )
diff --git a/tests/platforms/android/gradle/test_package.py b/tests/platforms/android/gradle/test_package.py
index 20a518d6..35d27d7f 100644
--- a/tests/platforms/android/gradle/test_package.py
+++ b/tests/platforms/android/gradle/test_package.py
@@ -43,10 +43,10 @@ def test_execute_gradle(package_command, first_app_config, host_os, gradlew_name
     package_command.package_app(first_app_config)
     package_command.subprocess.run.assert_called_once_with(
         [
-            str(package_command.bundle_path(first_app_config) / gradlew_name),
+            package_command.bundle_path(first_app_config) / gradlew_name,
             "bundleRelease",
         ],
-        cwd=str(package_command.bundle_path(first_app_config)),
+        cwd=package_command.bundle_path(first_app_config),
         env=package_command.android_sdk.env,
         check=True,
     )
diff --git a/tests/platforms/android/gradle/test_verify_tools.py b/tests/platforms/android/gradle/test_verify_tools.py
index 98419b4a..54691f11 100644
--- a/tests/platforms/android/gradle/test_verify_tools.py
+++ b/tests/platforms/android/gradle/test_verify_tools.py
@@ -24,7 +24,7 @@ def build_command(tmp_path, first_app_config):
 
     # Override the `os` module so the app has an environment with JAVA_HOME
     command.os = mock.MagicMock(environ={
-        'JAVA_HOME': str(command.java_home_path)
+        'JAVA_HOME': os.fsdecode(command.java_home_path)
     })
     # Enable the command to use `os.access()` and `os.X_OK`.
     command.os.access = os.access
diff --git a/tests/platforms/linux/appimage/test_build.py b/tests/platforms/linux/appimage/test_build.py
index dc56f5c3..bb527cf8 100644
--- a/tests/platforms/linux/appimage/test_build.py
+++ b/tests/platforms/linux/appimage/test_build.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 import sys
 from unittest import mock
@@ -116,25 +117,25 @@ def test_build_appimage(build_command, first_app, tmp_path):
     app_dir = tmp_path / 'linux' / 'appimage' / 'First App' / 'First App.AppDir'
     build_command._subprocess.run.assert_called_with(
         [
-            str(build_command.linuxdeploy.appimage_path),
+            os.fsdecode(build_command.linuxdeploy.appimage_path),
             "--appimage-extract-and-run",
             "--appdir={appdir}".format(appdir=app_dir),
-            "-d", str(app_dir / "com.example.first-app.desktop"),
+            "-d", os.fsdecode(app_dir / "com.example.first-app.desktop"),
             "-o", "appimage",
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app' / 'support'),
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app_packages' / 'firstlib'),
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app_packages' / 'secondlib'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app' / 'support'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app_packages' / 'firstlib'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app_packages' / 'secondlib'),
         ],
         env={
             'PATH': '/usr/local/bin:/usr/bin',
             'VERSION': '0.0.1',
         },
         check=True,
-        cwd=str(tmp_path / 'linux')
+        cwd=os.fsdecode(tmp_path / 'linux')
     )
     # Binary is marked executable
     build_command.os.chmod.assert_called_with(
-        str(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage'),
+        tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage',
         0o755
     )
 
@@ -156,21 +157,21 @@ def test_build_failure(build_command, first_app, tmp_path):
     app_dir = tmp_path / 'linux' / 'appimage' / 'First App' / 'First App.AppDir'
     build_command._subprocess.run.assert_called_with(
         [
-            str(build_command.linuxdeploy.appimage_path),
+            os.fsdecode(build_command.linuxdeploy.appimage_path),
             "--appimage-extract-and-run",
             "--appdir={appdir}".format(appdir=app_dir),
-            "-d", str(app_dir / "com.example.first-app.desktop"),
+            "-d", os.fsdecode(app_dir / "com.example.first-app.desktop"),
             "-o", "appimage",
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app' / 'support'),
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app_packages' / 'firstlib'),
-            "--deploy-deps-only", str(app_dir / 'usr' / 'app_packages' / 'secondlib'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app' / 'support'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app_packages' / 'firstlib'),
+            "--deploy-deps-only", os.fsdecode(app_dir / 'usr' / 'app_packages' / 'secondlib'),
         ],
         env={
             'PATH': '/usr/local/bin:/usr/bin',
             'VERSION': '0.0.1',
         },
         check=True,
-        cwd=str(tmp_path / 'linux')
+        cwd=os.fsdecode(tmp_path / 'linux')
     )
 
     # chmod isn't invoked if the binary wasn't created.
@@ -217,10 +218,10 @@ def test_build_appimage_with_docker(build_command, first_app, tmp_path):
             "--deploy-deps-only", "/app/appimage/First App/First App.AppDir/usr/app_packages/secondlib",
         ],
         check=True,
-        cwd=str(tmp_path / 'linux')
+        cwd=os.fsdecode(tmp_path / 'linux')
     )
     # Binary is marked executable
     build_command.os.chmod.assert_called_with(
-        str(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage'),
+        tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage',
         0o755
     )
diff --git a/tests/platforms/linux/appimage/test_run.py b/tests/platforms/linux/appimage/test_run.py
index 5f3279b8..f2456620 100644
--- a/tests/platforms/linux/appimage/test_run.py
+++ b/tests/platforms/linux/appimage/test_run.py
@@ -1,3 +1,4 @@
+import os
 from unittest.mock import MagicMock
 
 import pytest
@@ -47,7 +48,7 @@ def test_run_app(first_app_config, tmp_path):
     command.run_app(first_app_config)
 
     command.subprocess.run.assert_called_with(
-        [str(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage')],
+        [os.fsdecode(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage')],
         check=True
     )
 
@@ -67,6 +68,6 @@ def test_run_app_failed(first_app_config, tmp_path):
 
     # The run command was still invoked, though
     command.subprocess.run.assert_called_with(
-        [str(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage')],
+        [os.fsdecode(tmp_path / 'linux' / 'First_App-0.0.1-wonky.AppImage')],
         check=True
     )
diff --git a/tests/platforms/macOS/app/test_create.py b/tests/platforms/macOS/app/test_create.py
index 88fefbc7..46511aa9 100644
--- a/tests/platforms/macOS/app/test_create.py
+++ b/tests/platforms/macOS/app/test_create.py
@@ -8,7 +8,7 @@ def test_install_app_support_package(first_app_config, tmp_path):
     "A support package can be downloaded and unpacked where it is needed"
     # Write a temporary support zip file which includes the Python lib
     support_file = tmp_path / 'out.zip'
-    with zipfile.ZipFile(str(support_file), 'w') as support_zip:
+    with zipfile.ZipFile(support_file, 'w') as support_zip:
         support_zip.writestr('internal/file.txt', data='hello world')
         support_zip.writestr('Python/Resources/lib/module.py', data='code')
 
diff --git a/tests/platforms/macOS/app/test_package.py b/tests/platforms/macOS/app/test_package.py
index 20d256b1..ec389309 100644
--- a/tests/platforms/macOS/app/test_package.py
+++ b/tests/platforms/macOS/app/test_package.py
@@ -1,3 +1,4 @@
+import os
 from unittest import mock
 
 import pytest
@@ -46,8 +47,8 @@ def test_package_app(first_app_with_binaries, tmp_path):
             [
                 'codesign',
                 '--sign', 'Sekrit identity (DEADBEEF)',
-                '--entitlements', str(tmp_path / 'macOS' / 'app' / 'First App' / 'Entitlements.plist'),
-                '--deep', str(filepath),
+                '--entitlements', os.fsdecode(tmp_path / 'macOS' / 'app' / 'First App' / 'Entitlements.plist'),
+                '--deep', os.fsdecode(filepath),
                 '--force',
                 '--options', 'runtime',
             ],
@@ -71,10 +72,10 @@ def test_package_app(first_app_with_binaries, tmp_path):
 
     # The DMG has been built as expected
     command.dmgbuild.build_dmg.assert_called_with(
-        filename=str(tmp_path / 'macOS' / 'First App-0.0.1.dmg'),
+        filename=os.fsdecode(tmp_path / 'macOS' / 'First App-0.0.1.dmg'),
         volume_name='First App 0.0.1',
         settings={
-            'files': [str(tmp_path / 'macOS' / 'app' / 'First App' / 'First App.app')],
+            'files': [os.fsdecode(tmp_path / 'macOS' / 'app' / 'First App' / 'First App.app')],
             'symlinks': {'Applications': '/Applications'},
             'icon_locations': {
                 'First App.app': (75, 75),
diff --git a/tests/platforms/macOS/app/test_run.py b/tests/platforms/macOS/app/test_run.py
index 0884a0c8..76fb5c3b 100644
--- a/tests/platforms/macOS/app/test_run.py
+++ b/tests/platforms/macOS/app/test_run.py
@@ -1,3 +1,4 @@
+import os
 import subprocess
 from unittest import mock
 
@@ -18,7 +19,7 @@ def test_run_app(first_app_config, tmp_path):
     bin_path = command.binary_path(first_app_config)
     command.subprocess.run.assert_has_calls([
         mock.call(
-            ['open', '-n', str(bin_path)],
+            ['open', '-n', os.fsdecode(bin_path)],
             check=True
         ),
         mock.call(
@@ -42,7 +43,7 @@ def test_run_app_failed(first_app_config, tmp_path):
     command = macOSAppRunCommand(base_path=tmp_path)
     command.subprocess = mock.MagicMock()
     command.subprocess.run.side_effect = subprocess.CalledProcessError(
-        cmd=['open', '-n', str(command.binary_path(first_app_config))],
+        cmd=['open', '-n', os.fsdecode(command.binary_path(first_app_config))],
         returncode=1
     )
 
@@ -51,7 +52,7 @@ def test_run_app_failed(first_app_config, tmp_path):
 
     # The run command was still invoked, though
     command.subprocess.run.assert_called_with(
-        ['open', '-n', str(command.binary_path(first_app_config))],
+        ['open', '-n', os.fsdecode(command.binary_path(first_app_config))],
         check=True
     )
 
@@ -76,7 +77,7 @@ def test_run_app_log_stream_failed(first_app_config, tmp_path):
     bin_path = command.binary_path(first_app_config)
     command.subprocess.run.assert_has_calls([
         mock.call(
-            ['open', '-n', str(bin_path)],
+            ['open', '-n', os.fsdecode(bin_path)],
             check=True
         ),
         mock.call(
diff --git a/tests/platforms/macOS/xcode/test_run.py b/tests/platforms/macOS/xcode/test_run.py
index 75382535..d6c27d22 100644
--- a/tests/platforms/macOS/xcode/test_run.py
+++ b/tests/platforms/macOS/xcode/test_run.py
@@ -1,6 +1,7 @@
 # Xcode uses the same run implementation as the base app;
 # Run a basic test to ensure coverage, but fall back to
 # the app backend for exhaustive tests.
+import os
 from unittest import mock
 
 from briefcase.platforms.macOS.xcode import macOSXcodeRunCommand
@@ -17,7 +18,7 @@ def test_run_app(first_app_config, tmp_path):
     bin_path = command.binary_path(first_app_config)
     command.subprocess.run.assert_has_calls([
         mock.call(
-            ['open', '-n', str(bin_path)],
+            ['open', '-n', os.fsdecode(bin_path)],
             check=True
         ),
         mock.call(
diff --git a/tests/platforms/windows/msi/test_package.py b/tests/platforms/windows/msi/test_package.py
index ce9647ef..7362fa7f 100644
--- a/tests/platforms/windows/msi/test_package.py
+++ b/tests/platforms/windows/msi/test_package.py
@@ -1,4 +1,3 @@
-# import sys
 from unittest import mock
 
 import pytest
@@ -24,7 +23,7 @@ def test_package_msi(package_command, first_app_config, tmp_path):
         # Collect manifest
         mock.call(
             [
-                str(tmp_path / 'wix' / 'bin' / 'heat.exe'),
+                tmp_path / 'wix' / 'bin' / 'heat.exe',
                 "dir",
                 "src",
                 "-nologo",
@@ -39,12 +38,12 @@ def test_package_msi(package_command, first_app_config, tmp_path):
                 "-out", "first-app-manifest.wxs",
             ],
             check=True,
-            cwd=str(tmp_path / 'windows' / 'msi' / 'First App'),
+            cwd=tmp_path / 'windows' / 'msi' / 'First App',
         ),
         # Compile MSI
         mock.call(
             [
-                str(tmp_path / 'wix' / 'bin' / 'candle.exe'),
+                tmp_path / 'wix' / 'bin' / 'candle.exe',
                 "-nologo",
                 "-ext", "WixUtilExtension",
                 "-ext", "WixUIExtension",
@@ -53,21 +52,21 @@ def test_package_msi(package_command, first_app_config, tmp_path):
                 "first-app-manifest.wxs",
             ],
             check=True,
-            cwd=str(tmp_path / 'windows' / 'msi' / 'First App'),
+            cwd=tmp_path / 'windows' / 'msi' / 'First App',
         ),
 
         # Link MSI
         mock.call(
             [
-                str(tmp_path / 'wix' / 'bin' / 'light.exe'),
+                tmp_path / 'wix' / 'bin' / 'light.exe',
                 "-nologo",
                 "-ext", "WixUtilExtension",
                 "-ext", "WixUIExtension",
-                "-o", str(tmp_path / 'windows' / 'First App-0.0.1.msi'),
+                "-o", tmp_path / 'windows' / 'First App-0.0.1.msi',
                 "first-app.wixobj",
                 "first-app-manifest.wixobj",
             ],
             check=True,
-            cwd=str(tmp_path / 'windows' / 'msi' / 'First App'),
+            cwd=tmp_path / 'windows' / 'msi' / 'First App',
         ),
     ])
diff --git a/tests/platforms/windows/msi/test_run.py b/tests/platforms/windows/msi/test_run.py
index 831aa445..2f7139db 100644
--- a/tests/platforms/windows/msi/test_run.py
+++ b/tests/platforms/windows/msi/test_run.py
@@ -1,3 +1,4 @@
+import os
 from unittest import mock
 
 import pytest
@@ -15,7 +16,7 @@ def test_run_app(first_app_config, tmp_path):
 
     command.subprocess.run.assert_called_with(
         [
-            str(tmp_path / 'windows' / 'msi' / 'First App' / 'src' / 'python' / 'pythonw.exe'),
+            os.fsdecode(tmp_path / 'windows' / 'msi' / 'First App' / 'src' / 'python' / 'pythonw.exe'),
             "-m", "first_app"
         ],
         check=True
@@ -34,7 +35,7 @@ def test_run_app_failed(first_app_config, tmp_path):
     # The run command was still invoked, though
     command.subprocess.run.assert_called_with(
         [
-            str(tmp_path / 'windows' / 'msi' / 'First App' / 'src' / 'python' / 'pythonw.exe'),
+            os.fsdecode(tmp_path / 'windows' / 'msi' / 'First App' / 'src' / 'python' / 'pythonw.exe'),
             "-m", "first_app"
         ],
         check=True
diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py
index 0c73d79f..be9645e5 100644
--- a/tests/test_cmdline.py
+++ b/tests/test_cmdline.py
@@ -14,7 +14,7 @@ from briefcase.exceptions import (
 from briefcase.platforms.linux.appimage import LinuxAppImageCreateCommand
 from briefcase.platforms.macOS.app import (
     macOSAppCreateCommand,
-    macOSAppPublishCommand,
+    macOSAppPublishCommand
 )
 from briefcase.platforms.windows.msi import WindowsMSICreateCommand
 
diff --git a/tests/utils.py b/tests/utils.py
index 5289d60e..81bc9c79 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,4 +1,5 @@
 from briefcase.console import Console, InputDisabled
+from unittest.mock import MagicMock
 
 
 class DummyConsole(Console):
@@ -12,3 +13,17 @@ class DummyConsole(Console):
             raise InputDisabled()
         self.prompts.append(prompt)
         return self.values.pop(0)
+
+
+# Consider to remove  class definition when we drop python 3.7 support.
+class FsPathMock(MagicMock):
+    def __init__(self, path):
+        super().__init__()
+        self.path = path
+
+    def __fspath__(self):
+        return self.path
+
+    def _get_child_mock(self, **kw):
+        """Create child mocks with right MagicMock class."""
+        return MagicMock(**kw)