diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fe44062ad77f0e077ad3329c8ddfec727ffea0fb..42b3ccb7834caac3e1bf4fe072f520b5c4238fe4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,6 +12,11 @@ test:
   script:
     - pip install -U tox
     - tox -e py38
+  artifacts:
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
   rules:
     - if: ($CI_PIPELINE_SOURCE == "push") &&
           (($CI_COMMIT_BRANCH == "develop") || ($CI_COMMIT_BRANCH =~ "/^r\d\.\d+/"))
@@ -26,6 +31,11 @@ test-full:
   script:
     - pip install -U tox
     - tox -e py38 -- --fulltest
+  artifacts:
+    reports:
+      coverage_report:
+        coverage_format: cobertura
+        path: coverage.xml
   rules:
     - if: ($CI_PIPELINE_SOURCE == "merge_request_event") &&
           ($CI_MERGE_REQUEST_TITLE !~ /^Draft:.*/)
diff --git a/pyproject.toml b/pyproject.toml
index 2d7af89823f7b6713f8e3cd0e1f6ca88989ee4ed..af3e8de13bc5cf5c41cfdcca31ee1dba0fb440a8 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -86,6 +86,7 @@ tests = [
     "pylint >= 2.14",
     "pytest >= 6.1",
     "pytest-asyncio",
+    "pytest-cov >= 4.0",
 ]
 
 [project.urls]
@@ -96,6 +97,21 @@ repository = "https://gitlab.inria.fr/magnet/declearn/declearn2.git"
 line-length = 79
 extend-exclude = "(.*_pb2.*py$)"  # exclude auto-generated protobuf code files
 
+[tool.coverage.run]
+# enable coverage collection within multiprocessing
+concurrency = ["multiprocessing"]
+parallel = true
+sigterm = true
+# define rules to select the code files that need covering
+source_pkgs = ["declearn"]
+omit = [
+    "**/grpc/protobufs/*.py",  # auto-generated rotobuf code files
+    "**/test_utils/*.py",  # dev-only test-oriented utils
+]
+
+[tool.coverage.paths]
+source = ["declearn", ".tox/**/declearn"]
+
 [tool.mypy]
 exclude = [".*_pb2.*.py$"]
 follow_imports = "skip"  # otherwise excluded files are checked
diff --git a/tox.ini b/tox.ini
index 2d19fd2fd6a81761b7400d944be49b2977cb0242..664ce9eebdf81f9942cfb2f10efb6f5b3b7f972e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -12,11 +12,15 @@ allowlist_externals =
 commands=
     # run unit tests first
     pytest {posargs} \
+        --cov --cov-report= \  # reset then accumulate coverage quietly
         --ignore=test/functional/ \
         test
     # run functional tests (that build on units)
     pytest {posargs} \
+        --cov --cov-append --cov-report=term \  # acc. and display coverage
         test/functional/
+    # export the finalized coverage report to xml
+    coverage xml
     # verify code acceptance by pylint
     pylint declearn
     pylint --recursive=y test