Browse Source

[refactor] unit tests to utilize paramaterized and break down monolithic tests

- for tests which perform the same arrange/act/assert pattern but with different
  data, the data portion has been moved to the ``paramaterized.expand`` fields

- for monolithic tests which performed multiple arrange/act/asserts,
  they have been broken up into different unit tests.

- when possible, change generic assert statements to more concise
  asserts (i.e. ``assertIsNone``)

This work ultimately is focused on creating smaller and more concise tests.
While paramaterized may make adding new configurations for existing tests
easier, that is just a beneficial side effect.  The main benefit is that smaller
tests are easier to reason about, meaning they are easier to debug when they
start failing.  This improves the developer experience in debugging what went
wrong when refactoring the project.

Total number of tests went from 192 -> 259; or, broke apart larger tests into 69
more concise ones.
Grant Lanham 7 months ago
parent
commit
44a06190bb

+ 2 - 1
searx/query.py

@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # pylint: disable=invalid-name, missing-module-docstring, missing-class-docstring
 # pylint: disable=invalid-name, missing-module-docstring, missing-class-docstring
 
 
+from __future__ import annotations
 from abc import abstractmethod, ABC
 from abc import abstractmethod, ABC
 import re
 import re
 
 
@@ -258,7 +259,7 @@ class RawTextQuery:
         FeelingLuckyParser,  # redirect to the first link in the results list
         FeelingLuckyParser,  # redirect to the first link in the results list
     ]
     ]
 
 
-    def __init__(self, query, disabled_engines):
+    def __init__(self, query: str, disabled_engines: list):
         assert isinstance(query, str)
         assert isinstance(query, str)
         # input parameters
         # input parameters
         self.query = query
         self.query = query

+ 5 - 4
tests/unit/test_answerers.py

@@ -2,15 +2,16 @@
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 
 
 from mock import Mock
 from mock import Mock
+from parameterized import parameterized
 
 
 from searx.answerers import answerers
 from searx.answerers import answerers
 from tests import SearxTestCase
 from tests import SearxTestCase
 
 
 
 
 class AnswererTest(SearxTestCase):  # pylint: disable=missing-class-docstring
 class AnswererTest(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_unicode_input(self):
+    @parameterized.expand(answerers)
+    def test_unicode_input(self, answerer):
         query = Mock()
         query = Mock()
         unicode_payload = 'árvíztűrő tükörfúrógép'
         unicode_payload = 'árvíztűrő tükörfúrógép'
-        for answerer in answerers:
-            query.query = '{} {}'.format(answerer.keywords[0], unicode_payload)
-            self.assertTrue(isinstance(answerer.answer(query), list))
+        query.query = '{} {}'.format(answerer.keywords[0], unicode_payload)
+        self.assertIsInstance(answerer.answer(query), list)

+ 22 - 28
tests/unit/test_exceptions.py

@@ -1,42 +1,36 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 
 
+from parameterized import parameterized
 from tests import SearxTestCase
 from tests import SearxTestCase
 import searx.exceptions
 import searx.exceptions
 from searx import get_setting
 from searx import get_setting
 
 
 
 
 class TestExceptions(SearxTestCase):  # pylint: disable=missing-class-docstring
 class TestExceptions(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_default_suspend_time(self):
-        with self.assertRaises(searx.exceptions.SearxEngineAccessDeniedException) as e:
-            raise searx.exceptions.SearxEngineAccessDeniedException()
+    @parameterized.expand(
+        [
+            searx.exceptions.SearxEngineAccessDeniedException,
+            searx.exceptions.SearxEngineCaptchaException,
+            searx.exceptions.SearxEngineTooManyRequestsException,
+        ]
+    )
+    def test_default_suspend_time(self, exception):
+        with self.assertRaises(exception) as e:
+            raise exception()
         self.assertEqual(
         self.assertEqual(
             e.exception.suspended_time,
             e.exception.suspended_time,
-            get_setting(searx.exceptions.SearxEngineAccessDeniedException.SUSPEND_TIME_SETTING),
+            get_setting(exception.SUSPEND_TIME_SETTING),
         )
         )
 
 
-        with self.assertRaises(searx.exceptions.SearxEngineCaptchaException) as e:
-            raise searx.exceptions.SearxEngineCaptchaException()
-        self.assertEqual(
-            e.exception.suspended_time, get_setting(searx.exceptions.SearxEngineCaptchaException.SUSPEND_TIME_SETTING)
-        )
-
-        with self.assertRaises(searx.exceptions.SearxEngineTooManyRequestsException) as e:
-            raise searx.exceptions.SearxEngineTooManyRequestsException()
-        self.assertEqual(
-            e.exception.suspended_time,
-            get_setting(searx.exceptions.SearxEngineTooManyRequestsException.SUSPEND_TIME_SETTING),
-        )
-
-    def test_custom_suspend_time(self):
-        with self.assertRaises(searx.exceptions.SearxEngineAccessDeniedException) as e:
-            raise searx.exceptions.SearxEngineAccessDeniedException(suspended_time=1337)
+    @parameterized.expand(
+        [
+            searx.exceptions.SearxEngineAccessDeniedException,
+            searx.exceptions.SearxEngineCaptchaException,
+            searx.exceptions.SearxEngineTooManyRequestsException,
+        ]
+    )
+    def test_custom_suspend_time(self, exception):
+        with self.assertRaises(exception) as e:
+            raise exception(suspended_time=1337)
         self.assertEqual(e.exception.suspended_time, 1337)
         self.assertEqual(e.exception.suspended_time, 1337)
-
-        with self.assertRaises(searx.exceptions.SearxEngineCaptchaException) as e:
-            raise searx.exceptions.SearxEngineCaptchaException(suspended_time=1409)
-        self.assertEqual(e.exception.suspended_time, 1409)
-
-        with self.assertRaises(searx.exceptions.SearxEngineTooManyRequestsException) as e:
-            raise searx.exceptions.SearxEngineTooManyRequestsException(suspended_time=1543)
-        self.assertEqual(e.exception.suspended_time, 1543)

+ 3 - 3
tests/unit/test_external_bangs.py

@@ -90,7 +90,7 @@ class TestGetBangDefinitionAndAutocomplete(SearxTestCase):  # pylint:disable=mis
 
 
     def test_partial(self):
     def test_partial(self):
         bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('examp', external_bangs_db=TEST_DB)
         bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('examp', external_bangs_db=TEST_DB)
-        self.assertEqual(bang_definition, None)
+        self.assertIsNone(bang_definition)
         self.assertEqual(new_autocomplete, ['example'])
         self.assertEqual(new_autocomplete, ['example'])
 
 
     def test_partial2(self):
     def test_partial2(self):
@@ -100,7 +100,7 @@ class TestGetBangDefinitionAndAutocomplete(SearxTestCase):  # pylint:disable=mis
 
 
     def test_error(self):
     def test_error(self):
         bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('error', external_bangs_db=TEST_DB)
         bang_definition, new_autocomplete = get_bang_definition_and_autocomplete('error', external_bangs_db=TEST_DB)
-        self.assertEqual(bang_definition, None)
+        self.assertIsNone(bang_definition)
         self.assertEqual(new_autocomplete, [])
         self.assertEqual(new_autocomplete, [])
 
 
     def test_actual_data(self):
     def test_actual_data(self):
@@ -112,7 +112,7 @@ class TestGetBangDefinitionAndAutocomplete(SearxTestCase):  # pylint:disable=mis
 class TestExternalBangJson(SearxTestCase):  # pylint:disable=missing-class-docstring
 class TestExternalBangJson(SearxTestCase):  # pylint:disable=missing-class-docstring
     def test_no_external_bang_query(self):
     def test_no_external_bang_query(self):
         result = get_bang_url(SearchQuery('test', engineref_list=[EngineRef('wikipedia', 'general')]))
         result = get_bang_url(SearchQuery('test', engineref_list=[EngineRef('wikipedia', 'general')]))
-        self.assertEqual(result, None)
+        self.assertIsNone(result)
 
 
     def test_get_bang_url(self):
     def test_get_bang_url(self):
         url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='example'), external_bangs_db=TEST_DB)
         url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='example'), external_bangs_db=TEST_DB)

+ 91 - 83
tests/unit/test_locales.py

@@ -2,6 +2,8 @@
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 """Test some code from module :py:obj:`searx.locales`"""
 """Test some code from module :py:obj:`searx.locales`"""
 
 
+from __future__ import annotations
+from parameterized import parameterized
 from searx import locales
 from searx import locales
 from searx.sxng_locales import sxng_locales
 from searx.sxng_locales import sxng_locales
 from tests import SearxTestCase
 from tests import SearxTestCase
@@ -13,98 +15,104 @@ class TestLocales(SearxTestCase):
     - :py:obj:`searx.locales.match_locale`
     - :py:obj:`searx.locales.match_locale`
     """
     """
 
 
-    def test_match_locale(self):
-
-        locale_tag_list = [x[0] for x in sxng_locales]
-
+    @classmethod
+    def setUpClass(cls):
+        cls.locale_tag_list = [x[0] for x in sxng_locales]
+
+    @parameterized.expand(
+        [
+            'de',
+            'fr',
+            'zh',
+        ]
+    )
+    def test_locale_languages(self, locale: str):
         # Test SearXNG search languages
         # Test SearXNG search languages
-
-        self.assertEqual(locales.match_locale('de', locale_tag_list), 'de')
-        self.assertEqual(locales.match_locale('fr', locale_tag_list), 'fr')
-        self.assertEqual(locales.match_locale('zh', locale_tag_list), 'zh')
-
+        self.assertEqual(locales.match_locale(locale, self.locale_tag_list), locale)
+
+    @parameterized.expand(
+        [
+            ('ca-es', 'ca-ES'),
+            ('de-at', 'de-AT'),
+            ('de-de', 'de-DE'),
+            ('en-UK', 'en-GB'),
+            ('fr-be', 'fr-BE'),
+            ('fr-be', 'fr-BE'),
+            ('fr-ca', 'fr-CA'),
+            ('fr-ch', 'fr-CH'),
+            ('zh-cn', 'zh-CN'),
+            ('zh-tw', 'zh-TW'),
+            ('zh-hk', 'zh-HK'),
+        ]
+    )
+    def test_match_region(self, locale: str, expected_locale: str):
         # Test SearXNG search regions
         # Test SearXNG search regions
-
-        self.assertEqual(locales.match_locale('ca-es', locale_tag_list), 'ca-ES')
-        self.assertEqual(locales.match_locale('de-at', locale_tag_list), 'de-AT')
-        self.assertEqual(locales.match_locale('de-de', locale_tag_list), 'de-DE')
-        self.assertEqual(locales.match_locale('en-UK', locale_tag_list), 'en-GB')
-        self.assertEqual(locales.match_locale('fr-be', locale_tag_list), 'fr-BE')
-        self.assertEqual(locales.match_locale('fr-be', locale_tag_list), 'fr-BE')
-        self.assertEqual(locales.match_locale('fr-ca', locale_tag_list), 'fr-CA')
-        self.assertEqual(locales.match_locale('fr-ch', locale_tag_list), 'fr-CH')
-        self.assertEqual(locales.match_locale('zh-cn', locale_tag_list), 'zh-CN')
-        self.assertEqual(locales.match_locale('zh-tw', locale_tag_list), 'zh-TW')
-        self.assertEqual(locales.match_locale('zh-hk', locale_tag_list), 'zh-HK')
-
+        self.assertEqual(locales.match_locale(locale, self.locale_tag_list), expected_locale)
+
+    @parameterized.expand(
+        [
+            ('zh-hans', 'zh-CN'),
+            ('zh-hans-cn', 'zh-CN'),
+            ('zh-hant', 'zh-TW'),
+            ('zh-hant-tw', 'zh-TW'),
+        ]
+    )
+    def test_match_lang_script_code(self, locale: str, expected_locale: str):
         # Test language script code
         # Test language script code
+        self.assertEqual(locales.match_locale(locale, self.locale_tag_list), expected_locale)
 
 
-        self.assertEqual(locales.match_locale('zh-hans', locale_tag_list), 'zh-CN')
-        self.assertEqual(locales.match_locale('zh-hans-cn', locale_tag_list), 'zh-CN')
-        self.assertEqual(locales.match_locale('zh-hant', locale_tag_list), 'zh-TW')
-        self.assertEqual(locales.match_locale('zh-hant-tw', locale_tag_list), 'zh-TW')
-
-        # Test individual locale lists
-
-        self.assertEqual(locales.match_locale('es', [], fallback='fallback'), 'fallback')
-
+    def test_locale_de(self):
         self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
         self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
         self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
         self.assertEqual(locales.match_locale('de', ['de-CH', 'de-DE']), 'de-DE')
+
+    def test_locale_es(self):
+        self.assertEqual(locales.match_locale('es', [], fallback='fallback'), 'fallback')
         self.assertEqual(locales.match_locale('es', ['ES']), 'ES')
         self.assertEqual(locales.match_locale('es', ['ES']), 'ES')
         self.assertEqual(locales.match_locale('es', ['es-AR', 'es-ES', 'es-MX']), 'es-ES')
         self.assertEqual(locales.match_locale('es', ['es-AR', 'es-ES', 'es-MX']), 'es-ES')
         self.assertEqual(locales.match_locale('es-AR', ['es-AR', 'es-ES', 'es-MX']), 'es-AR')
         self.assertEqual(locales.match_locale('es-AR', ['es-AR', 'es-ES', 'es-MX']), 'es-AR')
         self.assertEqual(locales.match_locale('es-CO', ['es-AR', 'es-ES']), 'es-ES')
         self.assertEqual(locales.match_locale('es-CO', ['es-AR', 'es-ES']), 'es-ES')
         self.assertEqual(locales.match_locale('es-CO', ['es-AR']), 'es-AR')
         self.assertEqual(locales.match_locale('es-CO', ['es-AR']), 'es-AR')
 
 
-        # Tests from the commit message of 9ae409a05a
-
-        # Assumption:
-        #   A. When a user selects a language the results should be optimized according to
-        #      the selected language.
-        #
-        #   B. When user selects a language and a territory the results should be
-        #      optimized with first priority on territory and second on language.
-
-        # Assume we have an engine that supports the following locales:
-        locale_tag_list = ['zh-CN', 'zh-HK', 'nl-BE', 'fr-CA']
-
-        # Examples (Assumption A.)
-        # ------------------------
-
-        # A user selects region 'zh-TW' which should end in zh_HK.
-        # hint: CN is 'Hans' and HK ('Hant') fits better to TW ('Hant')
-        self.assertEqual(locales.match_locale('zh-TW', locale_tag_list), 'zh-HK')
-
-        # A user selects only the language 'zh' which should end in CN
-        self.assertEqual(locales.match_locale('zh', locale_tag_list), 'zh-CN')
-
-        # A user selects only the language 'fr' which should end in fr_CA
-        self.assertEqual(locales.match_locale('fr', locale_tag_list), 'fr-CA')
-
-        # The difference in priority on the territory is best shown with a
-        # engine that supports the following locales:
-        locale_tag_list = ['fr-FR', 'fr-CA', 'en-GB', 'nl-BE']
-
-        # A user selects only a language
-        self.assertEqual(locales.match_locale('en', locale_tag_list), 'en-GB')
-
-        # hint: the engine supports fr_FR and fr_CA since no territory is given,
-        # fr_FR takes priority ..
-        self.assertEqual(locales.match_locale('fr', locale_tag_list), 'fr-FR')
-
-        # Examples (Assumption B.)
-        # ------------------------
-
-        #  A user selects region 'fr-BE' which should end in nl-BE
-        self.assertEqual(locales.match_locale('fr-BE', locale_tag_list), 'nl-BE')
-
-        # If the user selects a language and there are two locales like the
-        # following:
-
-        locale_tag_list = ['fr-BE', 'fr-CH']
-
-        # The get_engine_locale selects the locale by looking at the "population
-        # percent" and this percentage has an higher amount in BE (68.%)
-        # compared to CH (21%)
-
-        self.assertEqual(locales.match_locale('fr', locale_tag_list), 'fr-BE')
+    @parameterized.expand(
+        [
+            ('zh-TW', ['zh-HK'], 'zh-HK'),  # A user selects region 'zh-TW' which should end in zh_HK.
+            # hint: CN is 'Hans' and HK ('Hant') fits better to TW ('Hant')
+            ('zh', ['zh-CN'], 'zh-CN'),  # A user selects only the language 'zh' which should end in CN
+            ('fr', ['fr-CA'], 'fr-CA'),  # A user selects only the language 'fr' which should end in fr_CA
+            ('nl', ['nl-BE'], 'nl-BE'),  # A user selects only the language 'fr' which should end in fr_CA
+            # Territory tests
+            ('en', ['en-GB'], 'en-GB'),  # A user selects only a language
+            (
+                'fr',
+                ['fr-FR', 'fr-CA'],
+                'fr-FR',
+            ),  # the engine supports fr_FR and fr_CA since no territory is given, fr_FR takes priority
+        ]
+    )
+    def test_locale_optimized_selected(self, locale: str, locale_list: list[str], expected_locale: str):
+        """
+        Tests from the commit message of 9ae409a05a
+
+        Assumption:
+          A. When a user selects a language the results should be optimized according to
+             the selected language.
+        """
+        self.assertEqual(locales.match_locale(locale, locale_list), expected_locale)
+
+    @parameterized.expand(
+        [
+            ('fr-BE', ['fr-FR', 'fr-CA', 'nl-BE'], 'nl-BE'),  # A user selects region 'fr-BE' which should end in nl-BE
+            ('fr', ['fr-BE', 'fr-CH'], 'fr-BE'),  # A user selects fr with 2 locales,
+            # the get_engine_locale selects the locale by looking at the "population
+            # percent" and this percentage has an higher amount in BE (68.%)
+            # compared to CH (21%)
+        ]
+    )
+    def test_locale_optimized_territory(self, locale: str, locale_list: list[str], expected_locale: str):
+        """
+        Tests from the commit message of 9ae409a05a
+
+          B. When user selects a language and a territory the results should be
+             optimized with first priority on territory and second on language.
+        """
+        self.assertEqual(locales.match_locale(locale, locale_list), expected_locale)

+ 94 - 111
tests/unit/test_plugins.py

@@ -2,6 +2,7 @@
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 
 
 from mock import Mock
 from mock import Mock
+from parameterized.parameterized import parameterized
 
 
 from searx import (
 from searx import (
     plugins,
     plugins,
@@ -23,143 +24,125 @@ class PluginMock:  # pylint: disable=missing-class-docstring, too-few-public-met
 
 
 
 
 class PluginStoreTest(SearxTestCase):  # pylint: disable=missing-class-docstring
 class PluginStoreTest(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_PluginStore_init(self):
-        store = plugins.PluginStore()
-        self.assertTrue(isinstance(store.plugins, list) and len(store.plugins) == 0)
+    def setUp(self):
+        self.store = plugins.PluginStore()
 
 
-    def test_PluginStore_register(self):
-        store = plugins.PluginStore()
+    def test_init(self):
+        self.assertEqual(0, len(self.store.plugins))
+        self.assertIsInstance(self.store.plugins, list)
+
+    def test_register(self):
         testplugin = PluginMock()
         testplugin = PluginMock()
-        store.register(testplugin)
+        self.store.register(testplugin)
+        self.assertEqual(1, len(self.store.plugins))
 
 
-        self.assertTrue(len(store.plugins) == 1)
+    def test_call_empty(self):
+        testplugin = PluginMock()
+        self.store.register(testplugin)
+        setattr(testplugin, 'asdf', Mock())
+        request = Mock()
+        self.store.call([], 'asdf', request, Mock())
+        self.assertFalse(getattr(testplugin, 'asdf').called)  # pylint: disable=E1101
 
 
-    def test_PluginStore_call(self):
+    def test_call_with_plugin(self):
         store = plugins.PluginStore()
         store = plugins.PluginStore()
         testplugin = PluginMock()
         testplugin = PluginMock()
         store.register(testplugin)
         store.register(testplugin)
         setattr(testplugin, 'asdf', Mock())
         setattr(testplugin, 'asdf', Mock())
         request = Mock()
         request = Mock()
-        store.call([], 'asdf', request, Mock())
-
-        self.assertFalse(testplugin.asdf.called)  # pylint: disable=E1101
-
         store.call([testplugin], 'asdf', request, Mock())
         store.call([testplugin], 'asdf', request, Mock())
-        self.assertTrue(testplugin.asdf.called)  # pylint: disable=E1101
+        self.assertTrue(getattr(testplugin, 'asdf').called)  # pylint: disable=E1101
 
 
 
 
-class SelfIPTest(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_PluginStore_init(self):
+class PluginIPSelfInfo(SearxTestCase):  # pylint: disable=missing-class-docstring
+    def setUp(self):
         plugin = plugins.load_and_initialize_plugin('searx.plugins.self_info', False, (None, {}))
         plugin = plugins.load_and_initialize_plugin('searx.plugins.self_info', False, (None, {}))
-        store = plugins.PluginStore()
-        store.register(plugin)
+        self.store = plugins.PluginStore()
+        self.store.register(plugin)
         cfg = limiter.get_cfg()
         cfg = limiter.get_cfg()
         botdetection.init(cfg, None)
         botdetection.init(cfg, None)
 
 
-        self.assertTrue(len(store.plugins) == 1)
+    def test_plugin_store_init(self):
+        self.assertEqual(1, len(self.store.plugins))
 
 
-        # IP test
+    def test_ip_in_answer(self):
         request = Mock()
         request = Mock()
         request.remote_addr = '127.0.0.1'
         request.remote_addr = '127.0.0.1'
         request.headers = {'X-Forwarded-For': '1.2.3.4, 127.0.0.1', 'X-Real-IP': '127.0.0.1'}
         request.headers = {'X-Forwarded-For': '1.2.3.4, 127.0.0.1', 'X-Real-IP': '127.0.0.1'}
-        search = get_search_mock(
-            query='ip',
-            pageno=1,
-        )
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue('127.0.0.1' in search.result_container.answers["ip"]["answer"])
+        search = get_search_mock(query='ip', pageno=1)
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertIn('127.0.0.1', search.result_container.answers["ip"]["answer"])
 
 
+    def test_ip_not_in_answer(self):
+        request = Mock()
+        request.remote_addr = '127.0.0.1'
+        request.headers = {'X-Forwarded-For': '1.2.3.4, 127.0.0.1', 'X-Real-IP': '127.0.0.1'}
         search = get_search_mock(query='ip', pageno=2)
         search = get_search_mock(query='ip', pageno=2)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertFalse('ip' in search.result_container.answers)
-
-        # User agent test
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertNotIn('ip', search.result_container.answers)
+
+    @parameterized.expand(
+        [
+            'user-agent',
+            'What is my User-Agent?',
+        ]
+    )
+    def test_user_agent_in_answer(self, query: str):
         request = Mock(user_agent=Mock(string='Mock'))
         request = Mock(user_agent=Mock(string='Mock'))
-
-        search = get_search_mock(query='user-agent', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue('Mock' in search.result_container.answers["user-agent"]["answer"])
-
-        search = get_search_mock(query='user-agent', pageno=2)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertFalse('user-agent' in search.result_container.answers)
-
-        search = get_search_mock(query='user-agent', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue('Mock' in search.result_container.answers["user-agent"]["answer"])
-
-        search = get_search_mock(query='user-agent', pageno=2)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertFalse('user-agent' in search.result_container.answers)
-
-        search = get_search_mock(query='What is my User-Agent?', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue('Mock' in search.result_container.answers["user-agent"]["answer"])
-
-        search = get_search_mock(query='What is my User-Agent?', pageno=2)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertFalse('user-agent' in search.result_container.answers)
+        search = get_search_mock(query=query, pageno=1)
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertIn('Mock', search.result_container.answers["user-agent"]["answer"])
+
+    @parameterized.expand(
+        [
+            'user-agent',
+            'What is my User-Agent?',
+        ]
+    )
+    def test_user_agent_not_in_answer(self, query: str):
+        request = Mock(user_agent=Mock(string='Mock'))
+        search = get_search_mock(query=query, pageno=2)
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertNotIn('user-agent', search.result_container.answers)
 
 
 
 
-class HashPluginTest(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_PluginStore_init(self):
-        store = plugins.PluginStore()
+class PluginHashTest(SearxTestCase):  # pylint: disable=missing-class-docstring
+    def setUp(self):
+        self.store = plugins.PluginStore()
         plugin = plugins.load_and_initialize_plugin('searx.plugins.hash_plugin', False, (None, {}))
         plugin = plugins.load_and_initialize_plugin('searx.plugins.hash_plugin', False, (None, {}))
-        store.register(plugin)
-
-        self.assertTrue(len(store.plugins) == 1)
-
+        self.store.register(plugin)
+
+    def test_plugin_store_init(self):
+        self.assertEqual(1, len(self.store.plugins))
+
+    @parameterized.expand(
+        [
+            ('md5 test', 'md5 hash digest: 098f6bcd4621d373cade4e832627b4f6'),
+            ('sha1 test', 'sha1 hash digest: a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'),
+            ('sha224 test', 'sha224 hash digest: 90a3ed9e32b2aaf4c61c410eb925426119e1a9dc53d4286ade99a809'),
+            ('sha256 test', 'sha256 hash digest: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'),
+            (
+                'sha384 test',
+                'sha384 hash digest: 768412320f7b0aa5812fce428dc4706b3c'
+                'ae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf1'
+                '7a0a9',
+            ),
+            (
+                'sha512 test',
+                'sha512 hash digest: ee26b0dd4af7e749aa1a8ee3c10ae9923f6'
+                '18980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5'
+                'fa9ad8e6f57f50028a8ff',
+            ),
+        ]
+    )
+    def test_hash_digest_new(self, query: str, hash_str: str):
         request = Mock(remote_addr='127.0.0.1')
         request = Mock(remote_addr='127.0.0.1')
+        search = get_search_mock(query=query, pageno=1)
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertIn(hash_str, search.result_container.answers['hash']['answer'])
 
 
-        # MD5
-        search = get_search_mock(query='md5 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'md5 hash digest: 098f6bcd4621d373cade4e832627b4f6' in search.result_container.answers['hash']['answer']
-        )
-
+    def test_md5_bytes_no_answer(self):
+        request = Mock(remote_addr='127.0.0.1')
         search = get_search_mock(query=b'md5 test', pageno=2)
         search = get_search_mock(query=b'md5 test', pageno=2)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertFalse('hash' in search.result_container.answers)
-
-        # SHA1
-        search = get_search_mock(query='sha1 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'sha1 hash digest: a94a8fe5ccb19ba61c4c0873d391e9879'
-            '82fbbd3' in search.result_container.answers['hash']['answer']
-        )
-
-        # SHA224
-        search = get_search_mock(query='sha224 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'sha224 hash digest: 90a3ed9e32b2aaf4c61c410eb9254261'
-            '19e1a9dc53d4286ade99a809' in search.result_container.answers['hash']['answer']
-        )
-
-        # SHA256
-        search = get_search_mock(query='sha256 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'sha256 hash digest: 9f86d081884c7d659a2feaa0c55ad015a'
-            '3bf4f1b2b0b822cd15d6c15b0f00a08' in search.result_container.answers['hash']['answer']
-        )
-
-        # SHA384
-        search = get_search_mock(query='sha384 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'sha384 hash digest: 768412320f7b0aa5812fce428dc4706b3c'
-            'ae50e02a64caa16a782249bfe8efc4b7ef1ccb126255d196047dfedf1'
-            '7a0a9' in search.result_container.answers['hash']['answer']
-        )
-
-        # SHA512
-        search = get_search_mock(query='sha512 test', pageno=1)
-        store.call(store.plugins, 'post_search', request, search)
-        self.assertTrue(
-            'sha512 hash digest: ee26b0dd4af7e749aa1a8ee3c10ae9923f6'
-            '18980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5'
-            'fa9ad8e6f57f50028a8ff' in search.result_container.answers['hash']['answer']
-        )
+        self.store.call(self.store.plugins, 'post_search', request, search)
+        self.assertNotIn('hash', search.result_container.answers)

+ 12 - 11
tests/unit/test_preferences.py

@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # pylint: disable=missing-module-docstring, invalid-name
 # pylint: disable=missing-module-docstring, invalid-name
 
 
+from tests import SearxTestCase
 from searx.locales import locales_initialize
 from searx.locales import locales_initialize
 from searx.preferences import (
 from searx.preferences import (
     EnumStringSetting,
     EnumStringSetting,
@@ -10,12 +11,12 @@ from searx.preferences import (
     PluginsSetting,
     PluginsSetting,
     ValidationException,
     ValidationException,
 )
 )
-from tests import SearxTestCase
+from searx.plugins import Plugin
 
 
 locales_initialize()
 locales_initialize()
 
 
 
 
-class PluginStub:  # pylint: disable=missing-class-docstring, too-few-public-methods
+class PluginStub(Plugin):  # pylint: disable=missing-class-docstring, too-few-public-methods
     def __init__(self, plugin_id, default_on):
     def __init__(self, plugin_id, default_on):
         self.id = plugin_id
         self.id = plugin_id
         self.default_on = default_on
         self.default_on = default_on
@@ -47,22 +48,22 @@ class TestSettings(SearxTestCase):  # pylint: disable=missing-class-docstring
 
 
     def test_enum_setting_invalid_default_value(self):
     def test_enum_setting_invalid_default_value(self):
         with self.assertRaises(ValidationException):
         with self.assertRaises(ValidationException):
-            EnumStringSetting(3, choices=[0, 1, 2])
+            EnumStringSetting('3', choices=['0', '1', '2'])
 
 
     def test_enum_setting_invalid_choice(self):
     def test_enum_setting_invalid_choice(self):
-        setting = EnumStringSetting(0, choices=[0, 1, 2])
+        setting = EnumStringSetting('0', choices=['0', '1', '2'])
         with self.assertRaises(ValidationException):
         with self.assertRaises(ValidationException):
-            setting.parse(3)
+            setting.parse('3')
 
 
     def test_enum_setting_valid_default(self):
     def test_enum_setting_valid_default(self):
-        setting = EnumStringSetting(3, choices=[1, 2, 3])
-        self.assertEqual(setting.get_value(), 3)
+        setting = EnumStringSetting('3', choices=['1', '2', '3'])
+        self.assertEqual(setting.get_value(), '3')
 
 
     def test_enum_setting_valid_choice(self):
     def test_enum_setting_valid_choice(self):
-        setting = EnumStringSetting(3, choices=[1, 2, 3])
-        self.assertEqual(setting.get_value(), 3)
-        setting.parse(2)
-        self.assertEqual(setting.get_value(), 2)
+        setting = EnumStringSetting('3', choices=['1', '2', '3'])
+        self.assertEqual(setting.get_value(), '3')
+        setting.parse('2')
+        self.assertEqual(setting.get_value(), '2')
 
 
     # multiple choice settings
     # multiple choice settings
 
 

+ 40 - 57
tests/unit/test_query.py

@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # SPDX-License-Identifier: AGPL-3.0-or-later
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 
 
+from parameterized.parameterized import parameterized
 from searx.engines import load_engines
 from searx.engines import load_engines
 from searx.query import RawTextQuery
 from searx.query import RawTextQuery
 from tests import SearxTestCase
 from tests import SearxTestCase
@@ -129,49 +130,32 @@ class TestLanguageParser(SearxTestCase):  # pylint:disable=missing-class-docstri
         query = RawTextQuery(query_text, [])
         query = RawTextQuery(query_text, [])
         self.assertEqual(query.autocomplete_list, [":en", ":en_us", ":english", ":united_kingdom"])
         self.assertEqual(query.autocomplete_list, [":en", ":en_us", ":english", ":united_kingdom"])
 
 
-    def test_autocomplete(self):
-        query = RawTextQuery(':englis', [])
-        self.assertEqual(query.autocomplete_list, [":english"])
-
-        query = RawTextQuery(':deutschla', [])
-        self.assertEqual(query.autocomplete_list, [":deutschland"])
-
-        query = RawTextQuery(':new_zea', [])
-        self.assertEqual(query.autocomplete_list, [":new_zealand"])
-
-        query = RawTextQuery(':hu-H', [])
-        self.assertEqual(query.autocomplete_list, [":hu-hu"])
-
-        query = RawTextQuery(':zh-', [])
-        self.assertEqual(query.autocomplete_list, [':zh-cn', ':zh-hk', ':zh-tw'])
+    @parameterized.expand(
+        [
+            (':englis', [":english"]),
+            (':deutschla', [":deutschland"]),
+            (':new_zea', [":new_zealand"]),
+            (':zh-', [':zh-cn', ':zh-hk', ':zh-tw']),
+        ]
+    )
+    def test_autocomplete(self, query: str, autocomplete_list: list):
+        query = RawTextQuery(query, [])
+        self.assertEqual(query.autocomplete_list, autocomplete_list)
 
 
 
 
 class TestTimeoutParser(SearxTestCase):  # pylint:disable=missing-class-docstring
 class TestTimeoutParser(SearxTestCase):  # pylint:disable=missing-class-docstring
-    def test_timeout_below100(self):
-        query_text = '<3 the query'
-        query = RawTextQuery(query_text, [])
-
-        self.assertEqual(query.getFullQuery(), query_text)
-        self.assertEqual(len(query.query_parts), 1)
-        self.assertEqual(query.timeout_limit, 3)
-        self.assertFalse(query.specific)
-
-    def test_timeout_above100(self):
-        query_text = '<350 the query'
+    @parameterized.expand(
+        [
+            ('<3 the query', 3),
+            ('<350 the query', 0.35),
+            ('<3500 the query', 3.5),
+        ]
+    )
+    def test_timeout_limit(self, query_text: str, timeout_limit: float):
         query = RawTextQuery(query_text, [])
         query = RawTextQuery(query_text, [])
-
-        self.assertEqual(query.getFullQuery(), query_text)
-        self.assertEqual(len(query.query_parts), 1)
-        self.assertEqual(query.timeout_limit, 0.35)
-        self.assertFalse(query.specific)
-
-    def test_timeout_above1000(self):
-        query_text = '<3500 the query'
-        query = RawTextQuery(query_text, [])
-
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(len(query.query_parts), 1)
         self.assertEqual(len(query.query_parts), 1)
-        self.assertEqual(query.timeout_limit, 3.5)
+        self.assertEqual(query.timeout_limit, timeout_limit)
         self.assertFalse(query.specific)
         self.assertFalse(query.specific)
 
 
     def test_timeout_invalid(self):
     def test_timeout_invalid(self):
@@ -182,7 +166,7 @@ class TestTimeoutParser(SearxTestCase):  # pylint:disable=missing-class-docstrin
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(len(query.query_parts), 0)
         self.assertEqual(len(query.query_parts), 0)
         self.assertEqual(query.getQuery(), query_text)
         self.assertEqual(query.getQuery(), query_text)
-        self.assertEqual(query.timeout_limit, None)
+        self.assertIsNone(query.timeout_limit)
         self.assertFalse(query.specific)
         self.assertFalse(query.specific)
 
 
     def test_timeout_autocomplete(self):
     def test_timeout_autocomplete(self):
@@ -193,7 +177,7 @@ class TestTimeoutParser(SearxTestCase):  # pylint:disable=missing-class-docstrin
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(len(query.query_parts), 0)
         self.assertEqual(len(query.query_parts), 0)
         self.assertEqual(query.getQuery(), query_text)
         self.assertEqual(query.getQuery(), query_text)
-        self.assertEqual(query.timeout_limit, None)
+        self.assertIsNone(query.timeout_limit)
         self.assertFalse(query.specific)
         self.assertFalse(query.specific)
         self.assertEqual(query.autocomplete_list, ['<3', '<850'])
         self.assertEqual(query.autocomplete_list, ['<3', '<850'])
 
 
@@ -212,7 +196,7 @@ class TestExternalBangParser(SearxTestCase):  # pylint:disable=missing-class-doc
         query = RawTextQuery(query_text, [])
         query = RawTextQuery(query_text, [])
 
 
         self.assertEqual(query.getFullQuery(), query_text)
         self.assertEqual(query.getFullQuery(), query_text)
-        self.assertEqual(query.external_bang, None)
+        self.assertIsNone(query.external_bang)
         self.assertFalse(query.specific)
         self.assertFalse(query.specific)
 
 
     def test_external_bang_autocomplete(self):
     def test_external_bang_autocomplete(self):
@@ -239,23 +223,22 @@ class TestBang(SearxTestCase):  # pylint:disable=missing-class-docstring
     def tearDown(self):
     def tearDown(self):
         load_engines([])
         load_engines([])
 
 
-    def test_bang(self):
-
-        for bang in TestBang.SPECIFIC_BANGS:
-            with self.subTest(msg="Check bang", bang=bang):
-                query_text = TestBang.THE_QUERY + ' ' + bang
-                query = RawTextQuery(query_text, [])
-
-                self.assertEqual(query.getFullQuery(), bang + ' ' + TestBang.THE_QUERY)
-                self.assertEqual(query.query_parts, [bang])
-                self.assertEqual(query.user_query_parts, TestBang.THE_QUERY.split(' '))
-
-    def test_specific(self):
-        for bang in TestBang.SPECIFIC_BANGS:
-            with self.subTest(msg="Check bang is specific", bang=bang):
-                query_text = TestBang.THE_QUERY + ' ' + bang
-                query = RawTextQuery(query_text, [])
-                self.assertTrue(query.specific)
+    @parameterized.expand(SPECIFIC_BANGS)
+    def test_bang(self, bang: str):
+        with self.subTest(msg="Check bang", bang=bang):
+            query_text = TestBang.THE_QUERY + ' ' + bang
+            query = RawTextQuery(query_text, [])
+
+            self.assertEqual(query.getFullQuery(), bang + ' ' + TestBang.THE_QUERY)
+            self.assertEqual(query.query_parts, [bang])
+            self.assertEqual(query.user_query_parts, TestBang.THE_QUERY.split(' '))
+
+    @parameterized.expand(SPECIFIC_BANGS)
+    def test_specific(self, bang: str):
+        with self.subTest(msg="Check bang is specific", bang=bang):
+            query_text = TestBang.THE_QUERY + ' ' + bang
+            query = RawTextQuery(query_text, [])
+            self.assertTrue(query.specific)
 
 
     def test_bang_not_found(self):
     def test_bang_not_found(self):
         query = RawTextQuery('the query !bang_not_found', [])
         query = RawTextQuery('the query !bang_not_found', [])

+ 4 - 3
tests/unit/test_search.py

@@ -110,7 +110,7 @@ class SearchTestCase(SearxTestCase):  # pylint: disable=missing-class-docstring
             search.search()
             search.search()
         self.assertEqual(search.actual_timeout, 10.0)
         self.assertEqual(search.actual_timeout, 10.0)
 
 
-    def test_external_bang(self):
+    def test_external_bang_valid(self):
         search_query = SearchQuery(
         search_query = SearchQuery(
             'yes yes',
             'yes yes',
             [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
             [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
@@ -124,8 +124,9 @@ class SearchTestCase(SearxTestCase):  # pylint: disable=missing-class-docstring
         search = searx.search.Search(search_query)
         search = searx.search.Search(search_query)
         results = search.search()
         results = search.search()
         # For checking if the user redirected with the youtube external bang
         # For checking if the user redirected with the youtube external bang
-        self.assertTrue(results.redirect_url is not None)
+        self.assertIsNotNone(results.redirect_url)
 
 
+    def test_external_bang_none(self):
         search_query = SearchQuery(
         search_query = SearchQuery(
             'youtube never gonna give you up',
             'youtube never gonna give you up',
             [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
             [EngineRef(PUBLIC_ENGINE_NAME, 'general')],
@@ -140,4 +141,4 @@ class SearchTestCase(SearxTestCase):  # pylint: disable=missing-class-docstring
         with self.app.test_request_context('/search'):
         with self.app.test_request_context('/search'):
             results = search.search()
             results = search.search()
         # This should not redirect
         # This should not redirect
-        self.assertTrue(results.redirect_url is None)
+        self.assertIsNone(results.redirect_url)

+ 16 - 11
tests/unit/test_settings_loader.py

@@ -6,6 +6,8 @@ from pathlib import Path
 import os
 import os
 from unittest.mock import patch
 from unittest.mock import patch
 
 
+from parameterized import parameterized
+
 from searx.exceptions import SearxSettingsException
 from searx.exceptions import SearxSettingsException
 from searx import settings_loader
 from searx import settings_loader
 from tests import SearxTestCase
 from tests import SearxTestCase
@@ -31,13 +33,13 @@ class TestDefaultSettings(SearxTestCase):  # pylint: disable=missing-class-docst
         settings, msg = settings_loader.load_settings(load_user_settings=False)
         settings, msg = settings_loader.load_settings(load_user_settings=False)
         self.assertTrue(msg.startswith('load the default settings from'))
         self.assertTrue(msg.startswith('load the default settings from'))
         self.assertFalse(settings['general']['debug'])
         self.assertFalse(settings['general']['debug'])
-        self.assertTrue(isinstance(settings['general']['instance_name'], str))
+        self.assertIsInstance(settings['general']['instance_name'], str)
         self.assertEqual(settings['server']['secret_key'], "ultrasecretkey")
         self.assertEqual(settings['server']['secret_key'], "ultrasecretkey")
-        self.assertTrue(isinstance(settings['server']['port'], int))
-        self.assertTrue(isinstance(settings['server']['bind_address'], str))
-        self.assertTrue(isinstance(settings['engines'], list))
-        self.assertTrue(isinstance(settings['doi_resolvers'], dict))
-        self.assertTrue(isinstance(settings['default_doi_resolver'], str))
+        self.assertIsInstance(settings['server']['port'], int)
+        self.assertIsInstance(settings['server']['bind_address'], str)
+        self.assertIsInstance(settings['engines'], list)
+        self.assertIsInstance(settings['doi_resolvers'], dict)
+        self.assertIsInstance(settings['default_doi_resolver'], str)
 
 
 
 
 class TestUserSettings(SearxTestCase):  # pylint: disable=missing-class-docstring
 class TestUserSettings(SearxTestCase):  # pylint: disable=missing-class-docstring
@@ -50,11 +52,14 @@ class TestUserSettings(SearxTestCase):  # pylint: disable=missing-class-docstrin
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             self.assertFalse(settings_loader.is_use_default_settings({'use_default_settings': 0}))
             self.assertFalse(settings_loader.is_use_default_settings({'use_default_settings': 0}))
 
 
-    def test_user_settings_not_found(self):
-        with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': _settings("not_exists.yml")}):
-            with self.assertRaises(EnvironmentError):
-                _s, _m = settings_loader.load_settings()
-        with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': "/folder/not/exists"}):
+    @parameterized.expand(
+        [
+            _settings("not_exists.yml"),
+            "/folder/not/exists",
+        ]
+    )
+    def test_user_settings_not_found(self, path: str):
+        with patch.dict(os.environ, {'SEARXNG_SETTINGS_PATH': path}):
             with self.assertRaises(EnvironmentError):
             with self.assertRaises(EnvironmentError):
                 _s, _m = settings_loader.load_settings()
                 _s, _m = settings_loader.load_settings()
 
 

+ 18 - 9
tests/unit/test_utils.py

@@ -3,6 +3,7 @@
 
 
 import lxml.etree
 import lxml.etree
 from lxml import html
 from lxml import html
+from parameterized.parameterized import parameterized
 
 
 from searx.exceptions import SearxXPathSyntaxException, SearxEngineXPathException
 from searx.exceptions import SearxXPathSyntaxException, SearxEngineXPathException
 from searx import utils
 from searx import utils
@@ -66,9 +67,15 @@ class TestUtils(SearxTestCase):  # pylint: disable=missing-class-docstring
         self.assertEqual(utils.extract_text(dom.xpath('boolean(//span)')), 'True')
         self.assertEqual(utils.extract_text(dom.xpath('boolean(//span)')), 'True')
         self.assertEqual(utils.extract_text(dom.xpath('//img/@src')), 'test.jpg')
         self.assertEqual(utils.extract_text(dom.xpath('//img/@src')), 'test.jpg')
         self.assertEqual(utils.extract_text(dom.xpath('//unexistingtag')), '')
         self.assertEqual(utils.extract_text(dom.xpath('//unexistingtag')), '')
+
+    def test_extract_text_allow_none(self):
         self.assertEqual(utils.extract_text(None, allow_none=True), None)
         self.assertEqual(utils.extract_text(None, allow_none=True), None)
+
+    def test_extract_text_error_none(self):
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             utils.extract_text(None)
             utils.extract_text(None)
+
+    def test_extract_text_error_empty(self):
         with self.assertRaises(ValueError):
         with self.assertRaises(ValueError):
             utils.extract_text({})
             utils.extract_text({})
 
 
@@ -103,14 +110,16 @@ class TestHTMLTextExtractor(SearxTestCase):  # pylint: disable=missing-class-doc
     def test__init__(self):
     def test__init__(self):
         self.assertEqual(self.html_text_extractor.result, [])
         self.assertEqual(self.html_text_extractor.result, [])
 
 
-    def test_handle_charref(self):
-        self.html_text_extractor.handle_charref('xF')
-        self.assertIn('\x0f', self.html_text_extractor.result)
-        self.html_text_extractor.handle_charref('XF')
-        self.assertIn('\x0f', self.html_text_extractor.result)
-
-        self.html_text_extractor.handle_charref('97')
-        self.assertIn('a', self.html_text_extractor.result)
+    @parameterized.expand(
+        [
+            ('xF', '\x0f'),
+            ('XF', '\x0f'),
+            ('97', 'a'),
+        ]
+    )
+    def test_handle_charref(self, charref: str, expected: str):
+        self.html_text_extractor.handle_charref(charref)
+        self.assertIn(expected, self.html_text_extractor.result)
 
 
     def test_handle_entityref(self):
     def test_handle_entityref(self):
         entity = 'test'
         entity = 'test'
@@ -191,7 +200,7 @@ class TestXPathUtils(SearxTestCase):  # pylint: disable=missing-class-docstring
         self.assertEqual(utils.eval_xpath_getindex(doc, '//i/text()', 1, default='something'), 'something')
         self.assertEqual(utils.eval_xpath_getindex(doc, '//i/text()', 1, default='something'), 'something')
 
 
         # default is None
         # default is None
-        self.assertEqual(utils.eval_xpath_getindex(doc, '//i/text()', 1, default=None), None)
+        self.assertIsNone(utils.eval_xpath_getindex(doc, '//i/text()', 1, default=None))
 
 
         # index not found
         # index not found
         with self.assertRaises(SearxEngineXPathException) as context:
         with self.assertRaises(SearxEngineXPathException) as context:

+ 49 - 36
tests/unit/test_webutils.py

@@ -2,52 +2,59 @@
 # pylint: disable=missing-module-docstring
 # pylint: disable=missing-module-docstring
 
 
 import mock
 import mock
+from parameterized.parameterized import parameterized
 from searx import webutils
 from searx import webutils
 from tests import SearxTestCase
 from tests import SearxTestCase
 
 
 
 
 class TestWebUtils(SearxTestCase):  # pylint: disable=missing-class-docstring
 class TestWebUtils(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_prettify_url(self):
-        data = (
+
+    @parameterized.expand(
+        [
             ('https://searx.me/', 'https://searx.me/'),
             ('https://searx.me/', 'https://searx.me/'),
             ('https://searx.me/ű', 'https://searx.me/ű'),
             ('https://searx.me/ű', 'https://searx.me/ű'),
             ('https://searx.me/' + (100 * 'a'), 'https://searx.me/[...]aaaaaaaaaaaaaaaaa'),
             ('https://searx.me/' + (100 * 'a'), 'https://searx.me/[...]aaaaaaaaaaaaaaaaa'),
             ('https://searx.me/' + (100 * 'ű'), 'https://searx.me/[...]űűűűűűűűűűűűűűűűű'),
             ('https://searx.me/' + (100 * 'ű'), 'https://searx.me/[...]űűűűűűűűűűűűűűűűű'),
-        )
-
-        for test_url, expected in data:
-            self.assertEqual(webutils.prettify_url(test_url, max_length=32), expected)
-
-    def test_highlight_content(self):
-        self.assertEqual(webutils.highlight_content(0, None), None)
-        self.assertEqual(webutils.highlight_content(None, None), None)
-        self.assertEqual(webutils.highlight_content('', None), None)
-        self.assertEqual(webutils.highlight_content(False, None), None)
-
-        contents = ['<html></html>not<']
-        for content in contents:
-            self.assertEqual(webutils.highlight_content(content, None), content)
-
-        content = 'a'
-        query = 'test'
-        self.assertEqual(webutils.highlight_content(content, query), 'a')
-        query = 'a test'
-        self.assertEqual(webutils.highlight_content(content, query), '<span class="highlight">a</span>')
-
-        # pylint: disable=line-too-long
-        data = (
+        ]
+    )
+    def test_prettify_url(self, test_url: str, expected: str):
+        self.assertEqual(webutils.prettify_url(test_url, max_length=32), expected)
+
+    @parameterized.expand(
+        [
+            (0, None, None),
+            (None, None, None),
+            ('', None, None),
+            (False, None, None),
+        ]
+    )
+    def test_highlight_content_none(self, content, query, expected):
+        self.assertEqual(webutils.highlight_content(content, query), expected)
+
+    def test_highlight_content_same(self):
+        content = '<html></html>not<'
+        self.assertEqual(webutils.highlight_content(content, None), content)
+
+    @parameterized.expand(
+        [
+            ('test', 'a', 'a'),
+            ('a test', 'a', '<span class="highlight">a</span>'),
             ('" test "', 'a test string', 'a <span class="highlight">test</span> string'),
             ('" test "', 'a test string', 'a <span class="highlight">test</span> string'),
             ('"a"', 'this is a test string', 'this is <span class="highlight">a</span> test string'),
             ('"a"', 'this is a test string', 'this is <span class="highlight">a</span> test string'),
             (
             (
                 'a test',
                 'a test',
                 'this is a test string that matches entire query',
                 'this is a test string that matches entire query',
-                'this is <span class="highlight">a</span> <span class="highlight">test</span> string that matches entire query',
+                'this is <span class="highlight">a</span>'
+                ' <span class="highlight">test</span>'
+                ' string that matches entire query',
             ),
             ),
             (
             (
                 'this a test',
                 'this a test',
                 'this is a string to test.',
                 'this is a string to test.',
                 (
                 (
-                    '<span class="highlight">this</span> is <span class="highlight">a</span> string to <span class="highlight">test</span>.'
+                    '<span class="highlight">this</span>'
+                    ' is <span class="highlight">a</span>'
+                    ' string to <span class="highlight">test</span>.'
                 ),
                 ),
             ),
             ),
             (
             (
@@ -65,9 +72,10 @@ class TestWebUtils(SearxTestCase):  # pylint: disable=missing-class-docstring
                 'a string with class.',
                 'a string with class.',
                 '<span class="highlight">a</span> string with <span class="highlight">class</span>.',
                 '<span class="highlight">a</span> string with <span class="highlight">class</span>.',
             ),
             ),
-        )
-        for query, content, expected in data:
-            self.assertEqual(webutils.highlight_content(content, query), expected)
+        ]
+    )
+    def test_highlight_content_equal(self, query: str, content: str, expected: str):
+        self.assertEqual(webutils.highlight_content(content, query), expected)
 
 
 
 
 class TestUnicodeWriter(SearxTestCase):  # pylint: disable=missing-class-docstring
 class TestUnicodeWriter(SearxTestCase):  # pylint: disable=missing-class-docstring
@@ -76,7 +84,7 @@ class TestUnicodeWriter(SearxTestCase):  # pylint: disable=missing-class-docstri
 
 
     def test_write_row(self):
     def test_write_row(self):
         row = [1, 2, 3]
         row = [1, 2, 3]
-        self.assertEqual(self.unicode_writer.writerow(row), None)
+        self.assertIsNone(self.unicode_writer.writerow(row))
 
 
     def test_write_rows(self):
     def test_write_rows(self):
         self.unicode_writer.writerow = mock.MagicMock()
         self.unicode_writer.writerow = mock.MagicMock()
@@ -86,13 +94,18 @@ class TestUnicodeWriter(SearxTestCase):  # pylint: disable=missing-class-docstri
 
 
 
 
 class TestNewHmac(SearxTestCase):  # pylint: disable=missing-class-docstring
 class TestNewHmac(SearxTestCase):  # pylint: disable=missing-class-docstring
-    def test_bytes(self):
+    @parameterized.expand(
+        [
+            b'secret',
+            1,
+        ]
+    )
+    def test_attribute_error(self, secret_key):
         data = b'http://example.com'
         data = b'http://example.com'
         with self.assertRaises(AttributeError):
         with self.assertRaises(AttributeError):
-            webutils.new_hmac(b'secret', data)
-
-        with self.assertRaises(AttributeError):
-            webutils.new_hmac(1, data)
+            webutils.new_hmac(secret_key, data)
 
 
+    def test_bytes(self):
+        data = b'http://example.com'
         res = webutils.new_hmac('secret', data)
         res = webutils.new_hmac('secret', data)
         self.assertEqual(res, '23e2baa2404012a5cc8e4a18b4aabf0dde4cb9b56f679ddc0fd6d7c24339d819')
         self.assertEqual(res, '23e2baa2404012a5cc8e4a18b4aabf0dde4cb9b56f679ddc0fd6d7c24339d819')