Skip to content

Commit

Permalink
[refactor] unit tests to utilize paramaterized and break down monolit…
Browse files Browse the repository at this point in the history
…hic 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.
  • Loading branch information
glanham-jr authored and return42 committed Oct 3, 2024
1 parent 042c719 commit 44a0619
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 357 deletions.
3 changes: 2 additions & 1 deletion searx/query.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=invalid-name, missing-module-docstring, missing-class-docstring

from __future__ import annotations
from abc import abstractmethod, ABC
import re

Expand Down Expand Up @@ -258,7 +259,7 @@ class RawTextQuery:
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)
# input parameters
self.query = query
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/test_answerers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
# pylint: disable=missing-module-docstring

from mock import Mock
from parameterized import parameterized

from searx.answerers import answerers
from tests import SearxTestCase


class AnswererTest(SearxTestCase): # pylint: disable=missing-class-docstring
def test_unicode_input(self):
@parameterized.expand(answerers)
def test_unicode_input(self, answerer):
query = Mock()
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)
50 changes: 22 additions & 28 deletions tests/unit/test_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,36 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring

from parameterized import parameterized
from tests import SearxTestCase
import searx.exceptions
from searx import get_setting


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(
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)

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)
6 changes: 3 additions & 3 deletions tests/unit/test_external_bangs.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_found_optimized(self):

def test_partial(self):
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'])

def test_partial2(self):
Expand All @@ -100,7 +100,7 @@ def test_partial2(self):

def test_error(self):
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, [])

def test_actual_data(self):
Expand All @@ -112,7 +112,7 @@ def test_actual_data(self):
class TestExternalBangJson(SearxTestCase): # pylint:disable=missing-class-docstring
def test_no_external_bang_query(self):
result = get_bang_url(SearchQuery('test', engineref_list=[EngineRef('wikipedia', 'general')]))
self.assertEqual(result, None)
self.assertIsNone(result)

def test_get_bang_url(self):
url = get_bang_url(SearchQuery('test', engineref_list=[], external_bang='example'), external_bangs_db=TEST_DB)
Expand Down
174 changes: 91 additions & 83 deletions tests/unit/test_locales.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# pylint: disable=missing-module-docstring
"""Test some code from module :py:obj:`searx.locales`"""

from __future__ import annotations
from parameterized import parameterized
from searx import locales
from searx.sxng_locales import sxng_locales
from tests import SearxTestCase
Expand All @@ -13,98 +15,104 @@ class TestLocales(SearxTestCase):
- :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

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

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
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')

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-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-CO', ['es-AR', 'es-ES']), 'es-ES')
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)
Loading

0 comments on commit 44a0619

Please sign in to comment.