diff --git a/hgext3rd/hgitaly/tests/__init__.py b/hgext3rd/hgitaly/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..580677ba3695293c5a9646f069a526c122738346_aGdleHQzcmQvaGdpdGFseS90ZXN0cy9fX2luaXRfXy5weQ==
--- /dev/null
+++ b/hgext3rd/hgitaly/tests/__init__.py
@@ -0,0 +1,1 @@
+# Python package
diff --git a/hgext3rd/hgitaly/tests/test_serve.py b/hgext3rd/hgitaly/tests/test_serve.py
new file mode 100644
index 0000000000000000000000000000000000000000..580677ba3695293c5a9646f069a526c122738346_aGdleHQzcmQvaGdpdGFseS90ZXN0cy90ZXN0X3NlcnZlLnB5
--- /dev/null
+++ b/hgext3rd/hgitaly/tests/test_serve.py
@@ -0,0 +1,105 @@
+# Copyright 2020 Georges Racinet <georges.racinet@octobus.net>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+import os
+import pytest
+
+from mercurial import (
+    error,
+)
+from heptapod.testhelpers import (
+    make_ui,
+    LocalRepoWrapper,
+)
+
+import hgext3rd.hgitaly as hgitaly_ext
+HGEXT_HGITALY_SOURCE = os.path.dirname(hgitaly_ext.__file__)
+
+DEFAULT_LISTEN_URL_STR = hgitaly_ext.DEFAULT_LISTEN_URL.decode()
+
+
+def activating_config():
+    return dict(extensions=dict(hgitaly=HGEXT_HGITALY_SOURCE))
+
+
+def recorder(recorders):
+    def inner(*args, **kwargs):
+        recorders.append((args, kwargs))
+
+    return inner
+
+
+def mock_run_forever(tmpdir, monkeypatch):
+    """Uses the monkeypatch fixture to mock `hgitaly.server.run_forever`
+
+    :return: ``ui, records`` where ``records`` is a list that will receive
+       a pair `(args, kwargs)` for each call of the  mocked `run_forever`
+    """
+    # using RepoWrapper is pure lazyness on our part: they  give us the easiest
+    # access to fully set up `ui` objects, with activated extensions
+    ui = LocalRepoWrapper.init(tmpdir, config=activating_config()).repo.ui
+    records = []
+    monkeypatch.setattr(hgitaly_ext, 'run_forever', recorder(records))
+    return ui, records
+
+
+def test_serve_default_url(tmpdir, monkeypatch):
+    ui, records = mock_run_forever(tmpdir, monkeypatch)
+    hgitaly_ext.serve(ui, listen=[], repositories_root=b'/some/path')
+    listen_urls, storages = records[0][0]
+
+    # we have no bytes in the inner call
+    assert listen_urls == [DEFAULT_LISTEN_URL_STR]
+    assert storages == dict(default=b'/some/path')
+
+
+def test_serve_config_repositories_root(tmpdir, monkeypatch):
+    ui, records = mock_run_forever(tmpdir, monkeypatch)
+    ui.setconfig(b'heptapod', b'repositories-root', b'/explicit/path')
+    hgitaly_ext.serve(ui, listen=[])
+    listen_urls, storages = records[0][0]
+
+    # we have no bytes in the inner call
+    assert listen_urls == [DEFAULT_LISTEN_URL_STR]
+    assert storages == dict(default=b'/explicit/path')
+
+
+def test_missing_repos_root():
+    ui = make_ui(None, config=activating_config())
+    with pytest.raises(error.Abort) as exc_info:
+        hgitaly_ext.serve(ui, listen=[])
+    assert b'repositories-root' in exc_info.value.args[0]
+
+
+def test_reraising():
+    # the point here is that we don't mock run_forever() , instead
+    # we test the whole loop from bad arguments, leading hgitaly.server.init()
+    # to raise exceptions and we test the final conversions of the latter.
+    ui = make_ui(None, config=activating_config())
+    repos_root = b'/some/path'
+
+    invalid_ipv6_url = b'tcp://[::'
+    with pytest.raises(error.Abort) as exc_info:
+        hgitaly_ext.serve(ui, listen=[invalid_ipv6_url],
+                          repositories_root=repos_root)
+    message = exc_info.value.args[0]
+    assert invalid_ipv6_url in message
+    assert b'Invalid IPv6 URL' in message  # explanation from urlparse()
+
+    unsupported_scheme_url = b'exotic://localhost:1234'
+    with pytest.raises(error.Abort) as exc_info:
+        hgitaly_ext.serve(ui, listen=[unsupported_scheme_url],
+                          repositories_root=repos_root)
+    message = exc_info.value.args[0]
+    assert b"scheme: 'exotic'" in message
+
+    cant_bind_url = b'tcp://unresolvable-or-youre-kidding-me:1234'
+    with pytest.raises(error.Abort) as exc_info:
+        hgitaly_ext.serve(ui, listen=[cant_bind_url],
+                          repositories_root=repos_root)
+    message = exc_info.value.args[0]
+    assert cant_bind_url in message
+    assert b'could not listen' in message
diff --git a/run-all-tests b/run-all-tests
index e5d2c2bd613948d4aaaede6f23799bd5bb17da36_cnVuLWFsbC10ZXN0cw==..580677ba3695293c5a9646f069a526c122738346_cnVuLWFsbC10ZXN0cw== 100755
--- a/run-all-tests
+++ b/run-all-tests
@@ -2,4 +2,4 @@
 
 set -ue
 
-pytest --doctest-modules --cov hgitaly --cov-config=.coveragerc --cov-fail-under 100 -v $@
+pytest --doctest-modules --cov hgext3rd --cov hgitaly --cov-config=.coveragerc --cov-fail-under 100 -v $@