diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..df29a127ca3480867828e46b7d703f3ca33854ee
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,2 @@
+[run]
+source=kvalikirstu2
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 0371e0f0529016dc8053323da469406e44025070..73c1f4b5142741c9921a1305949cc2c0b4bba81a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,11 +25,11 @@ docs/_static
 docs/_templates
 .hgignore
 .babelrc
-.coveragerc
 .hg/
 .hgtags
 .idea/
-.pylintrc
 .tox
 dist/
 kvalikirstu2.egg-info/
+build/*
+tests/test files/*
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000000000000000000000000000000000000..a63f80b63deb196ad4bec5f0ee89542544825b86
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,2 @@
+[FORMAT]
+max-line-length=120
\ No newline at end of file
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000000000000000000000000000000000000..de9ead0d6c1f9f582267d6c7ba3176c087117805
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,159 @@
+#!/usr/bin/env groovy
+
+node_name = 'default'
+email_recipient = ''
+pylint_targets = 'kvalikirstu2'
+
+def sendmail(build_result) {
+    stage('Send mail') {
+        mail(to: email_recipient,
+             subject: "Job '${env.JOB_NAME}' (${env.BUILD_NUMBER}) result is ${build_result}",
+             body: "See info at ${env.BUILD_URL}")
+    }
+}
+
+// Discard old builds
+properties([buildDiscarder(logRotator(artifactDaysToKeepStr: '', artifactNumToKeepStr: '', daysToKeepStr: '', numToKeepStr: '30'))])
+
+// node reserves executor and workspace
+node(node_name) {
+    // Prepare
+    // -------
+    def toxEnvName = '.env-tox'
+    def pylintEnvName = '.env-pylint'
+    def sqScannerHome = tool 'SonarQube Scanner'
+    def pylint_report_path = 'pylint_report.txt'
+    def coverage_xml_path = 'coverage.xml'
+    def sonar_properties_path = 'sonar-project.properties'
+
+    // prepare workspace
+    def myworkspace = ''
+    // Get current node, to force concurrent steps to be run on the same node.
+    def current_node = "${env.NODE_NAME}"
+
+    // Parallelism causes stages to be run in different workspaces.
+    // Before submitting to SonarQube we need to make sure pylint_report
+    // coverage.xml and sonar-project.properties files are in-place.
+    // tasks shall be run in parallel
+    def tasks_1 = [:]
+    def tasks_2 = [:]
+
+    myworkspace = "${WORKSPACE}"
+    echo "My workspace is ${myworkspace}"
+    deleteDir()
+
+    // Get recipient from git commit author
+    def scmVars = checkout scm
+    email_recipient = sh (
+        script: "git show -s --pretty=%ae",
+        returnStdout: true
+    )
+    echo "Build result will be sent to ${email_recipient}"
+
+    // Assign parallel tasks
+    tasks_1['Prepare Tox, Run With Coverage & Publish Report'] = {
+        node(current_node) {
+            dir(myworkspace) {
+                stage('Prepare Tox Venv') {
+                    if (!fileExists(toxEnvName)) {
+                        echo 'Build Python Virtualenv for testing...'
+                        sh """
+                        python-latest -m venv ${toxEnvName}
+                        . ./${toxEnvName}/bin/activate
+                        pip install --upgrade pip
+                        pip install tox
+                        """
+                    }
+                }
+                stage('Run Test Suite & Gather Coverage') {
+                    sh """
+                    . ./${toxEnvName}/bin/activate
+                    tox -e jenkins-with-coverage
+                    """
+                }
+                stage('Publish Cobertura Report') {
+                    cobertura(coberturaReportFile: "${coverage_xml_path}",
+                              zoomCoverageChart: false)
+                }
+            }
+        }
+    }
+    tasks_1['Prepare Pylint, Run Analysis, Archive & Publish report'] = {
+        node(current_node) {
+            dir(myworkspace) {
+                stage('Prepare Pylint Venv') {
+                    if (!fileExists(pylintEnvName)) {
+                        echo 'Build Python Virtualenv for linting...'
+                        sh """
+                        python-latest -m venv ${pylintEnvName}
+                        . ./${pylintEnvName}/bin/activate
+                        pip install --upgrade pip
+                        pip install -r ./requirements.txt
+                        pip install .
+                        pip install pylint
+                        """
+                    }
+                }
+                stage('Run PyLint') {
+                    echo 'Run pylint'
+                    sh """
+                    . ./${pylintEnvName}/bin/activate
+                    pylint -f parseable ${pylint_targets} | tee ${pylint_report_path}
+                    """
+                }
+                stage('Archive PyLint Report') {
+                    archiveArtifacts artifacts: pylint_report_path
+                }
+                stage('Publish PyLint Report') {
+                    recordIssues tool: pyLint(pattern: pylint_report_path)
+                }
+            }
+        }
+    }
+    tasks_2['Run Tests py311'] = {
+        node(current_node) {
+            dir(myworkspace) {
+                stage('Run Tests') {
+                    sh """
+                    . ./${toxEnvName}/bin/activate
+                    tox -e jenkins
+                    """
+                }
+            }
+        }
+    }
+    tasks_2['Initiate SonarQube Analysis'] = {
+        node(current_node) {
+            dir(myworkspace) {
+                stage('Prepare sonar-project.properties') {
+                    sh "echo sonar.projectVersion = \$(cat VERSION) >> ${sonar_properties_path}"
+                }
+                stage('Initiate SonarQube analysis') {
+                    withSonarQubeEnv() {
+                        sh "${sqScannerHome}bin/sonar-scanner"
+                    }
+                }
+            }
+        }
+    }
+    try {
+        // run parallel tasks
+        parallel tasks_1
+        parallel tasks_2
+    } catch (err) {
+        currentBuild.result = 'FAILURE'
+        sendmail('FAILURE')
+    }
+}
+// Wait for sonar quality gate
+stage("Quality Gate") {
+    timeout(time: 10, unit: 'MINUTES') { // Just in case something goes wrong, pipeline will be killed after a timeout
+        def qg = waitForQualityGate() // Reuse taskId previously collected by withSonarQubeEnv
+        if (qg.status != 'OK') {
+            echo "Pipeline unstable due to quality gate failure: ${qg.status}"
+            currentBuild.result = 'UNSTABLE'
+            sendmail('UNSTABLE')
+        }
+    }
+}
+
diff --git a/MANIFEST.in b/MANIFEST.in
index 16cb432faab5739f4f278a56b0275f86639aa2a2..baf28d9b5320f52312b25359994996618cd38e36 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,14 +1,11 @@
-include README.txt
-recursive-include tests *
-recursive-include kvalikirstu2 *
-recursive-include docs *
-recursive-include "kvalikirstu2/locale" *
-include scripts/*
-include scripts/docs/*
-include *.include
-include *.ini
+include VERSION
+include *.rst
 include *.txt
+include tox.ini
+include .pylintrc
+include .coveragerc
+include tests/*
 
-global-exclude *.py[co]
-global-exclude __pycache__
-prune kvalikirstu2/.vscode
\ No newline at end of file
+recursive-include "tests/test files src" *
+recursive-include tests *
+recursive-include "kvalikirstu2/locale" *
\ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000000000000000000000000000000000000..345f8cc0391bff8037eee5caeeff124f0f4de824
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+0.1.10
\ No newline at end of file
diff --git a/kvalikirstu2/utils.py b/kvalikirstu2/utils.py
index e82c4e3ce60544d92770f693aff986cbb45730b5..14c5376c354c75e15a24f9feaccdad44d4a892aa 100644
--- a/kvalikirstu2/utils.py
+++ b/kvalikirstu2/utils.py
@@ -153,11 +153,10 @@ def json_serialize(obj, path: str):
 
     """
     logger.debug('Saving %s to path %s', obj, path)
-    json_string = jsonpickle.encode(obj)
+    json_string = jsonpickle.encode(obj, make_refs=False)
     obj = json.loads(json_string)
     output = json.dumps(obj, indent=4, ensure_ascii=False, sort_keys=True).encode('utf8')
     create_folder_if_not_exists(os.path.dirname(path))
-
     with open(path, 'wb') as file_stream:
         file_stream.write(output)
 
diff --git a/requirements.txt b/requirements.txt
index 7fc8b60e9f4cdb823b7973e91a230105ea6f20be..44b7ef120b3adc2d13dfc9087b4015d44f676f42 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,14 @@
-Babel>=2.8.0
-chardet==3.0.4
+Babel==2.13.1
+chardet==5.2
 ConfigArgParse==0.14.0
 defusedxml==0.5.0
-Jinja2==2.10
-jsonpickle==1.1
-MarkupSafe==1.1.1
-natsort==6.2.0
+Jinja2==3.0.3
+jsonpickle==3.0.2
+MarkupSafe==2.1.3
+natsort==8.4.0
 odfpy==1.4.0
-Pillow>=6.0.0
-six>=1.14.0
-wxPython==4.1.1;platform_system=="Windows"
+Pillow==10.1.0
+setuptools==58
+six==1.16.0
+wxPython==4.2.1;platform_system=="Windows"
 pypubsub===4.0.3
\ No newline at end of file
diff --git a/requirements_test.txt b/requirements_test.txt
index 92507001a0b6b8c260d9a62996c804c75175a834..0fcad4351a49db6aa5a81bba32c991b23ea97938 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -1,17 +1,3 @@
-Babel==2.6.0
-chardet==3.0.4
-ConfigArgParse==0.14.0
-defusedxml==0.5.0
-Jinja2==2.10
-jsonpickle==1.1
-MarkupSafe==1.1.1
-natsort==6.2.0
-odfpy==1.4.0
-Pillow>=6.0.0
-six>=1.14.0
-wxPython==4.1.1;platform_system=="Windows"
-https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04/wxPython-4.1.1-cp38-cp38-linux_x86_64.whl; sys_platform == "linux"
-pytest<=4.0
-pytest-cov<=2.8
-pyvirtualdisplay==0.2.1
-pypubsub===4.0.3
\ No newline at end of file
+pytest==7.4.3
+pytest-cov==4.1.0
+pyvirtualdisplay==3.0
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 6af4de7343eab71441e43d37a2418fdb615a2304..22996b61dd7da2a7bc91fd0c9e100b04f78e4c4e 100644
--- a/setup.py
+++ b/setup.py
@@ -2,12 +2,16 @@
 # Author(s): Saara Saaninkoski and Jukka Lipsanen
 # Copyright 2019 Finnish Social Science Data Archive FSD / University of Tampere
 # Licensed under the EUPL. See LICENCE.txt for full license.
-
+import os
 from setuptools import setup, find_packages
 
+this_dir = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(this_dir, 'VERSION'), 'r') as fileobj:
+    version = fileobj.readline().strip()
+
 setup(
     name='kvalikirstu2',
-    version='1.0.10',
+    version=version,
     url='',
     description='A tool for analyzing qualitative studies',
     author='Jukka Lipsanen and Saara Saaninkoski',
@@ -22,9 +26,6 @@ setup(
         'pypubsub',
         'natsort'
         ],
-    tests_require=['pytest'],
-    setup_requires=['Babel'],
-    include_package_data=True,
     entry_points={
         'console_scripts': [
             'python-kvalikirstu2-gui = kvalikirstu2.gui:main',
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000000000000000000000000000000000000..9ee5b18f7d87e0cf6b14855ae99968d4a3b606db
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,9 @@
+# unique project identifier (required) - The project's unique key. Allowed characters are: letters,
+# numbers, -, _, . and :, with at least one non-digit.
+sonar.projectKey=kvalikirstu2
+# path to source directories (required) - Comma-separated paths to directories containing main source files.
+sonar.sources=kvalikirstu2
+# Path to coverage (Cobertura XML) report
+sonar.python.coverage.reportPaths=coverage.xml
+# Path to pylint report
+sonar.python.pylint.reportPath=pylint_report.txt
diff --git a/tests/conftest.py b/tests/conftest.py
index 49222557e9f71292d0ffb84a2d5287cddfe399a6..4afba12bae6c2203e4c44c807a0c596b94a2b1b2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -3,6 +3,8 @@ import os
 import pytest
 import logging
 
+from kvalikirstu2 import argument_parser
+
 TEST_FILES_SRC = os.path.join(os.path.dirname(__file__), "test files src")
 TEST_FILES_DST = os.path.join(os.path.dirname(__file__), "test files")
 VARS = {'display': None}
@@ -20,10 +22,11 @@ def setup(request):
     request.addfinalizer(finalize)
     setup_rootlogger()
 
-    if os.name == "posix" and "DISPLAY" not in os.environ:
+    if os.name == "posix" and (all(not os.getenv(x) for x in ["DISPLAY", "NO_WX"])):
         import pyvirtualdisplay
         VARS['display'] = pyvirtualdisplay.Display()
         VARS['display'].start()
+    argument_parser.Settings().set_up_cli_settings([])
 
 
 def rootlogger_error(*args, **kwargs):
diff --git a/tests/test_argument_parser.py b/tests/test_argument_parser.py
index 1164368ece5c442d27e63886ea1dc85506a699cd..16a6223c8ccf669eae2c275e55df10c71fd12fff 100644
--- a/tests/test_argument_parser.py
+++ b/tests/test_argument_parser.py
@@ -82,6 +82,7 @@ class ArgumentParserTest(unittest.TestCase, test_writer.CitReqMixin, test_kvali_
 
             self.assertTrue(mock.called)
 
+    @unittest.skipIf(os.getenv('NO_WX'), 'Skipping tests that require wxPython')
     def test_handler_for_gui(self):
         settings = argument_parser.Settings()
         settings.set_up_gui_settings([])
diff --git a/tests/test_backup_select_window.py b/tests/test_backup_select_window.py
index 5b1a7d978af3a49b14b092ba98b5a90cb680cb31..5515c7cdc166af4809c8fd36e1cdcb31a51a6813 100644
--- a/tests/test_backup_select_window.py
+++ b/tests/test_backup_select_window.py
@@ -1,4 +1,9 @@
+# Skip this module if NO_WX in env
+import pytest
 import os
+if os.getenv('NO_WX'):
+    pytest.skip('Skipping module because wxPython is not available', allow_module_level=True)
+
 import unittest
 import wx
 from kvalikirstu2 import argument_parser
diff --git a/tests/test_converter.py b/tests/test_converter.py
index b9e53d829f922e487cb1afa594320cc10dddd966..1d8f92ec069299fb5349ffc63cb2b2ec2c35b9e5 100644
--- a/tests/test_converter.py
+++ b/tests/test_converter.py
@@ -119,6 +119,7 @@ class TestConvertEncoding(unittest.TestCase):
         self.subfolder_conversion_test("qwertyuiopasdfghjklzxcvbnmåäö", 'cp1252', 10)
         self.subfolder_conversion_test("qwertyuiopasdfghjklzxcvbnmåäö", 'utf-8', 10)
 
+    @unittest.skipIf(os.getenv('NO_ODT'), 'Skipping tests that require LibreOffice')
     def test_libreoffice_conversion_to_odt(self):
         docx_path = os.path.join(self.temp_path, "data", "test_data1.docx")
         data_path = os.path.join(self.temp_path, "data")
@@ -133,6 +134,7 @@ class TestConvertEncoding(unittest.TestCase):
         self.assertFalse(os.path.exists(docx_path))
         self.assertEqual(expected_content, content)
 
+    @unittest.skipIf(os.getenv('NO_ODT'), 'Skipping tests that require LibreOffice')
     def test_libreoffice_conversion_to_txt(self):
         docx_path = os.path.join(self.temp_path, "data", "test_data1.docx")
         data_path = os.path.join(self.temp_path, "data")
@@ -148,6 +150,7 @@ class TestConvertEncoding(unittest.TestCase):
         self.assertFalse(os.path.exists(docx_path))
         self.assertEqual(lines1, lines2)
 
+    @unittest.skipIf(os.getenv('NO_ODT'), 'Skipping tests that require LibreOffice')
     def test_timeout_low_throws(self):
         data_path = os.path.join(self.temp_path, "data")
         with self.assertRaises(subprocess.TimeoutExpired):
diff --git a/tests/test_csv_generator.py b/tests/test_csv_generator.py
index 1fb5c075e2b87c9285df7c139b73ff514e6cbb7c..f27f8bbdf7c5960b253494db96932a87c43e10db 100644
--- a/tests/test_csv_generator.py
+++ b/tests/test_csv_generator.py
@@ -24,7 +24,7 @@ class TestCSVGenerator(unittest.TestCase):
         self.interface = DummyInterface()
 
     def test_generator(self):
-        reader = study_reader.StudyReader('FSDxxxx', True, data_file=get_testpath(), study_path=get_testdir())
+        reader = study_reader.StudyReader('FSDxxxx', 'Title', data_file=get_testpath(), study_path=get_testdir())
         header_info = header_scanner.get_header_info_for_study(get_testpath())
         study = reader.get_study()
         csv_path = os.path.join(get_testdir(), 'daFxxxx.csv')
diff --git a/tests/test_gui.py b/tests/test_gui.py
index bb0d9964e071ec01b75908f5a46aab1056f78716..69abf12a6a6cfeae4f3ade836de77ab0f8886235 100644
--- a/tests/test_gui.py
+++ b/tests/test_gui.py
@@ -1,5 +1,11 @@
 """ Tests for gui classes.
 """
+# Skip this module if NO_WX in env
+import pytest
+import os
+if os.getenv('NO_WX'):
+    pytest.skip('Skipping module because wxPython is not available', allow_module_level=True)
+
 import os
 import unittest
 import wx
diff --git a/tests/test_gui_model.py b/tests/test_gui_model.py
index bf762542a8df0743387d198e04fa65c834ca6d4f..032409a5901a16726d0b084cfd2f159a04f52d89 100644
--- a/tests/test_gui_model.py
+++ b/tests/test_gui_model.py
@@ -68,6 +68,7 @@ class TestGUIModel(unittest.TestCase):
         self.model.save_changes_to_daf()
         self.assertFalse(self.model.check_unsaved_changes())
 
+    @unittest.skipIf(os.getenv('NO_ODT'), 'Skipping tests that require Libreoffice')
     def test_conversion(self):
         self.model.convert_to_odt()
         test_data_path = os.path.join(self.temp_path, "data", "to_convert.odt")
diff --git a/tests/test_gui_utils.py b/tests/test_gui_utils.py
index bc69af5c912d2f139a43c5e369afcd7e75a06e8a..a9bbcb5f05f1b2f1197729fa4d6c113c3a5ccdb3 100644
--- a/tests/test_gui_utils.py
+++ b/tests/test_gui_utils.py
@@ -1,3 +1,9 @@
+# Skip this module if NO_WX in env
+import pytest
+import os
+if os.getenv('NO_WX'):
+    pytest.skip('Skipping module because wxPython is not available', allow_module_level=True)
+
 import unittest
 import wx
 from kvalikirstu2 import argument_parser, gui_utils
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index 8e97c2b8a047bf7edfe1d6c1d2b55a169e8a0c0b..da69036d3aa6cb5dbc2988f48a9c16ee6da52263 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -11,7 +11,7 @@ from kvalikirstu2 import argument_parser
 
 @pytest.fixture(name='testdir')
 def get_testdir():
-    return os.path.join(os.path.dirname(__file__), "test files", "valid")
+    return os.path.join(os.path.dirname(__file__), "test files")
 
 
 @pytest.fixture(name='settings')
@@ -34,7 +34,7 @@ class TestSerialization(unittest.TestCase):
     @pytest.fixture(autouse=True)
     def init_reader_valid(self, testdir, test_id, store_args):
         store_args
-        self.reader = study_reader.StudyReader(testdir, test_id, "", study_path=testdir)
+        self.reader = study_reader.StudyReader(study_id=test_id, study_path=testdir, study_title="", data_folder_name="data")
 
     """For testing the serialization in the utils module"""
     def test_study_reader(self):
@@ -47,7 +47,7 @@ class TestSerialization(unittest.TestCase):
         loaded_study.write_to_file()
 
         # Load a copy of the study from file
-        copy = study.Study(self.reader.study_id, self.reader.study_path, "")
+        copy = study.Study(study_id=self.reader.study_id, study_path=self.reader.study_path, study_title="")
         copy.load_from_file()
         os.unlink(loaded_study.get_full_path())
 
diff --git a/tests/test_tmp_edit_window.py b/tests/test_tmp_edit_window.py
index 987ec00fc6faa4e75033fd2b15f80afc2bf881dd..e53291e0dacc3e5659c5c967c998eb148a0ebd83 100644
--- a/tests/test_tmp_edit_window.py
+++ b/tests/test_tmp_edit_window.py
@@ -1,3 +1,9 @@
+# Skip this module if NO_WX in env
+import pytest
+import os
+if os.getenv('NO_WX'):
+    pytest.skip('Skipping module because wxPython is not available', allow_module_level=True)
+
 import unittest
 import os
 import tempfile
diff --git a/tests/test_warning_handler.py b/tests/test_warning_handler.py
index 9976ffaa72b971bb0d56c4a381f0149eba9c4dab..7dcbfe32e26d3bce1463ce0b5fdd051f42b66aa7 100644
--- a/tests/test_warning_handler.py
+++ b/tests/test_warning_handler.py
@@ -1,4 +1,11 @@
+# Skip this module if NO_WX in env
+import pytest
+import os
+if os.getenv('NO_WX'):
+    pytest.skip('Skipping module because wxPython is not available', allow_module_level=True)
+
 import logging
+from unittest import mock
 import unittest
 from kvalikirstu2 import argument_parser
 
@@ -9,22 +16,22 @@ class TestWarningHandler(unittest.TestCase):
         settings.set_up_gui_settings([])
         self.logger = logging.getLogger('kvalikirstu2.testlogger')
 
-    @unittest.mock.patch('wx.MessageBox')
+    @mock.patch('wx.MessageBox')
     def test_logger_opens_warning_message(self, mock):
         self.logger.warning('test')
         self.assertTrue(mock.called)
 
-    @unittest.mock.patch('wx.MessageBox')
+    @mock.patch('wx.MessageBox')
     def test_logger_opens_error_message(self, mock):
         self.logger.error('test')
         self.assertFalse(mock.called)
 
-    @unittest.mock.patch('wx.MessageBox')
+    @mock.patch('wx.MessageBox')
     def test_info(self, mock):
         self.logger.info('test')
         self.assertFalse(mock.called)
 
-    @unittest.mock.patch('wx.MessageBox')
+    @mock.patch('wx.MessageBox')
     def test_debug(self, mock):
         self.logger.info('test')
         self.assertFalse(mock.called)
diff --git a/tox.ini b/tox.ini
index b23ea8bee7a5ad8af16000750cf76128cc855c79..a8af8e8763cda11ab06dcb8e026a3772ad8f2c68 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,8 +1,31 @@
+[tox]
+envlist = py38, py310, py311
+
 [testenv]
-deps = -rrequirements_test.txt
+deps = -rrequirements.txt
+       -rrequirements_test.txt
+commands = pytest
+
+[testenv:with-coverage]
+deps = {[testenv]deps}
+       coverage==7.2.3
+commands = pytest --cov=kvalikirstu2
+           coverage xml
+           coverage report
+
+[testenv:jenkins]
+setenv =
+    NO_WX = 1
+    NO_ODT = 1
+deps = {[testenv]deps}
+commands = {[testenv]commands}
 
-commands = pytest --cov --cov-append --cov-report=term-missing {posargs}
-           pytest --cov --cov-report xml {posargs}
+[testenv:jenkins-with-coverage]
+setenv =
+    NO_WX = 1
+    NO_ODT = 1
+deps = {[testenv:with-coverage]deps}
+commands = {[testenv:with-coverage]commands}
 
 [flake8]
 max-line-length = 120
\ No newline at end of file