socialgekon.com
  • Principal
  • Procese Financiare
  • Rentabilitate Și Eficiență
  • Investitori Și Finanțare
  • Filmare
Tehnologie

Introducere în batjocura în Python

Cum să rulați teste de unitate în Python fără a vă testa răbdarea

Cel mai adesea, software-ul pe care îl scriem interacționează direct cu ceea ce am eticheta drept servicii „murdare”. În termeni simpli: servicii care sunt cruciale pentru aplicația noastră, dar ale căror interacțiuni au avut efecte secundare intenționate, dar nedorite - adică nedorite în contextul unei testări autonome.

De exemplu: poate scriem o aplicație socială și dorim să testăm noua noastră funcție „Postează pe Facebook”, dar nu dorim de fapt postează pe Facebook de fiecare dată când rulăm suita de testare.

Python unittest biblioteca include un subpachet numit unittest.mock —sau dacă îl declarați ca o dependență, pur și simplu mock — care oferă mijloace extrem de puternice și utile prin care să batjocoriți și să eliminați aceste efecte secundare nedorite.



batjocură și teste unitare în biblioteca Python unittest

Notă: mock este nou inclus în biblioteca standard începând cu Python 3.3; distribuțiile anterioare vor trebui să utilizeze biblioteca Mock descărcabilă prin PyPI .

Apeluri de sistem vs. Python Mocking

Pentru a vă oferi un alt exemplu și unul cu care vom rula pentru restul articolului, luați în considerare apeluri de sistem . Nu este dificil de văzut că aceștia sunt candidații principali pentru batjocură: fie că scrieți un script pentru a scoate o unitate CD, un server web care elimină fișierele cache vechi din /tmp sau un server socket care se leagă de un TCP port, aceste apeluri prezintă efecte secundare nedorite în contextul testelor unitare.

Ca dezvoltator, îți pasă mai mult că biblioteca ta a apelat cu succes funcția de sistem pentru scoaterea unui CD, spre deosebire de experiența tăvii CD deschise de fiecare dată când se execută un test.

Ca dezvoltator, îți pasă mai mult că biblioteca ta a apelat cu succes funcția de sistem pentru a scoate un CD (cu argumentele corecte etc.), spre deosebire de a experimenta tava CD deschisă de fiecare dată când se execută un test. (Sau mai rău, de mai multe ori, deoarece mai multe teste fac referire la codul de ejectare în timpul unei singure teste unitare!)

La fel, menținerea eficientă și performantă a testelor unitare înseamnă păstrarea a cât mai mult „cod lent” în afara testelor automate, și anume acces la sistem de fișiere și la rețea.

Pentru primul nostru exemplu, vom refactura un caz de test Python standard de la forma originală la unul folosind mock. Vom demonstra cum scrierea unui caz de testare cu simulări va face testele noastre mai inteligente, mai rapide și capabile să dezvăluie mai multe despre modul în care funcționează software-ul.

O funcție de ștergere simplă

Cu toții trebuie să ștergem fișiere din sistemul nostru de fișiere din când în când, așa că haideți să scriem o funcție în Python care va face un lucru mai ușor pentru scripturile noastre să o facă.

#!/usr/bin/env python # -*- coding: utf-8 -*- import os def rm(filename): os.remove(filename)

Evident, | | + _ | nostru metodă în acest moment nu oferă mult mai mult decât rm de bază , dar baza noastră de coduri se va îmbunătăți, permițându-ne să adăugăm mai multe funcționalități aici.

Să scriem un caz de test tradițional, adică fără batjocuri:

os.remove

Cazul nostru de testare este destul de simplu, dar de fiecare dată când este rulat, este creat un fișier temporar și apoi șters. În plus, nu avem nicio modalitate de a testa dacă #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import os.path import tempfile import unittest class RmTestCase(unittest.TestCase): tmpfilepath = os.path.join(tempfile.gettempdir(), 'tmp-testfile') def setUp(self): with open(self.tmpfilepath, 'wb') as f: f.write('Delete me!') def test_rm(self): # remove the file rm(self.tmpfilepath) # test that it was actually removed self.assertFalse(os.path.isfile(self.tmpfilepath), 'Failed to remove the file.') metoda trece corect argumentul la rm apel. Noi putem presupune că face pe baza testului de mai sus, dar mai este mult de dorit.

Refactorizare cu Python Mocks

Să refacem cazul nostru de testare folosind os.remove:

mock

Cu acești refactori, am schimbat fundamental modul în care funcționează testul. Acum, avem un din interior , un obiect pe care îl putem folosi pentru a verifica funcționalitatea altuia.

Potențiale capcane de batjocură în Python

Unul dintre primele lucruri care ar trebui să reziste este că folosim #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os') def test_rm(self, mock_os): rm('any path') # test that rm called os.remove with the right parameters mock_os.remove.assert_called_with('any path') decorator de metode pentru a batjocori un obiect situat la mock.patch și a injecta acea batjocură în metoda noastră de caz de testare. Nu ar avea mai mult sens să batjocori mymodule.os în sine, mai degrabă decât referința la acesta la os?

Ei bine, Python este oarecum un șarpe șmecher când vine vorba de importuri și gestionarea modulelor. În timpul rulării, mymodule.os modulul are propriul său mymodule care este importat în propriul său scop local în modul. Astfel, dacă vom batjocori os, nu vom vedea efectele batjocurii în os modul.

Mantra care trebuie repetată este următoarea:

Batjocorește un articol de unde este folosit, nu de unde a venit.

Dacă trebuie să batjocorești mymodule modul pentru tempfile, probabil că trebuie să aplicați falsul la myproject.app.MyElaborateClass, deoarece fiecare modul își păstrează propriile importuri.

Cu această capcană în afara drumului, să continuăm să ne batem joc.

Adăugarea validării la „rm”

myproject.app.tempfile metoda definită mai devreme este destul de simplificată. Am dori să confirmăm că există o cale și este un fișier înainte de a încerca doar să o eliminăm. Să refactorizăm rm a fi un pic mai inteligent:

rm

Grozav. Acum, să ajustăm cazul nostru de testare pentru a menține acoperirea.

#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path def rm(filename): if os.path.isfile(filename): os.remove(filename)

Paradigma noastră de testare s-a schimbat complet. Acum putem verifica și valida funcționalitatea internă a metodelor fără orice efecte secundare.

Eliminarea fișierelor ca serviciu cu Mock Patch

Până acum, am lucrat doar cu furnizarea de simulări pentru funcții, dar nu și pentru metode pe obiecte sau cazuri în care este necesară batjocura pentru trimiterea parametrilor. Să acoperim mai întâi metodele obiectelor.

Vom începe cu un refactor al #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import rm import mock import unittest class RmTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # set up the mock mock_path.isfile.return_value = False rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True rm('any path') mock_os.remove.assert_called_with('any path') metoda într-o clasă de servicii. Într-adevăr nu există o nevoie justificabilă, în sine, de a încapsula o funcție atât de simplă într-un obiect, dar cel puțin ne va ajuta să demonstrăm conceptele cheie în rm. Să refactorizăm:

mock

Veți observa că nu s-au schimbat multe în cazul nostru de testare:

#!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): '''A service for removing objects from the filesystem.''' def rm(filename): if os.path.isfile(filename): os.remove(filename)

Super, așa că știm acum că #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path') funcționează conform planului. Să creăm un alt serviciu care îl declară ca dependență:

RemovalService

Deoarece avem deja o acoperire a testului pe #!/usr/bin/env python # -*- coding: utf-8 -*- import os import os.path class RemovalService(object): '''A service for removing objects from the filesystem.''' def rm(self, filename): if os.path.isfile(filename): os.remove(filename) class UploadService(object): def __init__(self, removal_service): self.removal_service = removal_service def upload_complete(self, filename): self.removal_service.rm(filename) , nu vom valida funcționalitatea internă a RemovalService metodă în testele noastre de rm. Mai degrabă, vom testa pur și simplu (fără efecte secundare, desigur) că UploadService apeluri | | + _ | metodă, despre care știm că „doar funcționează” din cazul nostru anterior de testare.

Există două modalități de a face acest lucru:

  1. Mock out UploadService metoda în sine.
  2. Furnizați o instanță batjocorită în constructorul RemovalService.rm.

Deoarece ambele metode sunt adesea importante în testarea unității, le vom analiza pe ambele.

Opțiunea 1: Metode de instanță batjocoritoare

RemovalService.rm biblioteca are un decorator de metode speciale pentru a batjocori metodele și proprietățile instanței obiectului, UploadService decorator:

mock

Grozav! Am validat că @mock.patch.object apelează cu succes instanța noastră #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path') class UploadServiceTestCase(unittest.TestCase): @mock.patch.object(RemovalService, 'rm') def test_upload_complete(self, mock_rm): # build our dependencies removal_service = RemovalService() reference = UploadService(removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete('my uploaded file') # check that it called the rm method of any RemovalService mock_rm.assert_called_with('my uploaded file') # check that it called the rm method of _our_ removal_service removal_service.rm.assert_called_with('my uploaded file') metodă. Observați ceva interesant acolo? Mecanismul de corecție a înlocuit efectiv UploadService metoda tuturor rm cazuri în metoda noastră de testare. Asta înseamnă că putem inspecta situațiile în sine. Dacă doriți să vedeți mai multe, încercați să introduceți un punct de întrerupere în codul dvs. de batjocură pentru a avea o impresie bună despre modul în care funcționează mecanismul de corecție.

Mock Patch Pitfall: Ordinul decoratorului

Când utilizați mai mulți decoratori pe metodele de testare, ordinea este importantă și este cam confuz. Practic, atunci când mapezi decoratorii la parametrii metodei, lucrează înapoi . Luați în considerare acest exemplu:

rm

Observați cum se potrivesc parametrii noștri cu ordinea inversă a decoratorilor? Asta se datorează parțial modul în care funcționează Python . Cu mai mulți decoratori de metode, iată ordinea de execuție în pseudocod:

RemovalService

De când patch-ul la @mock.patch('mymodule.sys') @mock.patch('mymodule.os') @mock.patch('mymodule.os.path') def test_something(self, mock_os_path, mock_os, mock_sys): pass este patch-ul cel mai exterior, acesta va fi executat ultimul, făcându-l ultimul parametru din argumentele metodei de testare reale. Rețineți acest bine și utilizați un depanator atunci când rulați testele pentru a vă asigura că parametrii corecți sunt injectați în ordinea corectă.

Opțiunea 2: crearea de instanțe simulate

În loc să batjocorim metoda instanței specifice, am putea furniza doar o instanță batjocorită la patch_sys(patch_os(patch_os_path(test_something))) cu constructorul său. Prefer opțiunea 1 de mai sus, deoarece este mult mai precisă, dar există multe cazuri în care opțiunea 2 ar putea fi eficientă sau necesară. Să refacem din nou testul nostru:

sys

În acest exemplu, nici măcar nu a trebuit să reparăm nicio funcționalitate, pur și simplu creăm o specificație automată pentru UploadService , apoi injectați această instanță în #!/usr/bin/env python # -*- coding: utf-8 -*- from mymodule import RemovalService, UploadService import mock import unittest class RemovalServiceTestCase(unittest.TestCase): @mock.patch('mymodule.os.path') @mock.patch('mymodule.os') def test_rm(self, mock_os, mock_path): # instantiate our service reference = RemovalService() # set up the mock mock_path.isfile.return_value = False reference.rm('any path') # test that the remove call was NOT called. self.assertFalse(mock_os.remove.called, 'Failed to not remove the file if not present.') # make the file 'exist' mock_path.isfile.return_value = True reference.rm('any path') mock_os.remove.assert_called_with('any path') class UploadServiceTestCase(unittest.TestCase): def test_upload_complete(self, mock_rm): # build our dependencies mock_removal_service = mock.create_autospec(RemovalService) reference = UploadService(mock_removal_service) # call upload_complete, which should, in turn, call `rm`: reference.upload_complete('my uploaded file') # test that it called the rm method mock_removal_service.rm.assert_called_with('my uploaded file') pentru a valida funcționalitatea.

RemovalService metoda creează o instanță echivalentă funcțional cu clasa furnizată. Ceea ce înseamnă asta, practic vorbind, este că, atunci când instanța returnată este interacționată, va ridica excepții dacă este utilizată în moduri ilegale. Mai precis, dacă se apelează o metodă cu un număr greșit de argumente, va fi ridicată o excepție. Acest lucru este extrem de important pe măsură ce se întâmplă refactori. Pe măsură ce o bibliotecă se schimbă, testele se întrerup și este de așteptat. Fără a utiliza o specificație automată, testele noastre vor trece în continuare, chiar dacă implementarea de bază este defectă.

Capcana: The UploadService și mock.create_autospec Clase

mock.Mock biblioteca include, de asemenea, două clase importante pe care se bazează cea mai mare parte a funcționalității interne: mock.MagicMock și mock . Când li se oferă posibilitatea de a utiliza un mock.Mock exemplu, un mock.MagicMock exemplu, sau o specificație automată, preferă întotdeauna utilizarea unei specificații automate, deoarece vă ajută să vă mențineți testele sănătoase pentru modificările viitoare. Acest lucru se datorează faptului că mock.Mock și mock.MagicMock acceptați toate apelurile de metodă și atribuțiile de proprietate, indiferent de API-ul de bază. Luați în considerare următorul caz de utilizare:

mock.Mock

Putem testa acest lucru cu un mock.MagicMock exemplu ca acesta:

class Target(object): def apply(value): return value def method(target, value): return target.apply(value)

Această logică pare sănătoasă, dar să modificăm mock.Mock metoda de a lua mai mulți parametri:

class MethodTestCase(unittest.TestCase): def test_method(self): target = mock.Mock() method(target, 'value') target.apply.assert_called_with('value')

Executați din nou testul și veți descoperi că acesta trece în continuare. Acest lucru se datorează faptului că nu este construit pe baza API-ului dvs. real. Acesta este motivul pentru care ar trebui mereu utilizați Target.apply metoda și class Target(object): def apply(value, are_you_sure): if are_you_sure: return value else: return None parametru cu create_autospec și autospec decoratori.

Python Mock Exemplu: batjocorirea unui apel API Facebook

Pentru a termina, să scriem un exemplu mai real aplicabil de piton din lumea reală, unul pe care l-am menționat în introducere: postarea unui mesaj pe Facebook. Vom scrie o clasă de împachetare frumoasă și un caz de testare corespunzător.

@patch

Iată cazul nostru de testare, care verifică dacă postăm mesajul fără de fapt postarea mesajului:

@patch.object

Așa cum am văzut până acum, este într-adevăr simplu pentru a începe să scrieți teste mai inteligente cu import facebook class SimpleFacebook(object): def __init__(self, oauth_token): self.graph = facebook.GraphAPI(oauth_token) def post_message(self, message): '''Posts a message to the Facebook wall.''' self.graph.put_object('me', 'feed', message=message) în Python.

Concluzie

Python’s import facebook import simple_facebook import mock import unittest class SimpleFacebookTestCase(unittest.TestCase): @mock.patch.object(facebook.GraphAPI, 'put_object', autospec=True) def test_post_message(self, mock_put_object): sf = simple_facebook.SimpleFacebook('fake oauth token') sf.post_message('Hello World!') # verify mock_put_object.assert_called_with(message='Hello World!') biblioteca, dacă este puțin confuză pentru a lucra, este un schimbător de jocuri pentru testarea unitara . Am demonstrat cazuri de utilizare obișnuite pentru a începe să utilizați mock în testarea unității și, sperăm, acest articol vă va ajuta Dezvoltatori Python depășește obstacolele inițiale și scrie un cod excelent, testat.

Înțelegerea elementelor de bază

Ce este batjocura în testarea unității?

Batjocura simulează existența și comportamentul unui obiect real, permițând inginerilor de software să testeze codul în diferite scenarii ipotetice fără a fi nevoie să recurgă la nenumărate apeluri de sistem. Batjocura poate îmbunătăți astfel viteza și eficiența testelor unitare.

Ce este batjocura în Python?

Derularea în Python înseamnă că biblioteca unittest.mock este utilizată pentru a înlocui părți ale sistemului cu obiecte simulate, permițând testarea unității mai ușoară și mai eficientă decât altfel ar fi posibilă.

5 lucruri pe care nu le-ați făcut niciodată cu o specificație REST

Back-End

5 lucruri pe care nu le-ați făcut niciodată cu o specificație REST
Cum să creați widget-uri Magento 2 personalizate

Cum să creați widget-uri Magento 2 personalizate

Back-End

Posturi Populare
Să redesenăm Facebook: 10 exemple pentru a inspira și a vă ajuta să începeți
Să redesenăm Facebook: 10 exemple pentru a inspira și a vă ajuta să începeți
Sfârșitul muncii tradiționale - Cealaltă economie gigantică
Sfârșitul muncii tradiționale - Cealaltă economie gigantică
Grey Matter - Ce este o hartă mentală în procesul de proiectare?
Grey Matter - Ce este o hartă mentală în procesul de proiectare?
Stai la răceală: Cum să iei feedback strategic în mod strategic
Stai la răceală: Cum să iei feedback strategic în mod strategic
Prioritizarea restanțelor de produse cu mai mulți părți interesate cheie: un studiu de caz
Prioritizarea restanțelor de produse cu mai mulți părți interesate cheie: un studiu de caz
 
Un ghid cuprinzător pentru proiectarea notificărilor
Un ghid cuprinzător pentru proiectarea notificărilor
Către diagrame D3.js actualizabile
Către diagrame D3.js actualizabile
Inginer Salesforce Back-end
Inginer Salesforce Back-end
Remedierea erorii OpenSSL „Heartbleed”: un tutorial pentru administratorii Sys
Remedierea erorii OpenSSL „Heartbleed”: un tutorial pentru administratorii Sys
Sporiți-vă UX-ul cu o ierarhie vizuală clară
Sporiți-vă UX-ul cu o ierarhie vizuală clară
Categorii
Kpis Și AnalyticsFront-End WebFilmareRise Of RemoteProces De DesignTendințeEditareMod De ViataPlanificare Și PrognozăOameni Și Echipe

© 2023 | Toate Drepturile Rezervate

socialgekon.com