diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 6dca8cb9..6738e0a5 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -47,7 +47,7 @@ jobs:
         rm -rf dist/
         python -m pip install build
         python -m build --sdist
-        
+
         echo "sdist_artifact_name=$(ls ./dist)" >> "$GITHUB_OUTPUT"
         echo "package_version=$(ls ./dist | sed -En 's/cffi-(.+)\.tar\.gz/\1/p')" >> "$GITHUB_OUTPUT"
 
@@ -76,21 +76,21 @@ jobs:
 
           - spec: cp39-manylinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
-            
+
           - spec: cp310-manylinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
 
           - spec: cp311-manylinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
-          
+
           - spec: cp312-manylinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
 
           - spec: cp313-manylinux_x86_64
-            
+
           - spec: cp313t-manylinux_x86_64
             skip_artifact_upload: 'true'
-  
+
           - spec: cp38-manylinux_i686
             omit: ${{ env.skip_ci_redundant_jobs }}
 
@@ -108,10 +108,10 @@ jobs:
 
           - spec: cp313-manylinux_i686
             omit: ${{ env.skip_ci_redundant_jobs }}
-          
+
           - spec: cp39-musllinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
-          
+
           - spec: cp310-musllinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
 
@@ -120,9 +120,9 @@ jobs:
 
           - spec: cp312-musllinux_x86_64
             omit: ${{ env.skip_ci_redundant_jobs }}
-  
+
           - spec: cp313-musllinux_x86_64
-  
+
           - spec: cp39-musllinux_i686
             omit: ${{ env.skip_ci_redundant_jobs }}
 
@@ -134,14 +134,14 @@ jobs:
 
           #- spec: cp312-musllinux_i686  # busted as of 2024-05-17
           #  omit: ${{ env.skip_ci_redundant_jobs }}
-  
+
           #- spec: cp313-musllinux_i686 # busted as of 2024-05-17
-  
+
           - spec: cp39-musllinux_aarch64
             foreign_arch: true
             test_args: '{package}/src/c'
             omit: ${{ env.skip_ci_redundant_jobs || env.skip_slow_jobs }}
-          
+
           - spec: cp310-musllinux_aarch64
             foreign_arch: true
             test_args: '{package}/src/c'
@@ -156,12 +156,12 @@ jobs:
             foreign_arch: true
             test_args: '{package}/src/c'
             omit: ${{ env.skip_ci_redundant_jobs || env.skip_slow_jobs }}
-  
+
           - spec: cp313-musllinux_aarch64
             foreign_arch: true
             # test_args: '{package}/src/c'
             omit: ${{ env.skip_slow_jobs}}
-  
+
           - spec: cp38-manylinux_aarch64
             foreign_arch: true
             test_args: '{package}/src/c'
@@ -186,12 +186,12 @@ jobs:
             foreign_arch: true
             test_args: '{package}/src/c'
             omit: ${{ env.skip_slow_jobs || env.skip_ci_redundant_jobs }}
-  
+
           - spec: cp313-manylinux_aarch64
             foreign_arch: true
             # test_args: '{package}/src/c'
             omit: ${{ env.skip_slow_jobs }}
-  
+
           - spec: cp38-manylinux_ppc64le
             foreign_arch: true
             test_args: '{package}/src/c'
@@ -221,7 +221,7 @@ jobs:
             foreign_arch: true
             test_args: '{package}/src/c'
             omit: ${{ env.skip_slow_jobs }}
-  
+
           - spec: cp38-manylinux_s390x
             foreign_arch: true
             test_args: '{package}/src/c'
@@ -298,13 +298,13 @@ jobs:
         CIBW_MUSLLINUX_AARCH64_IMAGE: ${{ matrix.musllinux_img || 'musllinux_1_1' }}
         CIBW_PRERELEASE_PYTHONS: 'True'
         CIBW_FREE_THREADED_SUPPORT: 'True'
-        CIBW_TEST_REQUIRES: pytest setuptools  # 3.12+ no longer includes distutils, just always ensure setuptools is present
-        CIBW_TEST_COMMAND: PYTHONUNBUFFERED=1 python -m pytest ${{ matrix.test_args || '{project}' }}  # default to test all
+        CIBW_TEST_REQUIRES: pytest pytest-xdist setuptools  # 3.12+ no longer includes distutils, just always ensure setuptools is present
+        CIBW_TEST_COMMAND: PYTHONUNBUFFERED=1 python -m pytest -n auto ${{ matrix.test_args || '{project}' }} # default to test all
       run: |
         set -eux
-        
+
         mkdir cffi
-        
+
         tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi
         python -m pip install --upgrade "${{ matrix.cibw_version || 'cibuildwheel' }}"
 
@@ -417,19 +417,19 @@ jobs:
       env:
         CIBW_BUILD: ${{ matrix.spec }}
         CIBW_PRERELEASE_PYTHONS: 'True'
-        CIBW_TEST_REQUIRES: pytest setuptools
-        CIBW_TEST_COMMAND: pip install pip --upgrade; cd {project}; PYTHONUNBUFFERED=1 pytest
+        CIBW_TEST_REQUIRES: pytest pytest-xdist setuptools
+        CIBW_TEST_COMMAND: pip install pip --upgrade; cd {project}; PYTHONUNBUFFERED=1 pytest -n auto
         MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment_target || '10.9' }}
         SDKROOT: ${{ matrix.sdkroot || 'macosx' }}
       run: |
         set -eux
-        
+
         mkdir cffi
-        
+
         tar zxf ${{ steps.fetch_sdist.outputs.download-path }}/cffi*.tar.gz --strip-components=1 -C cffi
 
         python3 -m cibuildwheel --output-dir dist cffi
-        
+
         echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT"
 
     - name: upload artifacts
@@ -508,21 +508,21 @@ jobs:
       env:
         CIBW_BUILD: ${{ matrix.spec }}
         CIBW_PRERELEASE_PYTHONS: 'True'
-        CIBW_TEST_REQUIRES: pytest setuptools
-        CIBW_TEST_COMMAND: 'python -m pytest {package}/src/c'
+        CIBW_TEST_REQUIRES: pytest pytest-xdist setuptools
+        CIBW_TEST_COMMAND: 'python -m pytest -n auto {package}/src/c'
         # FIXME: /testing takes ~45min on Windows and has some failures...
-        # CIBW_TEST_COMMAND='python -m pytest {package}/src/c {project}/testing'
+        # CIBW_TEST_COMMAND='python -m pytest -n auto {package}/src/c {project}/testing'
       run: |
         set -eux
-        
+
         mkdir cffi
-        
+
         tar zxf cffi*.tar.gz --strip-components=1 -C cffi
-        
+
         python -m pip install --upgrade pip
         pip install "${{ matrix.cibw_version || 'cibuildwheel'}}"
         python -m cibuildwheel --output-dir dist cffi
-        
+
         echo "artifact_name=$(ls ./dist/)" >> "$GITHUB_OUTPUT"
 
       shell: bash
diff --git a/README.md b/README.md
index 58e8b82c..7544ab50 100644
--- a/README.md
+++ b/README.md
@@ -31,9 +31,9 @@ Testing/development tips
 
 To run tests under CPython, run the following in the source root directory:
 
-    pip install pytest
+    pip install pytest pytest-xdist virtualenv
     pip install -e .  # editable install of CFFI for local development
-    pytest c/ testing/
+    pytest
 
 
 [Documentation]: http://cffi.readthedocs.org/
diff --git a/pyproject.toml b/pyproject.toml
index abd7b948..eea98b83 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -48,3 +48,18 @@ Downloads = "https://github.com/python-cffi/cffi/releases"
 Contact = "https://groups.google.com/forum/#!forum/python-cffi"
 "Source Code" = "https://github.com/python-cffi/cffi"
 "Issue Tracker" = "https://github.com/python-cffi/cffi/issues"
+
+[tool.pytest.ini_options]
+addopts ="""
+    -r a
+    --verbose
+    --strict-markers
+    --durations-min 1
+    --durations 10
+    --color yes
+    --showlocals
+"""
+testpaths = [
+    "src/c",
+    "testing/",
+]
diff --git a/testing/cffi0/test_zintegration.py b/testing/cffi0/test_zintegration.py
index 7612ba00..2ccbd679 100644
--- a/testing/cffi0/test_zintegration.py
+++ b/testing/cffi0/test_zintegration.py
@@ -1,201 +1,204 @@
-import py, os, sys, shutil
+import os
+import pathlib
+import pytest
+import site
 import subprocess
+import sys
 import sysconfig
 import textwrap
-from testing.udir import udir
-import pytest
+
+from importlib.util import find_spec
 
 if sys.platform == 'win32':
     pytestmark = pytest.mark.skip('snippets do not run on win32')
+
 if sys.version_info < (2, 7):
     pytestmark = pytest.mark.skip(
                  'fails e.g. on a Debian/Ubuntu which patches virtualenv'
                  ' in a non-2.6-friendly way')
 
-def create_venv(name):
-    tmpdir = udir.join(name)
-    try:
-        # FUTURE: we should probably update this to use venv for at least more modern Pythons, and
-        # install setuptools/pip/etc explicitly for the tests that require them (as venv has stopped including
-        # setuptools and wheel by default for newer versions).
-        subprocess.check_call(['virtualenv', 
-            #'--never-download', <= could be added, but causes failures
-            # in random cases on random machines
-                               '-p', os.path.abspath(sys.executable),
-                               str(tmpdir)])
-
-        # Python 3.12 venv/virtualenv no longer include setuptools and wheel by default, which
-        # breaks a number of these tests; ensure it's always present for 3.12+
-        if sys.version_info >= (3, 12):
-            subprocess.check_call([
-                os.path.join(tmpdir, 'bin/python'),
-                '-m',
-                'pip',
-                'install',
-                'setuptools',
-                'wheel',
-                '--upgrade'
-            ])
-
-    except OSError as e:
-        pytest.skip("Cannot execute virtualenv: %s" % (e,))
-
-    site_packages = None
-    for dirpath, dirnames, filenames in os.walk(str(tmpdir)):
-        if os.path.basename(dirpath) == 'site-packages':
-            site_packages = dirpath
-            break
-    paths = ""
-    if site_packages:
+@pytest.fixture(scope="session")
+def snippet_dir():
+    return pathlib.Path(__file__).parent / 'snippets'
+
+@pytest.fixture
+def create_venv(tmp_path):
+    venv_path = tmp_path / ".venv"
+
+    def _create_venv(name):
+        if find_spec("venv") is not None:
+            venv_module = "venv"
+            args = []
+        else:
+            venv_module = "virtualenv"
+            args = ["--python", sys.executable]
+
         try:
-            from cffi import _pycparser
-            modules = ('cffi', '_cffi_backend')
-        except ImportError:
-            modules = ('cffi', '_cffi_backend', 'pycparser')
-            try:
-                import ply
-            except ImportError:
-                pass
-            else:
-                modules += ('ply',)   # needed for older versions of pycparser
+            subprocess.check_call([sys.executable, "-m", venv_module, *args, str(venv_path)])
+
+            # Python 3.12 venv/virtualenv no longer include setuptools and wheel by default, which
+            # breaks a number of these tests; ensure it's always present for 3.12+
+            if sys.version_info >= (3, 12):
+                subprocess.check_call([
+                    venv_path / "bin" / "python", "-m", "pip", "install", "--upgrade",
+                    "setuptools",
+                    "wheel",
+                ])
+
+        except OSError as e:
+            pytest.skip("Cannot execute %s: %s" % (venv_module, e))
+
+        site_packages = site.getsitepackages()
         paths = []
-        for module in modules:
-            target = __import__(module, None, None, [])
-            if not hasattr(target, '__file__'):   # for _cffi_backend on pypy
-                continue
-            src = os.path.abspath(target.__file__)
-            for end in ['__init__.pyc', '__init__.pyo', '__init__.py']:
-                if src.lower().endswith(end):
-                    src = src[:-len(end)-1]
-                    break
-            paths.append(os.path.dirname(src))
-        paths = os.pathsep.join(paths)
-    return tmpdir, paths
-
-SNIPPET_DIR = py.path.local(__file__).join('..', 'snippets')
-
-def really_run_setup_and_program(dirname, venv_dir_and_paths, python_snippet):
-    venv_dir, paths = venv_dir_and_paths
-    def remove(dir):
-        dir = str(SNIPPET_DIR.join(dirname, dir))
-        shutil.rmtree(dir, ignore_errors=True)
-    remove('build')
-    remove('__pycache__')
-    for basedir in os.listdir(str(SNIPPET_DIR.join(dirname))):
-        remove(os.path.join(basedir, '__pycache__'))
-    olddir = os.getcwd()
-    python_f = udir.join('x.py')
-    python_f.write(textwrap.dedent(python_snippet))
-    try:
-        os.chdir(str(SNIPPET_DIR.join(dirname)))
-        if os.name == 'nt':
-            bindir = 'Scripts'
-        else:
-            bindir = 'bin'
-        vp = str(venv_dir.join(bindir).join('python'))
-        env = os.environ.copy()
-        env['PYTHONPATH'] = paths
-        subprocess.check_call((vp, 'setup.py', 'clean'), env=env)
-        # there's a setuptools/easy_install bug that causes this to fail when the build/install occur together and
-        # we're in the same directory with the build (it tries to look up dependencies for itself on PyPI);
-        # subsequent runs will succeed because this test doesn't properly clean up the build- use pip for now.
-        subprocess.check_call((vp, '-m', 'pip', 'install', '.'), env=env)
-        subprocess.check_call((vp, str(python_f)), env=env)
-    finally:
-        os.chdir(olddir)
+        if site_packages:
+            if find_spec("cffi._pycparser") is not None:
+                modules = ('cffi', '_cffi_backend')
+            else:
+                modules = ('cffi', '_cffi_backend', 'pycparser')
+                if find_spec("ply") is not None:
+                    modules += ('ply',)   # needed for older versions of pycparser
+
+            paths = []
+            for module in modules:
+                target = __import__(module, None, None, [])
+                if not hasattr(target, '__file__'):   # for _cffi_backend on pypy
+                    continue
+
+                src = os.path.abspath(target.__file__)
+                for end in ['__init__.pyc', '__init__.pyo', '__init__.py']:
+                    if src.lower().endswith(end):
+                        src = src[:-len(end)-1]
+                        break
+
+                paths.append(os.path.dirname(src))
+
+            paths = os.pathsep.join(paths)
+
+        return venv_path, paths
+
+    return _create_venv
+
+@pytest.fixture
+def setup_program(tmp_path_factory, snippet_dir):
+    def _setup_program(dirname, venv_dir_and_paths, python_snippet):
+        venv_dir, paths = venv_dir_and_paths
+        olddir = os.getcwd()
+        workdir = tmp_path_factory.mktemp("ffi-", numbered=True)
+        python_file = workdir.joinpath('x.py')
+        python_file.write_text(textwrap.dedent(python_snippet))
+        try:
+            os.chdir(str(snippet_dir.joinpath(dirname)))
+            if os.name == 'nt':
+                bindir = 'Scripts'
+            else:
+                bindir = 'bin'
+
+            venv_python = str(venv_dir.joinpath(bindir).joinpath('python'))
+            env = os.environ.copy()
+            env['PYTHONPATH'] = paths
+            subprocess.check_call((venv_python, 'setup.py', 'clean'), env=env)
+            # there's a setuptools/easy_install bug that causes this to fail when the build/install occur together and
+            # we're in the same directory with the build (it tries to look up dependencies for itself on PyPI);
+            # subsequent runs will succeed because this test doesn't properly clean up the build- use pip for now.
+            subprocess.check_call((venv_python, '-m', 'pip', 'install', '.'), env=env)
+            subprocess.check_call((venv_python, str(python_file)), env=env)
+        finally:
+            os.chdir(olddir)
 
-def run_setup_and_program(dirname, python_snippet):
-    venv_dir = create_venv(dirname + '-cpy')
-    really_run_setup_and_program(dirname, venv_dir, python_snippet)
-    #
-    sys._force_generic_engine_ = True
-    try:
-        venv_dir = create_venv(dirname + '-gen')
-        really_run_setup_and_program(dirname, venv_dir, python_snippet)
-    finally:
-        del sys._force_generic_engine_
-    # the two files lextab.py and yacctab.py are created by not-correctly-
-    # installed versions of pycparser.
-    assert not os.path.exists(str(SNIPPET_DIR.join(dirname, 'lextab.py')))
-    assert not os.path.exists(str(SNIPPET_DIR.join(dirname, 'yacctab.py')))
-
-class TestZIntegration(object):
-    def teardown_class(self):
-        if udir.isdir():
-            udir.remove(ignore_errors=True)
-        udir.ensure(dir=1)
-
-    def test_infrastructure(self):
-        run_setup_and_program('infrastructure', '''
-        import snip_infrastructure
-        assert snip_infrastructure.func() == 42
-        ''')
-
-    def test_distutils_module(self):
-        run_setup_and_program("distutils_module", '''
-        import snip_basic_verify
-        p = snip_basic_verify.C.getpwuid(0)
-        assert snip_basic_verify.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_distutils_package_1(self):
-        run_setup_and_program("distutils_package_1", '''
-        import snip_basic_verify1
-        p = snip_basic_verify1.C.getpwuid(0)
-        assert snip_basic_verify1.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_distutils_package_2(self):
-        run_setup_and_program("distutils_package_2", '''
-        import snip_basic_verify2
-        p = snip_basic_verify2.C.getpwuid(0)
-        assert snip_basic_verify2.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_setuptools_module(self):
-        run_setup_and_program("setuptools_module", '''
-        import snip_setuptools_verify
-        p = snip_setuptools_verify.C.getpwuid(0)
-        assert snip_setuptools_verify.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_setuptools_package_1(self):
-        run_setup_and_program("setuptools_package_1", '''
-        import snip_setuptools_verify1
-        p = snip_setuptools_verify1.C.getpwuid(0)
-        assert snip_setuptools_verify1.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_setuptools_package_2(self):
-        run_setup_and_program("setuptools_package_2", '''
-        import snip_setuptools_verify2
-        p = snip_setuptools_verify2.C.getpwuid(0)
-        assert snip_setuptools_verify2.ffi.string(p.pw_name) == b"root"
-        ''')
-
-    def test_set_py_limited_api(self):
-        from cffi.setuptools_ext import _set_py_limited_api
+    return _setup_program
+
+@pytest.fixture
+def run_setup_and_program(tmp_path, create_venv, snippet_dir, setup_program):
+    def _run_setup_and_program(dirname, python_snippet):
+        venv_dir_and_paths = create_venv(dirname + '-cpy')
+        setup_program(dirname, venv_dir_and_paths, python_snippet)
+
+        sys._force_generic_engine_ = True
         try:
-            import setuptools
-        except ImportError as e:
-            pytest.skip(str(e))
-        orig_version = setuptools.__version__
+            venv_dir = create_venv(dirname + '-gen')
+            setup_program(dirname, venv_dir, python_snippet)
+        finally:
+            del sys._force_generic_engine_
+
+        # the two files lextab.py and yacctab.py are created by not-correctly-
+        # installed versions of pycparser.
+        assert not os.path.exists(str(snippet_dir.joinpath(dirname, 'lextab.py')))
+        assert not os.path.exists(str(snippet_dir.joinpath(dirname, 'yacctab.py')))
+
+    return _run_setup_and_program
+
+
+def test_infrastructure(run_setup_and_program):
+    run_setup_and_program('infrastructure', '''
+    import snip_infrastructure
+    assert snip_infrastructure.func() == 42
+    ''')
+
+def test_distutils_module(run_setup_and_program):
+    run_setup_and_program("distutils_module", '''
+    import snip_basic_verify
+    p = snip_basic_verify.C.getpwuid(0)
+    assert snip_basic_verify.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_distutils_package_1(run_setup_and_program):
+    run_setup_and_program("distutils_package_1", '''
+    import snip_basic_verify1
+    p = snip_basic_verify1.C.getpwuid(0)
+    assert snip_basic_verify1.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_distutils_package_2(run_setup_and_program):
+    run_setup_and_program("distutils_package_2", '''
+    import snip_basic_verify2
+    p = snip_basic_verify2.C.getpwuid(0)
+    assert snip_basic_verify2.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_setuptools_module(run_setup_and_program):
+    run_setup_and_program("setuptools_module", '''
+    import snip_setuptools_verify
+    p = snip_setuptools_verify.C.getpwuid(0)
+    assert snip_setuptools_verify.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_setuptools_package_1(run_setup_and_program):
+    run_setup_and_program("setuptools_package_1", '''
+    import snip_setuptools_verify1
+    p = snip_setuptools_verify1.C.getpwuid(0)
+    assert snip_setuptools_verify1.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_setuptools_package_2(run_setup_and_program):
+    run_setup_and_program("setuptools_package_2", '''
+    import snip_setuptools_verify2
+    p = snip_setuptools_verify2.C.getpwuid(0)
+    assert snip_setuptools_verify2.ffi.string(p.pw_name) == b"root"
+    ''')
+
+def test_set_py_limited_api():
+    from cffi.setuptools_ext import _set_py_limited_api
+    try:
+        import setuptools
+    except ImportError as e:
+        pytest.skip(str(e))
+    orig_version = setuptools.__version__
         # free-threaded Python does not yet support limited API
-        expecting_limited_api = not hasattr(sys, 'gettotalrefcount') and not sysconfig.get_config_var("Py_GIL_DISABLED")
-        try:
-            setuptools.__version__ = '26.0.0'
-            from setuptools import Extension
+    expecting_limited_api = not hasattr(sys, 'gettotalrefcount') and not sysconfig.get_config_var("Py_GIL_DISABLED")
+    try:
+        setuptools.__version__ = '26.0.0'
+        from setuptools import Extension
 
-            kwds = _set_py_limited_api(Extension, {})
-            assert kwds.get('py_limited_api', False) == expecting_limited_api
+        kwds = _set_py_limited_api(Extension, {})
+        assert kwds.get('py_limited_api', False) is expecting_limited_api
 
-            setuptools.__version__ = '25.0'
-            kwds = _set_py_limited_api(Extension, {})
-            assert kwds.get('py_limited_api', False) == False
+        setuptools.__version__ = '25.0'
+        kwds = _set_py_limited_api(Extension, {})
+        assert kwds.get('py_limited_api', False) is False
 
-            setuptools.__version__ = 'development'
-            kwds = _set_py_limited_api(Extension, {})
-            assert kwds.get('py_limited_api', False) == expecting_limited_api
+        setuptools.__version__ = 'development'
+        kwds = _set_py_limited_api(Extension, {})
+        assert kwds.get('py_limited_api', False) is expecting_limited_api
 
-        finally:
-            setuptools.__version__ = orig_version
+    finally:
+        setuptools.__version__ = orig_version