Mocking en Python

Mocking en Python

Chilango Django #14

A04788bd45feba612272eaef0f4a01ee?s=128

Henoc Díaz

June 28, 2018
Tweet

Transcript

  1. Mocking en Python @henocdz 1

  2. Hola! @henocdz 2

  3. Henoc (/ˈiːnək/) Lead Developer @ Apli @henocdz 3

  4. ! @henocdz 4

  5. Development is about making things, while mocking is about faking

    things. — Mike Lin @henocdz 5
  6. ¿Qué es Mocking? Simular o imitar el comportamiento de objetos

    @henocdz 6
  7. Beneficios • Velocidad • Evitar side-effects "reales" • Crear escenarios

    difíciles de reproducir @henocdz 7
  8. ¿Cómo creamos Mocks en Python? # Python < 3.3 import

    mock # Python >= 3.3 import unittest.mock @henocdz 8
  9. Instalación • Python < 3.3 pip install mock • Python

    >= 3.3 Integrado por default @henocdz 9
  10. Principales objetos en unittest.mock • Mock • MagicMock • Patchers

    @henocdz 10
  11. unittest.mock.Mock Crea atributos y métodos conforme se acceden y almacena

    información sobre su uso. from unittest.mock import Mock mock = Mock() mock.return_value = 'Imaginémonos cosas chingonas!' print(mock()) >> Imaginémonos cosas chingonas! @henocdz 11
  12. return_value from unittest.mock import Mock mock = Mock() mock.return_value =

    'Test!' mock() >> Test! @henocdz 12
  13. side_effect Excepción, función o iterador que se ejecutará al momento

    de llamar al mock instance. mock.side_effect = lambda x: x mock.side_effect = ['h', 'o', 'l', 'a'] mock.side_effect = RuntimeError('x_x') @henocdz 13
  14. Args destacados @henocdz 14

  15. spec from unittest.mock import Mock spec_mock = Mock(spec=['foo']) spec_mock.foo =

    'Hola!' print(spec_mock.foo) >> Hola! @henocdz 15
  16. from unittest.mock import Mock spec_mock = Mock(spec=['foo']) print(spec_mock.bar) AttributeError: Mock

    object has no attribute 'bar' spec_mock.bar = 'Yeah! :)' print(spec_mock.bar) >> Yeah! :) @henocdz 16
  17. spec_set from unittest.mock import Mock from django.db.models import Model spec_set_mock

    = Mock(spec_set=Model) print(spec_set_mock.bar) AttributeError: Mock object has no attribute 'bar' print(spec_set_mock.pk) <Mock name='mock.pk' id='4426207184'> @henocdz 17
  18. **kwargs from unittest.mock import Mock spec_mock = Mock(spec=['foo'], mexico='chingón') spec_mock.mexico

    >> 'chingón' @henocdz 18
  19. asserts @henocdz 19

  20. asserts • assert_called • assert_called_once • assert_called_with • assert_called_once_with •

    assert_any_call • assert_has_calls • assert_not_called @henocdz 20
  21. unittest.mock.MagicMock Lo mismo que Mock pero incluye magic methods como:

    • __lt__: NotImplemented • __gt__: NotImplemented • __int__: 1 • __contains__: False • __len__: 0 @henocdz 21
  22. • __iter__: iter([]) • __float__: 1.0 • __bool__: True •

    __str__: default str for the mock e.g from unittest.mock import MagicMock mock = MagicMock() len(mock) >> 0 @henocdz 22
  23. Patchers @henocdz 23

  24. @patch Nos permite parchar un objecto en particular durante la

    ejecución de la función o bloque with from unittest.mock import patch with patch('path.to.module.Class.method', autospec=True) as mocked_method: pass @henocdz 24
  25. # En mailer module class MailSvc(object): def __init__(self): self.client =

    MailGun() if settings.PRODUCTION else SES() def send_mail(cls, sender, recipients, *etc, **etcetc): response = self.client.send_mail(sender, recipients, *etc, **etcetc) if response.succeed: return True, recipients, response.message return False, [], response.message @henocdz 25
  26. # En applicant_service module from mailer import MailSvc mail_service =

    MailSvc() class ApplicantSvc(object): def invite_applicant(self, applicant_pk): applicant = Applicant.objects.get(pk=applicant_pk) applicant.invited = True applicant.save() mail_service.send_mail(subject='...', recipients=[applicant.email]...) @henocdz 26
  27. from unittest.mock import patch @patch('applicant_service.MailSvc.send_mail', autospec=True) def test_invite_applicant(self, mocked_send_mail): recipients

    = ['self@henocdz.com'] mocked_send_mail.return_value = True, recipients, 'Email sent' applicant = applicant.objects.create(*dummy, **data) ApplicantSvc.invite_applicant(applicant_pk=applicant.pk) applicant.refresh_from_db() self.assertTrue(applicant.invited) mocked_send_mail.assert_called_once() @henocdz 27
  28. # Correcto @patch('applicant_service.MailSvc.send_mail', autospec=True) # Incorrecto @patch('mailer.MailSvc.send_mail', autospec=True) @henocdz 28

  29. @patch('some_module.Class.object') @patch('some_other_module.Class.object') def test_nested_object(self, mocked_object, mocked_other_object): pass ... @henocdz 29

  30. @patch.object # En applicant_service module class Applicant(models.Model): first_name = models.CharField(max_length=100)

    last_name = models.CharField(max_length=100) def index(self): # Update record in ElasticSearch pass def save(self, *args, **kwargs): super(Applicant, self).save(*args, **kwargs) self.index() @henocdz 30
  31. @patch.object class TestApplicantModel(TestCase): def test_index_is_called(self): with patch.object( Applicant, 'index', return_value=None

    ) as mocked_index: applicant = Applicant.objects.create( first_name='Chilango', last_name='Django' ) self.assertEqual(mocked_index.call_count, 0) applicant.last_name = 'Dev' applicant.save() self.assertEqual(mocked_index.call_count, 1) @henocdz 31
  32. calls ὸ @henocdz 32

  33. # En some_package.some_module module def send_sms(phone, message): # Some expensive

    call to external service return True def send_invites(phones): sent_to = [] for phone in phones: sent = send_sms(phone=phone, message='Invitación...') if sent: sent_to.append(phone) return True, sent_to @henocdz 33
  34. from unittest import TestCase from unittest.mock import patch, call from

    some_package.some_module import send_invites class TestSendInvites(TestCase): @patch('demo_magicmock.some_module.send_sms', autospec=True) def test_send_invites(self, mocked_send_sms): _recipients = ['5564137890', '5564137891'] status, recipients = send_invites(_recipients) self.assertTrue(status) self.assertListEqual(_recipients, recipients) calls = ( call(phone='5564137890', message='Invitación...'), call(phone='5564137891', message='Invitación...') ) mocked_send_sms.assert_has_calls(calls, any_order=True) mocked_send_sms.imaginemonos_cosas_chingonas() @henocdz 34
  35. Extras @henocdz 35

  36. httpretty # some_package.httpretty_demo module import requests def get_google(): return requests.get('https://google.com/?q=hola')

    @henocdz 36
  37. Primer opción @patch('some_package.requests.get') def test_httpretty_patch(self, mocked_get): mocked_get.return_value.status_code = 200 mocked_json_response

    = Mock() mocked_json_response.return_value = dict(foo='bar') mocked_get.return_value.json = mocked_json_response response = get_google() self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"foo": "bar"}) @henocdz 37
  38. from unittest import TestCase import httpretty from some_package.httpretty_demo import get_google

    class TestHTTPretty(TestCase): @httpretty.activate def test_httpretty(self): httpretty.register_uri( httpretty.GET, 'https://google.com', body='{"foo": "bar"}' ) response = get_google() self.assertEqual(response.status_code, 200) self.assertDictEqual(response.json(), {"foo": "bar"}) @henocdz 38
  39. Factory Boy from factory import Sequence, SubFactory from factory.django import

    DjangoModelFactory from database.factories.address import AddressFactory class ApplicantFactory(DjangoModelFactory): class Meta: model = 'applicants.Applicant' address = SubFactory(AddressFactory) first_name = Sequence(lambda n: "applicant %d" % n) last_name = Sequence(lambda n: "las_name %d" % n) email = Sequence(lambda n: "applicant_%d@apli.jobs" % n) mobile_phone = Sequence(lambda n: "556489679%d" % n) applicant = ApplicantFactory() @henocdz 39
  40. Faker from faker import Faker fake = Faker() fake.name() #

    'Lucy Cechtelar' fake.address() # "426 Jordy Lodge # Cartwrightshire, SC 88120-6700" fake.text() # Sint velit eveniet. Rerum atque repellat voluptatem quia rerum. Numquam excepturi... my_word_list = ['danish','cheesecake','sugar'] fake.sentence(ext_word_list=my_word_list) @henocdz 40
  41. Freezegun Let your Python tests travel through time from freezegun

    import freeze_time @freeze_time("2012-01-14 03:21:34", tz_offset=-4) def test(): assert datetime.datetime.utcnow() == datetime.datetime(2012, 1, 14, 3, 21, 34) assert datetime.datetime.now() == datetime.datetime(2012, 1, 13, 23, 21, 34) # datetime.date.today() uses local time assert datetime.date.today() == datetime.date(2012, 1, 13) @henocdz 41
  42. moto Mocks Boto3 import boto3 class MyModel(object): def __init__(self, name,

    value): self.name = name self.value = value def save(self): s3 = boto3.client('s3', region_name='us-east-1') s3.put_object(Bucket='mybucket', Key=self.name, Body=self.value) @henocdz 42
  43. import boto3 from moto import mock_s3 from mymodule import MyModel

    @mock_s3 def test_my_model_save(): conn = boto3.resource('s3', region_name='us-east-1') # We need to create the bucket since this is all in Moto's 'virtual' AWS account conn.create_bucket(Bucket='mybucket') model_instance = MyModel('steve', 'is awesome') model_instance.save() body = conn.Object('mybucket', 'steve').get()['Body'].read().decode("utf-8") assert body == b'is awesome' @henocdz 43
  44. Gracias ! @henocdz 44

  45. self@henocdz.com @henocdz 45