diff --git a/pytest.ini b/pytest.ini
index 9fa2d75a39cb3c34a52416c06d11f80d11abb1fd..b15e082c739bb4a8d62e9b2be811a076339b9e0b 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -3,3 +3,4 @@ addopts = --doctest-modules -p no:pytest_swh_core
 norecursedirs = docs .*
 markers =
     fs: tests that involve filesystem ios
+    requires_optional_deps: tests in test_cli.py that should not run if optional dependencies are not installed
diff --git a/requirements-test.txt b/requirements-test.txt
index f906d8ae9a901078112c21d99324e3fca99cdb92..984743d9b5d31b47b5cda7114b22dd0218ee86fb 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -1,4 +1,3 @@
 Click
-dulwich
 pytest
 pytz
diff --git a/setup.py b/setup.py
index 8f9d32fdcec61b2ac22ea9928cfe8ebabca8cd8a..a44dcbf60b851ec2b7936b2c43a341096f718d3d 100755
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,7 @@ setup(
     ),
     extras_require={
         "cli": parse_requirements("cli"),
+        "testing-minimal": parse_requirements("test"),
         "testing": parse_requirements("test") + parse_requirements("cli"),
     },
     include_package_data=True,
diff --git a/swh/model/tests/test_cli.py b/swh/model/tests/test_cli.py
index 9a006607f15d96be1917ca8a2cbc12aeb763f5f8..de0de4806c7fa2defb55bcbbc705adcbf610847f 100644
--- a/swh/model/tests/test_cli.py
+++ b/swh/model/tests/test_cli.py
@@ -4,9 +4,11 @@
 # See top-level LICENSE file for more information
 
 import os
+import sys
 import tarfile
 import tempfile
 import unittest
+import unittest.mock
 
 from click.testing import CliRunner
 import pytest
@@ -52,6 +54,7 @@ class TestIdentify(DataMixin, unittest.TestCase):
         result = self.runner.invoke(cli.identify, ["--type", "directory", path])
         self.assertSWHID(result, "swh:1:dir:e8b0f1466af8608c8a3fb9879db172b887e80759")
 
+    @pytest.mark.requires_optional_deps
     def test_snapshot_id(self):
         """identify a snapshot"""
         tarball = os.path.join(
@@ -68,6 +71,18 @@ class TestIdentify(DataMixin, unittest.TestCase):
                     result, "swh:1:snp:abc888898124270905a0ef3c67e872ce08e7e0c1"
                 )
 
+    def test_snapshot_without_dulwich(self):
+        """checks swh-identify returns a 'nice' message instead of a traceback
+        when dulwich is not installed"""
+        with unittest.mock.patch.dict(sys.modules, {"dulwich": None}):
+            with tempfile.TemporaryDirectory(prefix="swh.model.cli") as d:
+                result = self.runner.invoke(
+                    cli.identify, ["--type", "snapshot", d], catch_exceptions=False,
+                )
+
+        assert result.exit_code == 1
+        assert "'swh.model[cli]'" in result.output
+
     def test_origin_id(self):
         """identify an origin URL"""
         url = "https://github.com/torvalds/linux"
diff --git a/tox.ini b/tox.ini
index 1ea702f9571875aabb2279d34a6f9e1c316fcb13..25baa11db70079f1d41f3123066b763f44ed0024 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,25 +1,25 @@
 [tox]
-envlist=black,flake8,mypy,py3,identify
+envlist=black,flake8,mypy,py3-{minimal,full}
 
 [testenv]
 extras =
-  testing
+  full: testing
+  minimal: testing-minimal
 deps =
   pytest-cov
 commands =
   pytest --cov={envsitepackagesdir}/swh/model \
          --doctest-modules \
-         {envsitepackagesdir}/swh/model \
-           --cov-branch {posargs}
+  full:    {envsitepackagesdir}/swh/model \
+  minimal: {envsitepackagesdir}/swh/model/tests/test_cli.py -m 'not requires_optional_deps' \
+         --cov-branch {posargs}
 
-[testenv:identify]
-# no 'extras = testing', as it would install swh-core;
-# and this test is designed to check 'swh-identify' does not depend on swh-core.
-extras =
-deps =
-  -r requirements-test.txt
+[testenv:py3]
+skip_install = true
+deps = tox
 commands =
-  pytest {envsitepackagesdir}/swh/model/tests/test_cli.py
+  tox -e py3-full -- {posargs}
+  tox -e py3-minimal -- {posargs}
 
 [testenv:black]
 skip_install = true