Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

Unit-testing avec mock: qu'est-ce qu'on mocke?

+3 votes

Je me suis tapé le guide bien gras sur les unit-test et j'ai commencé à les implémenter dans mon code une fois que celui-ci était fini (oui je sais, d'un autre côté mieux vaut le faire maintenant que ne pas le faire).
J'ai commencé par faire une belle doc.
Puis j'ai ajouté des doctests.
Là j'attaque les tests "moins unitaires" avec pytest et mock, et comme le dit si bien Sam:

dès que vous allez vouloir faire des tests sérieux, vous allez vous heurter à la dure réalité.
La réalité est que pour tester, il vous faut la réalité.

J'ai donc lu l'article sur les mocks, j'ai regardé mon code, puis j'ai relu l'article, j'ai commencé à taper des trucs, puis j'ai relu l'article et ainsi de suite jusqu'à me décider à poster ici.

Le fait est que je ne panne pas comment adapter le mocking à mon code (sans doute pourri).

Un exemple avec une fonction qui me permet d'ouvrir un fichier CSV, extraire des données et constituer un set de résultat (j'appelle ça un set mais c'est une liste):

def get_subject_set_from_CSV_file(filename, n_subj=None):
"""
Extract IDs from a CSV file for a given number of subjects.\n

:param filename: the name of the CSV file.
:param n_subj: (optional) the number of subjects (if None, all subjects).
:type filename: string
:type n_subj: int
:returns: subject data.
:rtype: list of strings
"""
# Init
result_set = []
subj = 0
condition = 'True' if n_subj is None else 'subj<n_subj'

# Open the CSV file
with open(filename,'rb') as CSV_file:
    CSV_reader = csv.reader(CSV_file,delimiter='\t')

    # extract a set of subjects (may be all)
    try:
        while(eval(condition)):
            # Append the current subject to the result list
            result_set.append(CSV_reader.next())
            subj += 1
    except:
        pass

# sort the subjects by type and age
result_set = sorted(result_set,key=itemgetter(6,4))

return result_set

Pour tester cette fonction j'ai écrit ça:

def test_get_subject_set_from_CSV_file():
    s_set = au.get_subject_set_from_CSV_file(TEST_FILE,NB_SUBJ)
    assert s_set ==  [['18', '72', '11232', '61158', '45.69', '89', '5', '0']]

avec TESTFILE le chemin vers un fichier existant et NBSUBJ un int.
Ca marche, mais je sais que ça ne marchera plus dès que le soft quittera ma machine. Je voudrais donc utiliser un mock mais je ne sais pas comment partir:
- est-ce que je mocke le TEST_FILE? et si oui, quelle est la sortie à laquelle m'attendre dans mon test?
- est-ce que je mocke les appels dans ma fonction, comme suggéré ici? dans ce cas je ne teste plus vraiment ma fonction mais les fonctions qu'elle appelle, et encore une fois je ne peux que vérifier qu'elles ont été appelées avec le bon paramètre (et honnêtement je ne vois pas bien l'intérêt)

demandé 21-Aou-2015 par furankun (1,434 points)
edité 24-Aou-2015 par furankun

Petite précision suite à mise en place d'un mock d'appel de fonction: l'intérêt n'est pas de tester la fonction elle-même mais la/les autres fonctions qu'elle appelle. Vérifier qu'il n'y a pas de pertes en cours de route dans la mise en forme des arguments, etc.

Remarque en passant, puisque je n'ai vu ça que dans la doc (regarder à "target") et dans aucun exemple (bordel!): si vous patchez une fonction n'oubliez pas de fournir le chemin complet de votre fonction en remontant jusqu'au nom du package! Et ce même si votre fichier de test pytest est dans ledit package.

@mock.patch('nom_package.nom_module.nom_fonction/nom_classe')

sinon votre mock ne sera jamais appelé.

2 Réponses

+4 votes
 
Meilleure réponse

Ca marche, mais je sais que ça ne marchera plus dès que le soft quittera ma machine

Rien ne t’empêche d'ajouter un ou plusieurs fichier comme fixtures pour tes tests dans ton repo / package.

Si tu tiens vraiment a mocker, ici le plus pertinent semble de le faire au niveau du open pour injecter tes donnés de test (et c'est prévu par le module mock)

répondu 21-Aou-2015 par jc (2,704 points)
sélectionné 24-Aou-2015 par furankun

+1. Ici je n'utiliserais pas un mock, mais plutôt une fixture, c'est à dire un fichier bidon qui sert que pour les tests.

Sinon, tu peux utiliser le patch de mock:

from mock import mock_open, patch

def ta_fonction(filename):
        with open(filename, 'w') as h:                                 
            print(h.read())

m = mock_open(read_data='jfdkmjfdslmqfkdl\nfjdkmlsqfjdlmk')
with patch('__main__.open', m, create=True):                         
    ta_fonction('foo.csv')

Poof:

jfdkmjfdslmqfkdl
fjdkmlsqfjdlmk

J'utilise déjà des fichiers bidon, ou pseudo-bidon*, mais j'envisageais les faire sauter... du coup je vais les garder et prier pour que mon boss ne me pourrisse pas :P
(* le genre de fichier que tu fais au début pour t'aider à comprendre le problème, puis qui devient rapidement inutile sauf pour la petite fonction que tu avais codée au début et que tu n'as pas eu le courage de supprimer de ta lib parce qu' "on sait jamais, c'était pas mal et ça pourrait servir")

+2 votes

J'ai l'impression que tu utilises pytest. Dans ce cas, tu pourrais gérer ça avec un fichier temporaire.

Par exemple:

def get_content(filename):
    with open(filename) as f:
        return f.read()


def test_tmpdir(tmpdir):
    p = tmpdir.join('truc.txt')
    p.write("content")

    assert get_content(p.strpath) == 'content'
répondu 21-Aou-2015 par bubulle (2,238 points)

Oui désolé j'avais pas signalé pytest.
La solution que tu proposes fonctionne mais ça implique de cramer des ressources pour pas grand chose :-/ (voir le premier exemple dans le second lien que j'ai donnée: "a simple delete function")

d'accord avec toi, autant rester simple quand on peut.

...