Lors du paramétrage des tests et des fixtures dans pytest, pytest semble évaluer avec empressement tous les paramètres et construire une structure de données de listes de tests avant de commencer à exécuter les tests.
C'est un problème dans 2 situations:
Ainsi ma question: est-il possible de dire à pytest d'évaluer les paramètres à la volée (c'est-à-dire paresseusement)?
3 Réponses :
Quant à votre 2 question - proposée en lien de commentaire vers manual semble être exactement ce qu'il faut faire. Il permet "de configurer des ressources coûteuses comme des connexions à la base de données ou des sous-processus uniquement lorsque le test réel est exécuté." Vous pouvez passer directement le générateur à paramétrer
comme ceci:
@pytest.mark.parametrize("one", list_1) def test_maybe_convert_objects(self, one): for two in list_2: ...
Mais pytest va list ()
de votre générateur -> Les problèmes de RAM persistent ici aussi.
J'ai également trouvé des github problèmes que d'éclairer davantage sur pourquoi pytest ne gère pas le générateur paresseusement. Et cela semble être un problème de conception. Donc "il n'est pas possible de gérer correctement la paramétrisation ayant un générateur comme valeur" à cause de
"pytest devrait collecter tous ces tests avec toutes les métadonnées ... la collecte se produit toujours avant l'exécution du test ".
Il y a aussi des références à l ' hypothèse
ou aux tests de base de rendement de nose
dans de tels cas. Mais si vous voulez toujours vous en tenir à pytest
, il existe quelques solutions de contournement:
@pytest.mark.parametrize("one", list_1) @pytest.mark.parametrize("two", list_2) def test_maybe_convert_objects(self, one, two): ...
# data_generate # set_up test pytest.main(['test'])
Donc ici, vous paramétrez sur index
(ce qui n'est pas si utile - il suffit d'indiquer le nombre d'exécutions pytest) doit être fait) et générer des données dans la prochaine exécution.
Vous pouvez également l'envelopper dans memory_profiler
:
@pytest.mark.parametrize('param2', [15, 'asdb', 1j]) def test_two(fix, param2): data = fix ...
Et comparer avec simple:
data_gen = get_data(N) @pytest.fixture(scope='module', params=len_of_gen_if_known) def fix(): huge_data_chunk = next(data_gen) return huge_data_chunk @pytest.mark.parametrize('other_param', ['aaa', 'bbb']) def test_one(fix, other_param): data = fix ...
Ce qui 'mange' beaucoup plus de mémoire:
Results (48.11s): 3000 passed Filename: run_test.py Line # Mem usage Increment Line Contents ================================================ 5 40.7 MiB 40.7 MiB @profile 6 def to_profile(): 7 409.3 MiB 368.6 MiB pytest.main(['test.py'])
@pytest.mark.parametrize('data', data_gen) def test_yield(data): assert data
Nous utilisons donc fixture ici au niveau de la portée du module
afin de "prérégler" nos données pour un test paramétré. Notez qu'ici, vous pouvez ajouter un autre test et il recevra également les données générées. Ajoutez-le simplement après test_two:
Results (46.53s): 3000 passed Filename: run_test.py Line # Mem usage Increment Line Contents ================================================ 5 40.6 MiB 40.6 MiB @profile 6 def to_profile(): 7 76.6 MiB 36.1 MiB pytest.main(['test.py'])
REMARQUE: si vous ne connaissez pas le nombre de données générées, vous pouvez utiliser cette astuce: définissez une valeur approximative (mieux si c'est un peu supérieur au nombre de tests générés) et 'marquer' les tests réussis s'il s'arrête avec StopIteration
qui se produira lorsque toutes les données sont déjà générées.
Une autre possibilité est d'utiliser les Usines comme accessoires . Ici, vous intégrez votre générateur dans l'appareil et essayez
le rendement de votre test jusqu'à ce qu'il ne se termine pas. Mais voici un autre inconvénient: pytest le traitera comme un test unique (avec éventuellement un tas de vérifications à l'intérieur) et échouera si l'une des données générées échoue. En d'autres termes, si vous comparez à l'approche de paramétrage, toutes les statistiques / fonctionnalités de pytest ne sont pas accessibles.
Et encore, l'un pour l'autre consiste à utiliser pytest.main ()
dans la boucle quelque chose comme ceci:
@pytest.mark.parametrize('ind', range(N)) def test_yield(ind): data = next(data_gen) assert data
import pytest def get_data(N): for i in range(N): yield list(range(N)) N = 3000 data_gen = get_data(N)
Changer en:
@pytest.mark.parametrize('data', data_gen) def test_gen(data): ...
C'est similaire aux usines mais encore plus facile à implémenter. En outre, cela réduit non seulement la RAM plusieurs fois, mais également le temps de collecte des méta-informations. Inconvénients ici - pour pytest, ce serait un test pour toutes les valeurs deux
. Et cela fonctionne bien avec des tests "simples" - si l'on a des xmark
spéciaux à l'intérieur ou quelque chose, il peut y avoir des problèmes.
J'ai également ouvert le problème correspondant. apparaissent quelques informations / modifications supplémentaires sur ce problème.
Cette solution de contournement peut vous être utile:
========================================================================= test session starts ========================================================================== platform darwin -- Python 3.7.7, pytest-5.3.5, py-1.8.1, pluggy-0.13.1 -- /usr/local/opt/python/bin/python3.7 cachedir: .pytest_cache rootdir: /Users/apizarro/tmp collected 2 items test_that.py::test_it[<lambda>0] 2020-04-14 18:34:08.700531 2020-04-15 18:34:08.700550 PASSED test_that.py::test_it[<lambda>1] 2020-04-15 18:34:09.702914 2020-04-16 18:34:09.702919 PASSED ========================================================================== 2 passed in 2.02s ===========================================================================
Exemple de sortie:
from datetime import datetime, timedelta from time import sleep import pytest @pytest.mark.parametrize( 'lazy_params', [ lambda: (datetime.now() - timedelta(days=1), datetime.now()), lambda: (datetime.now(), datetime.now() + timedelta(days=1)), ], ) def test_it(lazy_params): yesterday, today = lazy_params() print(f'\n{yesterday}\n{today}') sleep(1) assert yesterday < today
Veuillez envisager de publier un exemple de sortie sous forme de texte (au lieu d'une capture d'écran recadrée) pour des raisons d'accessibilité, à l'avenir.
EDIT: ma première réaction serait "c'est exactement ce à quoi servent les appareils paramétrés": un appareil à portée fonctionnelle est une valeur paresseuse appelée juste avant que le nœud de test ne soit exécuté, et par paramétrage l'appareil, vous pouvez prédéfinir autant de variantes (par exemple à partir d'une liste de clés de base de données) que vous le souhaitez.
from functools import partial from random import random import pytest from pytest_cases import lazy_value database = [random() for i in range(10)] def get_param(i): return database[i] def make_param_getter(i, use_partial=False): if use_partial: return partial(get_param, i) else: def _get_param(): return database[i] return _get_param many_lazy_parameters = (make_param_getter(i) for i in range(10)) @pytest.mark.parametrize('a', [lazy_value(f) for f in many_lazy_parameters]) def test_foo(a): print(a)
Cela étant dit, dans certaines (rares) situations, vous avez encore besoin de valeurs paresseuses dans un paramètre fonction, et vous ne souhaitez pas que ce soit les variantes d'un appareil paramétré. Pour ces situations, il existe désormais une solution également dans pytest-cases
, avec lazy_value
. Avec lui, vous pouvez utiliser des fonctions dans les valeurs des paramètres, et ces fonctions ne sont appelées que lorsque le test en cours est exécuté.
Voici un exemple montrant deux styles de codage (changez l'argument booléen use_partial sur True en activer l'autre alternative)
from pytest_cases import fixture_plus @fixture_plus def db(): return <todo> @fixture_plus @pytest.mark.parametrize("key", [<list_of keys>]) def sample(db, key): return db.get(key) def test_foo(sample): return sample
Notez que lazy_value
a également un argument id
si vous souhaitez personnaliser les identifiants de test . La valeur par défaut consiste à utiliser la fonction __name__
, et la prise en charge des fonctions partielles est en route .
Vous pouvez paramétrer les appareils de la même manière, mais n'oubliez pas que vous devez utiliser @fixture_plus
au lieu de @ pytest.fixture
. Consultez la documentation pytest-cases
pour détails.
Je suis l'auteur de pytest-cases
au fait;)
Différer la configuration des ressources paramétrées