diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..40105a615c7b3c030f6db777603836d46bcf1c35
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,27 @@
+# .coveragerc to control coverage.py
+[run]
+branch = False
+concurrency = multiprocessing
+parallel = True
+omit = */site-packages/*,*/tests/*,*/.eggs/*
+
+[report]
+show_missing = True
+
+# Regexes for lines to exclude from consideration
+exclude_lines =
+    # Have to re-enable the standard pragma
+    pragma: no cover
+
+    # Don't complain if tests don't hit defensive assertion code:
+    raise AssertionError
+    raise NotImplementedError
+
+    # Don't complain if non-runnable code isn't run:
+    if 0:
+    if __name__ == .__main__.:
+
+ignore_errors = True
+
+[html]
+directory = htmlcov
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a1673fd6b2ecabaf5f10e8544996eb17e2874b3d..03257e14adcf170bc698d1192ea1d07f73c1024c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -13,18 +13,25 @@ test_geoarray:
   script:
     - source /root/mambaforge/bin/activate ci_env
 
+    - pip install pytest pytest-cov pytest-reporter-html1  # TODO remove after recreating CI runner
+
     # update py_tools_ds
     - pip install -U py_tools_ds -q
 
     # run tests
-    - make nosetests
+    - make pytest
     - make docs
   artifacts:
+    expose_as: 'Test and coverage report'
     paths:
     - htmlcov/
+    - report.html
     - docs/_build/html/
-    - nosetests.html
-    - nosetests.xml
+    reports:
+      cobertura: coverage.xml
+      junit: report.xml
+
+    expire_in: 30 days
     when: always
 
 
@@ -83,7 +90,7 @@ pages:  # this job must be called 'pages' to advise GitLab to upload content to
     - mkdir public
     - mkdir -p public/doc
     - mkdir -p public/coverage
-    - mkdir -p public/nosetests_reports
+    - mkdir -p public/test_reports
 
     # Copy over the docs
     - cp -r docs/_build/html/* public/doc/
@@ -91,14 +98,14 @@ pages:  # this job must be called 'pages' to advise GitLab to upload content to
     # Copy over the coverage reports
     - cp -r htmlcov/* public/coverage/
 
-    # Copy over the nosetests reports
-    - cp nosetests.* public/nosetests_reports/
+    # Copy over the test reports
+    - cp report.html public/test_reports/
 
     # Check if everything is working great
     - ls -al public
     - ls -al public/doc
     - ls -al public/coverage
-    - ls -al public/nosetests_reports
+    - ls -al public/test_reports
   artifacts:
     paths:
       - public
diff --git a/HISTORY.rst b/HISTORY.rst
index f9160ac93fcf36c9c8984ed4fff6bc5d7077afd2..5eb3a689daf47d7bba8d61b298ee6f1d8a86acec 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,6 +2,12 @@
 History
 =======
 
+0.15.6 (16.12.2021)
+-------------------
+
+* Migrated test calls from nosetests to pytest.
+
+
 0.15.5 (03.12.2021)
 -------------------
 
diff --git a/Makefile b/Makefile
index 2c225180cf9f792ddda129c386b7e6064697533c..b0c63bff543acab4f4389bc0e0e5b9fd88dfc9fb 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: clean clean-test clean-pyc clean-build docs help nosetests
+.PHONY: clean clean-test clean-pyc clean-build docs help pytest
 .DEFAULT_GOAL := help
 define BROWSER_PYSCRIPT
 import os, webbrowser, sys
@@ -47,8 +47,10 @@ clean-test: ## remove test and coverage artifacts
 	rm -fr .tox/
 	rm -fr .coverage.*
 	rm -fr htmlcov/
-	rm -fr nosetests.html
-	rm -fr nosetests.xml
+	rm -fr report.html
+	rm -fr report.xml
+	rm -fr coverage.xml
+	rm -fr .pytest_cache
 
 lint: ## check style with flake8
 	flake8 --max-line-length=120 geoarray tests > ./tests/linting/flake8.log || \
@@ -73,12 +75,22 @@ coverage: clean-test ## check code coverage quickly with the default Python
 	coverage html
 	# $(BROWSER) htmlcov/index.html
 
-nosetests: clean-test ## Runs nosetests with coverage, xUnit and nose-html-output
+pytest: clean-test ## Runs pytest with coverage and creates coverage and test report
 	## - puts the coverage results in the folder 'htmlcov'
-	## - generates 'nosetests.html' (--with-html)
-	## - generates 'nosetests.xml' (--with-xunit) which is currently not visualizable by GitLab
-	nosetests -vv --with-coverage --cover-package=geoarray --cover-erase --cover-html --cover-html-dir=htmlcov \
-		--with-html --with-xunit --rednose --force-color
+	## - generates cobertura 'coverage.xml' (needed to show coverage in GitLab MR changes)
+	## - generates 'report.html' based on pytest-reporter-html1
+	## - generates JUnit 'report.xml' to show the test report as a new tab in a GitLab MR
+	## NOTE: additional options pytest and coverage (plugin pytest-cov) are defined in .pytest.ini and .coveragerc
+	pytest tests \
+		--verbosity=3 \
+		--color=yes \
+		--tb=short \
+		--cov=geoarray \
+		--cov-report html:htmlcov \
+    	--cov-report term-missing \
+    	--cov-report xml:coverage.xml \
+    	--template=html1/index.html --report=report.html \
+    	--junitxml report.xml
 
 docs: ## generate Sphinx HTML documentation, including API docs
 	rm -f docs/geoarray.rst
diff --git a/geoarray/version.py b/geoarray/version.py
index 9669de5ace4a81e01a69e60c15dbc4f538f5b60a..3683ad99f7e94729e2778ccd0ca572b48d516b42 100644
--- a/geoarray/version.py
+++ b/geoarray/version.py
@@ -22,5 +22,5 @@
 # limitations under the License.
 
 
-__version__ = '0.15.5'
-__versionalias__ = '20211203.01'
+__version__ = '0.15.6'
+__versionalias__ = '20211216.01'
diff --git a/setup.py b/setup.py
index 16ea94087a11b5f5d04a4bc7c4951851c6aff1ac..14f4d192cb3a312979a90fd748ce7ad302480aa7 100644
--- a/setup.py
+++ b/setup.py
@@ -58,7 +58,7 @@ req_interactive_plotting = [
 
 req_setup = ['setuptools-git']
 
-req_test = req + ["coverage", "nose", "nose2", "nose-htmloutput", "rednose", "urlchecker", "parameterized"]
+req_test = req + ["pytest", "pytest-cov", "pytest-reporter-hmtl1", "urlchecker", "parameterized"]
 
 req_doc = ['sphinx-argparse', 'sphinx_rtd_theme']
 
diff --git a/tests/CI_docker/context/environment_geoarray.yml b/tests/CI_docker/context/environment_geoarray.yml
index ae2525d3b2148508c9ac04355d46ebee97d5f662..0963061decadeda4f2c3b9a1b855037a3c6d55a6 100644
--- a/tests/CI_docker/context/environment_geoarray.yml
+++ b/tests/CI_docker/context/environment_geoarray.yml
@@ -23,15 +23,13 @@ dependencies:
     - pyepsg  # optional dependency of cartopy, needed by geoarray
 
     # test/docs requirements
-    - coverage
     - flake8
     - parameterized
     - pycodestyle
     - pydocstyle
     - pylint
-    - nose
-    - nose2
-    - nose-htmloutput
-    - rednose
+    - pytest
+    - pytest-cov
+    - pytest-reporter-html1
     - sphinx-argparse
     - urlchecker
diff --git a/tests/test_geoarray.py b/tests/test_geoarray.py
index eb4511c221c63babdb2819dd87b0677e38e883d8..b24ec6c725bb6e0e83be6719b89ca8e91d6a68c5 100644
--- a/tests/test_geoarray.py
+++ b/tests/test_geoarray.py
@@ -734,3 +734,8 @@ class Test_GeoArray(TestCase):
                                                          [-9999, -9999],
                                                          [5352, 5766],
                                                          [5080, 5436]]).reshape(shape_exp)))
+
+
+if __name__ == '__main__':
+    import pytest
+    pytest.main()
diff --git a/tests/test_masks.py b/tests/test_masks.py
index c7b5f60e759e73bcdae6fb2cb44ce3ebc2bb0983..421d5da4dc3464f9937b91f138c656c6ce051495 100644
--- a/tests/test_masks.py
+++ b/tests/test_masks.py
@@ -65,3 +65,8 @@ class Test_BadDataMask(TestCase):
 
         with self.assertRaises(AssertionError):
             BadDataMask(np.array([[5, 2], [2, 5]]))
+
+
+if __name__ == '__main__':
+    import pytest
+    pytest.main()
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index c2b4036d3194b6b3fd0dd946525eeb1026f3a829..34edaefdfc4e79fc0ad14e93ad6e228a4a45c0c9 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -90,3 +90,8 @@ class Test_GDAL_Metadata(TestCase):
         for bN in gA.bandnames.keys():
             self.assertTrue(bN in content, msg="The band name '%s' is not in the written header. "
                                                "Header contains:  \n\n%s" % (bN, content))
+
+
+if __name__ == '__main__':
+    import pytest
+    pytest.main()