Repository: spyder-ide/spyder-unittest (language: python)

Issue:
Test configuration
<!--- If your issue is about a bug, please help us to reproduce and fix it by describing step by step in detail what you were doing when the error occurred. Feel free to delete irrelevant sections. Thanks. --->

## Description of your problem
I'd like to run my tests with the flag `--mpl` to enable `pytest-mpl`, but as far as I can see it is not possible?

As a minor feature request (as in less "urgent"), it would be nice to be able to set environment variables when running the tests as well. For example `PYTEST_QT_API`. It is possible to set it before starting Spyder, but sometimes one would like to change without restarting or simply forget to set it...


## What steps will reproduce the problem?

1. 
2. 
3. 

## What is the expected output? What do you see instead?



### Paste traceback/error below (if applicable)
*(Copy from error dialog or `View > Panes > Internal Console`)*

```python-traceback

<!--- PASTE TRACEBACK HERE --->

```

## Key versions and other information:

* Spyder version: 5.4.2
* Version of spyder-unittest plugin: 0.5.1
* Installation method for Spyder and the unittest plugin: conda
* Python version: 3.10.10
* Testing framework used: py.test
* Testing framework version:
* Operating system:



Discussion / hints:
@oscargus You're right, that's not possible at the moment. I can add a text field to the test configuration window where you can specify additional command line arguments like `--mpl`. Would that work for you?
That would be awesome! Thanks!

Fix patch:
diff --git a/README.md b/README.md
index d0930b5..47f11fe 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,6 @@
 [![Windows status](https://github.com/spyder-ide/spyder-unittest/workflows/Windows%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Windows+tests%22)
 [![Linux status](https://github.com/spyder-ide/spyder-unittest/workflows/Linux%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Linux+tests%22)
 [![MacOS status](https://github.com/spyder-ide/spyder-unittest/workflows/Macos%20tests/badge.svg)](https://github.com/spyder-ide/spyder-notebook/actions?query=workflow%3A%22Macos+tests%22)
-[![codecov](https://codecov.io/gh/spyder-ide/spyder-unittest/branch/master/graph/badge.svg)](https://codecov.io/gh/spyder-ide/spyder-notebook/branch/master)
 [![Crowdin](https://badges.crowdin.net/spyder-unittest/localized.svg)](https://crowdin.com/project/spyder-unittest)
 
 *Copyright © 2014 Spyder Project Contributors*
diff --git a/codecov.yml b/codecov.yml
deleted file mode 100644
index 79efd00..0000000
--- a/codecov.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-# See https://docs.codecov.io/docs/codecovyml-reference
-
-coverage:
-  status:
-    project:
-      default:
-        threshold: 5%  # allow for 5% decrease in total coverage
-    patch:
-      default:
-        target: 50%  # require 50% of diff to be covered
-
-comment:
-  layout: "files"


Test patch:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 91d7b2a..23b2767 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -89,7 +89,7 @@ jobs:
           shell: bash
           command: |
             . ~/.profile
-            xvfb-run --auto-servernum pytest spyder_unittest --cov=spyder_unittest --cov-report=xml -vv
+            xvfb-run --auto-servernum pytest spyder_unittest -vv
       - name: Run tests (MacOS)
         if: matrix.OS == 'macos'
         uses: nick-fields/retry@v2
@@ -107,9 +107,3 @@ jobs:
           timeout_minutes: 10
           max_attempts: 3
           command: pytest spyder_unittest -x -vv
-      - name: Upload coverage to Codecov
-        if: matrix.OS == 'ubuntu' && matrix.PYTHON_VERSION == '3.10'
-        uses: codecov/codecov-action@v3
-        with:
-          token: ${{ secrets.CODECOV_TOKEN }}
-
diff --git a/requirements/tests.txt b/requirements/tests.txt
index ed20f92..fc2889e 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,5 +1,4 @@
 flaky
 nose2
 pytest>=5
-pytest-cov
 pytest-qt
diff --git a/spyder_unittest/backend/nose2runner.py b/spyder_unittest/backend/nose2runner.py
index 5ad8dd8..81efb8f 100644
--- a/spyder_unittest/backend/nose2runner.py
+++ b/spyder_unittest/backend/nose2runner.py
@@ -27,10 +27,12 @@ class Nose2Runner(RunnerBase):
 
     def create_argument_list(self, config, cov_path):
         """Create argument list for testing process."""
-        return [
+        arguments = [
             '-m', self.module, '--plugin=nose2.plugins.junitxml',
             '--junit-xml', '--junit-xml-path={}'.format(self.resultfilename)
         ]
+        arguments += config.args
+        return arguments
 
     def finished(self):
         """Called when the unit test process has finished."""
diff --git a/spyder_unittest/backend/pytestrunner.py b/spyder_unittest/backend/pytestrunner.py
index 388af6e..641ce2a 100644
--- a/spyder_unittest/backend/pytestrunner.py
+++ b/spyder_unittest/backend/pytestrunner.py
@@ -36,9 +36,9 @@ def create_argument_list(self, config, cov_path):
         arguments = [pyfile, str(self.reader.port)]
         if config.coverage:
             arguments += [f'--cov={cov_path}', '--cov-report=term-missing']
+        arguments += config.args
         return arguments
 
-
     def start(self, config, cov_path, executable, pythonpath):
         """Start process which will run the unit test suite."""
         self.config = config
diff --git a/spyder_unittest/backend/tests/test_pytestrunner.py b/spyder_unittest/backend/tests/test_pytestrunner.py
index 3c30a49..95862dd 100644
--- a/spyder_unittest/backend/tests/test_pytestrunner.py
+++ b/spyder_unittest/backend/tests/test_pytestrunner.py
@@ -22,7 +22,7 @@
 
 
 def test_pytestrunner_create_argument_list(monkeypatch):
-    config = Config()
+    config = Config(args=['--extra-arg'])
     cov_path = None
     MockZMQStreamReader = Mock()
     monkeypatch.setattr(
@@ -34,9 +34,11 @@ def test_pytestrunner_create_argument_list(monkeypatch):
     runner.reader = mock_reader
     monkeypatch.setattr('spyder_unittest.backend.pytestrunner.os.path.dirname',
                         lambda _: 'dir')
-    pyfile, port, *coverage = runner.create_argument_list(config, cov_path)
+    arg_list = runner.create_argument_list(config, cov_path)
+    pyfile, port, *coverage, last = arg_list
     assert pyfile == osp.join('dir', 'workers', 'pytestworker.py')
     assert port == '42'
+    assert last == '--extra-arg'
 
 
 def test_pytestrunner_start(monkeypatch):
diff --git a/spyder_unittest/backend/tests/test_unittestrunner.py b/spyder_unittest/backend/tests/test_unittestrunner.py
index 9c4b15a..2cc5fea 100644
--- a/spyder_unittest/backend/tests/test_unittestrunner.py
+++ b/spyder_unittest/backend/tests/test_unittestrunner.py
@@ -20,7 +20,7 @@ def test_unittestrunner_create_argument_list(monkeypatch):
     """
     Test that UnittestRunner.createArgumentList() returns the expected list.
     """
-    config = Config()
+    config = Config(args=['--extra-arg'])
     cov_path = None
     MockZMQStreamReader = Mock()
     monkeypatch.setattr(
@@ -37,7 +37,7 @@ def test_unittestrunner_create_argument_list(monkeypatch):
     result = runner.create_argument_list(config, cov_path)
 
     pyfile = osp.join('dir', 'workers', 'unittestworker.py')
-    assert result == [pyfile, '42']
+    assert result == [pyfile, '42', '--extra-arg']
 
 
 def test_unittestrunner_start(monkeypatch):
diff --git a/spyder_unittest/backend/unittestrunner.py b/spyder_unittest/backend/unittestrunner.py
index 1cb4cd5..e47d86f 100644
--- a/spyder_unittest/backend/unittestrunner.py
+++ b/spyder_unittest/backend/unittestrunner.py
@@ -27,6 +27,7 @@ def create_argument_list(self, config: Config, cov_path: str) -> list[str]:
         dirname = osp.dirname(__file__)
         pyfile = osp.join(dirname, 'workers', 'unittestworker.py')
         arguments = [pyfile, str(self.reader.port)]
+        arguments += config.args
         return arguments
 
     def start(self, config: Config, cov_path: str, executable: str,
diff --git a/spyder_unittest/tests/conftest.py b/spyder_unittest/tests/conftest.py
index bbce544..13aed8d 100644
--- a/spyder_unittest/tests/conftest.py
+++ b/spyder_unittest/tests/conftest.py
@@ -19,10 +19,13 @@
 from qtpy import QtWebEngineWidgets  # noqa
 
 # Spyder imports
+from spyder import dependencies
+from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY
 from spyder.app import start
 from spyder.config.manager import CONF
 
 
+
 @pytest.fixture
 def main_window():
     """Main Window fixture"""
@@ -31,7 +34,8 @@ def main_window():
     CONF.set('tours', 'show_tour_message', False)
     QApplication.processEvents()
 
-    from spyder.api.plugin_registration.registry import PLUGIN_REGISTRY
+    # Reset global state
+    dependencies.DEPENDENCIES = []
     PLUGIN_REGISTRY.reset()
 
     # Start the window
diff --git a/spyder_unittest/tests/test_unittestplugin.py b/spyder_unittest/tests/test_unittestplugin.py
index 4edbe26..2769583 100644
--- a/spyder_unittest/tests/test_unittestplugin.py
+++ b/spyder_unittest/tests/test_unittestplugin.py
@@ -5,13 +5,6 @@
 # (see LICENSE.txt for details)
 """
 Tests for the integration of the plugin with Spyder.
-
-All tests needs to be decorated with pytest.mark.order('last') to ensure
-that they are executed after all other tests because monkeypatching
-functions inside the plugin does not work after running these tests.
-
-The reason for this is not clear, but it may have to do with how plugins
-are imported in spyder/otherplugins.py.
 """
 
 # Standard library imports
@@ -19,7 +12,6 @@
 import os
 
 # Third party imports
-import pytest
 from qtpy.QtCore import Qt
 
 # Spyder imports
diff --git a/spyder_unittest/widgets/configdialog.py b/spyder_unittest/widgets/configdialog.py
index 17fd47b..cf01c9e 100644
--- a/spyder_unittest/widgets/configdialog.py
+++ b/spyder_unittest/widgets/configdialog.py
@@ -9,18 +9,20 @@
 The main entry point is `ask_for_config()`.
 """
 
+from __future__ import annotations
+
 # Standard library imports
-from collections import namedtuple
 from os import getcwd
-from pkgutil import find_loader as find_spec_or_loader
 import os.path as osp
+import shlex
+from typing import Optional, NamedTuple
 
 # Third party imports
 from qtpy.compat import getexistingdirectory
 from qtpy.QtCore import Slot
-from qtpy.QtWidgets import (QApplication, QComboBox, QDialog, QDialogButtonBox,
-                            QHBoxLayout, QLabel, QLineEdit, QPushButton,
-                            QVBoxLayout, QCheckBox)
+from qtpy.QtWidgets import (
+    QApplication, QComboBox, QDialog, QDialogButtonBox, QGridLayout,
+    QHBoxLayout, QLabel, QLineEdit, QPushButton, QVBoxLayout, QCheckBox)
 from spyder.config.base import get_translation
 from spyder.utils import icon_manager as ima
 
@@ -31,8 +33,11 @@
     import gettext
     _ = gettext.gettext
 
-Config = namedtuple('Config', ['framework', 'wdir', 'coverage'])
-Config.__new__.__defaults__ = (None, '', False)
+class Config(NamedTuple):
+    framework: Optional[str] = None
+    wdir: str = ''
+    coverage: bool = False
+    args: list[str] = []
 
 
 class ConfigDialog(QDialog):
@@ -46,6 +51,13 @@ class ConfigDialog(QDialog):
     the OK button.
     """
 
+    # Width of strut in the layout of the dialog window; this determines
+    # the width of the dialog
+    STRUT_WIDTH = 400
+
+    # Extra vertical space added between elements in the dialog
+    EXTRA_SPACE = 10
+
     def __init__(self, frameworks, config, versions, parent=None):
         """
         Construct a dialog window.
@@ -65,10 +77,14 @@ def __init__(self, frameworks, config, versions, parent=None):
         self.versions = versions
         self.setWindowTitle(_('Configure tests'))
         layout = QVBoxLayout(self)
+        layout.addStrut(self.STRUT_WIDTH)
+
+        grid_layout = QGridLayout()
 
-        framework_layout = QHBoxLayout()
-        framework_label = QLabel(_('Test framework'))
-        framework_layout.addWidget(framework_label)
+        # Combo box for selecting the test framework
+
+        framework_label = QLabel(_('Test framework:'))
+        grid_layout.addWidget(framework_label, 0, 0)
 
         self.framework_combobox = QComboBox(self)
         for ix, (name, runner) in enumerate(sorted(frameworks.items())):
@@ -79,11 +95,25 @@ def __init__(self, frameworks, config, versions, parent=None):
                 label = '{} ({})'.format(name, _('not available'))
             self.framework_combobox.addItem(label)
             self.framework_combobox.model().item(ix).setEnabled(installed)
+        grid_layout.addWidget(self.framework_combobox, 0, 1)
+
+        # Line edit field for adding extra command-line arguments
+
+        args_label = QLabel(_('Command-line arguments:'))
+        grid_layout.addWidget(args_label, 1, 0)
+
+        self.args_lineedit = QLineEdit(self)
+        args_toolTip = _('Extra command-line arguments when running tests')
+        self.args_lineedit.setToolTip(args_toolTip)
+        grid_layout.addWidget(self.args_lineedit, 1, 1)
 
-        framework_layout.addWidget(self.framework_combobox)
-        layout.addLayout(framework_layout)
+        layout.addLayout(grid_layout)
+        spacing = grid_layout.verticalSpacing() + self.EXTRA_SPACE
+        grid_layout.setVerticalSpacing(spacing)
 
-        layout.addSpacing(10)
+        layout.addSpacing(self.EXTRA_SPACE)
+
+        # Checkbox for enabling coverage report
 
         coverage_label = _('Include coverage report in output')
         coverage_toolTip = _('Works only for pytest, requires pytest-cov')
@@ -94,9 +124,11 @@ def __init__(self, frameworks, config, versions, parent=None):
         coverage_layout.addWidget(self.coverage_checkbox)
         layout.addLayout(coverage_layout)
 
-        layout.addSpacing(10)
+        layout.addSpacing(self.EXTRA_SPACE)
+
+        # Line edit field for selecting directory
 
-        wdir_label = QLabel(_('Directory from which to run tests'))
+        wdir_label = QLabel(_('Directory from which to run tests:'))
         layout.addWidget(wdir_label)
         wdir_layout = QHBoxLayout()
         self.wdir_lineedit = QLineEdit(self)
@@ -107,7 +139,9 @@ def __init__(self, frameworks, config, versions, parent=None):
         wdir_layout.addWidget(self.wdir_button)
         layout.addLayout(wdir_layout)
 
-        layout.addSpacing(20)
+        layout.addSpacing(2 * self.EXTRA_SPACE)
+
+        # OK and Cancel buttons at the bottom
 
         self.buttons = QDialogButtonBox(QDialogButtonBox.Ok |
                                         QDialogButtonBox.Cancel)
@@ -120,14 +154,17 @@ def __init__(self, frameworks, config, versions, parent=None):
         self.framework_combobox.currentIndexChanged.connect(
             self.framework_changed)
 
+        # Set initial values to agree with the given config
+
         self.framework_combobox.setCurrentIndex(-1)
         if config.framework:
             index = self.framework_combobox.findText(config.framework)
             if index != -1:
                 self.framework_combobox.setCurrentIndex(index)
-        self.wdir_lineedit.setText(config.wdir)
         self.coverage_checkbox.setChecked(config.coverage)
         self.enable_coverage_checkbox_if_available()
+        self.args_lineedit.setText(shlex.join(config.args))
+        self.wdir_lineedit.setText(config.wdir)
 
     @Slot(int)
     def framework_changed(self, index):
@@ -173,8 +210,12 @@ def get_config(self):
         framework = self.framework_combobox.currentText()
         if framework == '':
             framework = None
+
+        args = self.args_lineedit.text()
+        args = shlex.split(args)
+
         return Config(framework=framework, wdir=self.wdir_lineedit.text(),
-                      coverage=self.coverage_checkbox.isChecked())
+                      coverage=self.coverage_checkbox.isChecked(), args=args)
 
 
 def ask_for_config(frameworks, config, versions, parent=None):
@@ -192,6 +233,14 @@ def ask_for_config(frameworks, config, versions, parent=None):
 
 if __name__ == '__main__':
     app = QApplication([])
-    frameworks = ['nose', 'pytest', 'unittest']
-    config = Config(framework=None, wdir=getcwd(), coverage=False)
-    print(ask_for_config(frameworks, config))
+    frameworks = {
+        'nose2': object,
+        'unittest': object,
+        'pytest': object}
+    versions = {
+        'nose2': {'available': False},
+        'unittest': {'available': True},
+        'pytest': {'available': True, 'plugins': {'pytest-cov', '3.1.4'}}
+    }
+    config = Config(wdir=getcwd())
+    print(ask_for_config(frameworks, config, versions))
diff --git a/spyder_unittest/widgets/tests/test_configdialog.py b/spyder_unittest/widgets/tests/test_configdialog.py
index bd646e2..4c7c30f 100644
--- a/spyder_unittest/widgets/tests/test_configdialog.py
+++ b/spyder_unittest/widgets/tests/test_configdialog.py
@@ -49,7 +49,7 @@ def is_installed(cls):
 
 
 def default_config():
-    return Config(framework=None, wdir=os.getcwd(), coverage=False)
+    return Config(framework=None, wdir=os.getcwd(), coverage=False, args=[])
 
 
 def test_configdialog_uses_frameworks(qtbot):
@@ -76,12 +76,13 @@ def test_configdialog_disables_unavailable_frameworks(qtbot):
 
 
 def test_configdialog_sets_initial_config(qtbot):
-    config = default_config()
+    config = Config(framework='pytest', wdir='/some/dir',
+                    coverage=True, args=['some', 'arg'])
     configdialog = ConfigDialog(frameworks, config, versions)
     assert configdialog.get_config() == config
 
 
-def test_configdialog_click_ham(qtbot):
+def test_configdialog_click_pytest(qtbot):
     configdialog = ConfigDialog(frameworks, default_config(), versions)
     qtbot.addWidget(configdialog)
     configdialog.framework_combobox.setCurrentIndex(1)
@@ -125,6 +126,13 @@ def test_configdialog_coverage_checkbox_pytestcov_noinstall(qtbot, monkeypatch):
     assert configdialog.coverage_checkbox.isEnabled() is False
 
 
+def test_configdialog_args_lineedit(qtbot):
+    configdialog = ConfigDialog(frameworks, default_config(), versions)
+    qtbot.addWidget(configdialog)
+    configdialog.args_lineedit.setText('-x "ham and" spam')
+    assert configdialog.get_config().args == ['-x', 'ham and', 'spam']
+
+
 def test_configdialog_wdir_lineedit(qtbot):
     configdialog = ConfigDialog(frameworks, default_config(), versions)
     qtbot.addWidget(configdialog)