web-dev-qa-db-fra.com

Comment se moquer du système de fichiers dans Python tests unitaires?

Existe-t-il un moyen standard (sans installer de bibliothèques tierces) de faire une simulation de système de fichiers multiplateforme en Python? Si je dois aller avec une bibliothèque tierce, quelle bibliothèque est la norme?

38
DudeOnRock

Le framework de simulation standard dans Python 3.3+ est nittest.mock ; vous pouvez l'utiliser pour le système de fichiers ou autre chose.

Vous pouvez également simplement le rouler à la main en vous moquant via le patch de singe:

Un exemple trivial:

import os.path
os.path.isfile = lambda path: path == '/path/to/testfile'

Un peu plus complet (non testé):

import classtobetested                                                                                                                                                                                      
import unittest                                                                                                                                                                                             

import contextlib                                                                                                                                                                                           

@contextlib.contextmanager                                                                                                                                                                                  
def monkey_patch(module, fn_name, patch):                                                                                                                                                                   
    unpatch = getattr(module, fn_name)                                                                                                                                                                      
    setattr(module, fn_name)                                                                                                                                                                                
    try:                                                                                                                                                                                                    
        yield                                                                                                                                                                                               
    finally:                                                                                                                                                                                                
        setattr(module, fn_name, unpatch)                                                                                                                                                                   


class TestTheClassToBeTested(unittest.TestCase):                                                                                                                                                              
    def test_with_fs_mocks(self):                                                                                                                                                                           
        with monkey_patch(classtobetested.os.path,                                                                                                                                                          
                          'isfile',                                                                                                                                                                         
                          lambda path: path == '/path/to/file'):                                                                                                                                            
            self.assertTrue(classtobetested.testable())                 

Dans cet exemple, les simulations réelles sont triviales, mais vous pouvez les sauvegarder avec quelque chose qui a un état qui peut représenter des actions du système de fichiers, telles que l'enregistrement et la suppression. Oui, tout cela est un peu moche car cela implique de répliquer/simuler le système de fichiers de base dans le code.

Notez que vous ne pouvez pas faire de patch de singe python builtins. Cela étant dit ...

Pour les versions antérieures, si possible, utilisez une bibliothèque tierce, j'irais avec le génial de Michael Foord Mock , qui est maintenant unittest.mock dans la bibliothèque standard depuis 3.3+ grâce à PEP 0417 , et vous pouvez l'obtenir sur PyPI pour Python 2.5+. Et , il peut se moquer des builtins!

12
jtmoulia

pyfakefs ( page d'accueil ) fait ce que vous voulez - un faux système de fichiers; il s'agit d'un tiers, bien que ce soit Google. Voir Comment remplacer les références d'accès aux fichiers pour un module sous test pour une discussion sur l'utilisation.

Pour mocking , nittest.mock est la bibliothèque standard pour Python 3.3+ (- PEP 0417 ); pour la version antérieure, voir PyPI: mock (pour Python 2.5+) ( page d'accueil ).

La terminologie des tests et des simulations est incohérente; en utilisant la terminologie Test Double de Gerard Meszaros, vous demandez un "faux": quelque chose qui se comporte comme un système de fichiers (vous pouvez créer, ouvrir et supprimer des fichiers), mais ce n'est pas le véritable système de fichiers (dans ce cas, il est en mémoire), vous n'avez donc pas besoin d'avoir des fichiers de test ou un répertoire temporaire.

Dans la simulation classique, vous feriez à la place des appels système (en Python, des fonctions de simulation dans le module os, comme os.rm et os.listdir), mais c'est beaucoup plus compliqué.

30
Nils von Barth

pytest gagne beaucoup de traction, et il peut faire tout cela en utilisant tmpdir et monkeypatching (mocking).

Vous pouvez utiliser l'argument de la fonction tmpdir qui fournira un répertoire temporaire unique à l'appel de test, créé dans le répertoire temporaire de base (qui sont par défaut créés comme sous-répertoires du répertoire temporaire du système).

import os
def test_create_file(tmpdir):
    p = tmpdir.mkdir("sub").join("hello.txt")
    p.write("content")
    assert p.read() == "content"
    assert len(tmpdir.listdir()) == 1

L'argument de la fonction monkeypatch vous aide à définir/supprimer en toute sécurité un attribut, un élément de dictionnaire ou une variable d'environnement ou à modifier sys.path pour l'importation.

import os
def test_some_interaction(monkeypatch):
    monkeypatch.setattr(os, "getcwd", lambda: "/")

Vous pouvez également lui passer une fonction au lieu d'utiliser lambda.

import os.path
def getssh(): # pseudo application code
    return os.path.join(os.path.expanduser("~admin"), '.ssh')

def test_mytest(monkeypatch):
    def mockreturn(path):
        return '/abc'
    monkeypatch.setattr(os.path, 'expanduser', mockreturn)
    x = getssh()
    assert x == '/abc/.ssh'

# You can still use lambda when passing arguments, e.g.
# monkeypatch.setattr(os.path, 'expanduser', lambda x: '/abc')

Si votre application a beaucoup d'interaction avec le système de fichiers, il pourrait être plus facile d'utiliser quelque chose comme pyfakefs , car la moquerie deviendrait fastidieuse et répétitive.

8
Dennis

Truquer ou se moquer?

Personnellement, je trouve qu'il y a beaucoup de cas Edge dans les choses du système de fichiers (comme ouvrir le fichier avec les bonnes autorisations, chaîne vs binaire, mode lecture/écriture, etc.), et utiliser un faux système de fichiers précis peut trouver beaucoup de bogues que vous pourriez ne pas trouver en vous moquant. Dans ce cas, je vérifierais le module memoryfs de pyfilesystem (il a plusieurs implémentations concrètes de la même interface, donc vous pouvez les échanger dans votre code).

Se moquer (et sans Monkey Patch!):

Cela dit, si vous voulez vraiment vous moquer, vous pouvez le faire facilement avec la bibliothèque unittest.mock de Python:

# production code file; note the default parameter
def make_hello_world(path, open_func=open):
    with open_func(path, 'w+') as f:
        f.write('hello, world!')

# test code file
def test_make_hello_world():
    file_mock = unittest.mock.Mock(write=unittest.mock.Mock())
    open_mock = unittest.mock.Mock(return_value=file_mock)

    # When `make_hello_world()` is called
    make_hello_world('/hello/world.txt', open_func=open_mock)

    # Then expect the file was opened and written-to properly
    open_mock.assert_called_once_with('/hello/world.txt', 'w+')
    file_mock.write.assert_called_once_with('hello, world!')

L'exemple ci-dessus montre uniquement la création et l'écriture dans des fichiers via la simulation de la méthode open(), mais vous pouvez tout aussi facilement simuler n'importe quelle méthode.

7
weberc2