diff --git a/.gitignore b/.gitignore
index 29678ee0deea7c45cbdbe94e8a8dde263f9eb0be..67eac53b45cd59a8db7b5e988128fb80bece7faf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,5 @@
 dist/*
 *.egg-info
+*.pyc
+
+.coverage
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f5d308f71614345ed3f475172e02af4732c91950..5788612ec2ee35c80bbd59e80dcc9e3daef340cc 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,17 +1,13 @@
 default:
-  image: python:3.7 # minimum supported version
-  # Make sure each step is executed in a virtual environment with some basic dependencies present
+  image: python:3.10  # use latest for building/linting
   before_script:
     - python --version # For debugging
-    - python -m venv venv
-    - source venv/bin/activate
     - python -m pip install --upgrade pip
-    # install common packages required for linting, building, docs etc.
-    - pip install --upgrade black build flake8 pylint setuptools sphinx twine wheel
+    - pip install --upgrade tox twine
   cache:
     paths:
       - .cache/pip
-      - venv/
+      # Do not cache .tox, to recreate virtualenvs for every step
 
 stages:
   - lint
@@ -29,22 +25,19 @@ variables:
 run_black:
   stage: lint
   script:
-    - python -m black --version
-    - python -m black --check --diff .
+    - tox -e black
   allow_failure: true
 
 run_flake8:
   stage: lint
   script:
-    - python -m flake8 --version
-    - python -m flake8 map
+    - tox -e pep8
   allow_failure: true
 
 run_pylint:
   stage: lint
   script:
-    - python -m pylint --version
-    - python -m pylint map
+    - tox -e pylint
   allow_failure: true
 
 # build_extensions:
@@ -54,9 +47,12 @@ run_pylint:
 
 run_unit_tests:
   stage: test
+  image: python:3.${PY_VERSION}
   script:
-    - pip install -r requirements.txt
-    - echo "run python unit tests /w coverage"
+    - tox -e py3${PY_VERSION}
+  parallel:
+    matrix: # use the matrix for testing
+      - PY_VERSION: [7, 8, 9, 10]
 
 package_files:
   stage: package
@@ -65,7 +61,7 @@ package_files:
     paths:
       - dist/*
   script:
-    - python -m build # or something similar
+    - tox -e build
 
 package_docs:
   stage: package
@@ -95,11 +91,11 @@ publish_on_gitlab:
     - if: $CI_COMMIT_TAG
   script:
     - echo "run twine for gitlab"
-    # - |
-    #   TWINE_PASSWORD=${CI_JOB_TOKEN} \
-    #   TWINE_USERNAME=gitlab-ci-token \
-    #   python -m twine upload \
-    #   --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
+    - |
+      TWINE_PASSWORD=${CI_JOB_TOKEN} \
+      TWINE_USERNAME=gitlab-ci-token \
+      python -m twine upload \
+      --repository-url ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/pypi dist/*
 
 publish_on_test_pypi:
   stage: publish
diff --git a/MANIFEST.in b/MANIFEST.in
index d79ea0268d481ed3370c7379c7ef90add6c227be..b13ed9c7090e38c21c3dfea23425f74b879f3da2 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,5 +3,4 @@ include README.md
 include VERSION
 
 recursive-include docs *
-
 recursive-exclude tests *
diff --git a/README.md b/README.md
index e554c826d2491a256b60587858ac0ef0cdf3db30..605e1f0c43569ad5546b1bcaa6247a6fb5074475 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,11 @@
 # Example Python Package
 
-An example repository of an CI/CD pipeline for building, testing and publishing a python package
+An example repository of an CI/CD pipeline for building, testing and publishing a python package.
 
+If you find some missing functionality with regards to CI/CD, testing, linting or something else, feel free to make a merge request with the proposed changes.
+
+
+## Example of README.md contents below:
 
 ## Installation
 ```
@@ -15,5 +19,10 @@ from map import cool_module
 cool_module.greeter()   # prints "Hello World"
 ```
 
+## Contributing
+
+To contribute, please create a feature branch and a "Draft" merge request.
+Upon completion, the merge request should be marked as ready and a reviewer should be assigned.
+
 ## License
 This project is licensed under the Apache License Version 2.0
diff --git a/tests/requirements.txt b/tests/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c28d83eb59417e45937be95328d3224a587a81d7
--- /dev/null
+++ b/tests/requirements.txt
@@ -0,0 +1,6 @@
+black
+build
+flake8
+pylint
+pytest
+pytest-cov
diff --git a/tests/test_cool_module.py b/tests/test_cool_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..da1002b8b66176f7efb5e24c3b4748d0eb49ed51
--- /dev/null
+++ b/tests/test_cool_module.py
@@ -0,0 +1,13 @@
+"""Testing of the Cool Module"""
+from unittest import TestCase
+
+from map.cool_module import greeter
+
+
+class TestCoolModule(TestCase):
+    """Test Case of the Cool Module"""
+
+    def test_greeter(self):
+        """Testing that the greeter does not crash"""
+        greeter()
+        self.assertEqual(2 + 2, 4)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000000000000000000000000000000000000..0c73407b467025c899dbf77bd869e9633a6f0356
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,36 @@
+[tox]
+# Generative environment list to test all supported Python versions
+envlist = py3{7,8,9,10},black,pep8,pylint
+minversion = 3.18.0
+# Source distributions are explicitly build using tox -e build
+skipsdist = True
+
+[testenv]
+usedevelop = True
+setenv =
+    LANGUAGE=en_US
+    LC_ALL=en_US.UTF-8
+    PYTHONWARNINGS=default::DeprecationWarning
+deps =
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/tests/requirements.txt
+commands =
+    {envpython} --version
+    {envpython} -m pytest --cov=map
+
+# Use generative name and command prefixes to reuse the same virtualenv
+# for all linting jobs.
+[testenv:{pep8,black,pylint}]
+usedevelop = False
+envdir = {toxworkdir}/linting
+commands =
+    pep8: {envpython} -m flake8 --version
+    pep8: {envpython} -m flake8 --extend-exclude './.venv/','./venv/'
+    black: {envpython} -m black --version
+    black: {envpython} -m black --check --diff .
+    pylint: {envpython} -m pylint --version
+    pylint: {envpython} -m pylint map tests
+
+[testenv:build]
+usedevelop = False
+commands = {envpython} -m build