IX Encontro PythonRio

Ataque às Fronteiras

Isolando e Testando suas Integrações


Bernardo Fontes


Rio de Janeiro/RJ

28 de Maio de 2016

Olar!

twitter.com/bbfontes

github.com/berinhard

soundcloud.com/2bonsai

pessoas.cc

bernardoxhc@gmail.com

berinhard.github.io/talks/

## Por favor, **critiquem** essa apresentação =)
## Fronteiras - ### Mais que **se comunicar** com uma API - ### **Pontos de conexão** do nosso próprio código com **um terceiro** desconhecido ou não
## Tipos - ### Fronteiras de **Contexto** - ### Fronteiras de **Aplicação** - ### Fronteiras de **Sistema**
## Fronteiras de Contexto - ### Integração entre **nossos próprios módulos** - ### Como o módulo que gerencia um carrinho de compras fala com o nosso módulo de pagamento? - ### Código **"sob controle"**
## Fronteiras de Aplicação - ### Integração entre nosso código com **libs terceiras** - ### Utilizar a lib `unipath` para gerenciar os arquivos - ### Código **"confiável"**
## Fronteiras de Sistema - ### Integração entre nosso código com **outras tecnologias** - ### Em geral protocolo + linguagem - ### Integrações via HTTP - ### Mensageria via AMQP - ### Consultas em DB via SQL - ### O **deconhecido** gera **preocupação** - ### Em geral são **mais críticas**
## Testando as Fronteiras de Sistema
## Exemplo **Simplificado** em Django Integração de Pagamento ```python """ A view de pagamento faz uma integração com o Paypal """ class TestPaymentView(TestCase): def test_pay_order(self): data = {'credit_card': '1234'} self.client.post('/pagamento/42', data) order = Order.objects.get(id=42) self.assertTrue(order.paid) ```
## Problemas na abordagem - ### Estamos fazendo uma **requisição de verdade** - ### **Tempo dos testes** fica lento - ### **Dependência do serviço** estar de pé nos testes - ### E se o serviço gerar alguma **cobrança** (envio de email, por exemplo)?
## Como resolver? Mock - ### **Simulamos** a requisição - ### Tempo de execução **normal do Python** - ### **Independência** do serviço - ### Utilizando a lib **requests** - ### `pip install requests`
```python """ Uma possível implementação da integração """ def payment_view(order_id): # códigos pra recuperar a ordem, validar form e tudo mais data = { 'resource_id': order_id, 'credit_card': form.cleaned_data['credit_card'] } paypal_response = requests.post('http://paypal.com/api', data=data) if paypal_response.text == 'SUCCESS': order.paid = True order.save() # códigos pra seguir o fluxo http ```
## Testando com Mock ```python """ Utilizando a lib requests-mock: pip install requests-mock """ import requests_mock class TestPaymentView(TestCase): def test_pay_order(self): data = {'credit_card': '1234'} with requests_mock.Mocker() as mock: mock.post('http://paypal.com/api', text='SUCCESS') self.client.post('/pagamento/42', data) order = Order.objects.get(id=42) self.assertTrue(order.paid) ```
## Problemas na abordagem - ### Teste **focado em integração** e não em funcionamento unitário da view - ### **Burocrática** para testar outros cenários - ### **Acoplamento direto** do código com a forma de pagamento
## Como resolver? - ### Uma ou mais novas **camadas de abstrações** - ### Componentes de código com **papéis especificos e limitados** - ### Facilita o **TDD** via **testes unitários** - ### Podemos nos preocupar mais com as características da **integração em si** e não no **papel da integração** no nosso sistema
## Princípios de OO - ### S - ### O - ### L - ### I - ### D
## Princípios de OO - ### **Single Responsibility Principle** - ### **O**pen Closed Principle - ### **L**iskov Substitution Principle - ### **I**nterface Segregation Principle - ### **D**ependency Inversion Principle

Object Mentor

## O que testar? - ### **Responsabilidade**: definir se o pagamento foi um sucesso ou não? - ### **Confiança**: e se a API mudar? - ### **Disponibilidade**: e se o sistema externo cair? - ### Todas essas questões devem impactar **somente** o código responsável pelo pagamento - ### **Encapsulamento** é a palavra chave
## Responsabilidade ```python import requests_mock class TestPaypalPaymentClient(TestCase): def setUp(self): self.paypal_client = PaypalPaymentClient() self.data = {'credit_card': '1234', 'resource_id': 42} def test_check_payment_was_successful(self): with requests_mock.Mocker() as mock: mock.post('http://paypal.com/api', text='SUCCESS') paypal_response = self.paypal_client.pay(**self.data) self.assertTrue(paypal_response.success) def test_check_payment_wasnt_successful(self): with requests_mock.Mocker() as mock: mock.post('http://paypal.com/api', text='ERROR') paypal_response = self.paypal_client.pay(**self.data) self.assertFalse(paypal_response.success()) ```
## Responsabilidade ```python import requests class PaypalResponse(object): def __init__(self, paypal_response_text): self.paypal_response_text = paypal_response_text def success(self): return self.paypal_response_text == 'SUCCESS' class PaypalPaymentClient(object): def pay(self, credit_card, resource_id): data = {'credit_card': credit_card, 'resource_id': resource_id} response = requests.post('http://paypal.com/api/', data=data) return PaypalResponse(response.text) ```
## Confiança ```python class TestPaypalPaymentClient(TestCase): """..... testes de responsabilidade""" def test_ensure_payment_url(self): self.assertEqual('http://paypal.com', self.paypal_client.host) self.assertEqual('/api/', self.paypal_client.path) self.assertEqual('http://paypal.com/api/', self.paypal_client.payment_url) ```
## Confiança ```python import requests class PaypalResponse(object): def __init__(self, paypal_response_text): self.paypal_response_text = paypal_response_text def success(self): return self.paypal_response_text == 'SUCCESS' class PaypalPaymentClient(object): def __init__(self): self.host = 'http://paypal.com' self.path = '/api/' self.payment_url = '{}{}'.format(self.host, self.path) def pay(self, credit_card, resource_id): data = {'credit_card': credit_card, 'resource_id': resource_id} response = requests.post(self.payment_url, data=data) return PaypalResponse(response.text) ```
## Disponibilidade ```python class TestPaypalPaymentClient(TestCase): """..... testes de responsabilidade e confiança""" def test_raises_custom_exception_if_not_200_api_response(self): with requests_mock.Mocker() as mock: mock.post('http://paypal.com/api', text='ERROR', status_code=408) self.assertRaises( PaypalApiCommunicationException, self.paypal_client.pay, **self.data ) ```
## Confiança ```python import requests class PaypalApiCommunicationException(Exception): """ Exception raised when Paypal API answers other than 200 status code """ class PaypalResponse(object): def __init__(self, paypal_response_text): self.paypal_response_text = paypal_response_text def success(self): return self.paypal_response_text == 'SUCCESS' class PaypalPaymentClient(object): def __init__(self): self.host = 'http://paypal.com' self.path = '/api/' self.payment_url = '{}{}'.format(self.host, self.path) def pay(self, credit_card, resource_id): data = {'credit_card': credit_card, 'resource_id': resource_id} response = requests.post(self.payment_url, data=data) if not response.ok: msg = 'Error {} - Content: {}'.format(response.status_code, response.text) raise PaypalApiCommunicationException() return PaypalResponse(response.text) ```
## O que atingimos? - ### Definimos um **único ponto de comunicação** com o Paypal no nosso sistema - ### Trouxemos **semântica de domínio** com novos objetos - ### Trouxemos **semântica de erro** com uma exceção customizada - ### **Encapsulamos os detalhes de comunicação** para despreocupar o código que precise realizar o pagamento
## Sabe quem sempre fez isso mas nunca nos atentamos? - ### **ORMs =)** - ### São pontos únicos para comunicação com o banco de dados - ### Possuem semãntica de construção de consultas - ### Possuem semântica com erros próprios - ### Encapsulam os detalhes da comunicação
## Refatoração da view usando a lib mock ```python from mock import patch, Mock class TestPaymentView(TestCase): @patch.object(PaypalResponse, 'success', Mock(return_value=True)) def test_pay_order(self): data = {'credit_card': '1234'} self.client.post('/pagamento/42', data) order = Order.objects.get(id=42) self.assertTrue(order.paid) @patch.object(PaypalResponse, 'success', Mock(side_effect=PaypalApiCommunicationException)) def test_400_if_paypal_error(self): data = {'credit_card': '1234'} response = self.client.post('/pagamento/42', data) self.assertEqual(400, response.status_code) ```
```python """ Nova possível implementação da integração """ def payment_view(order_id): # códigos pra recuperar a ordem, validar form e tudo mais data = { 'resource_id': order_id, 'credit_card': form.cleaned_data['credit_card'] } paypal_client = PaypalPaymentClient() try: response = paypal_client.pay(**data) except PaypalApiCommunicationException: return HttpResponseBadRequest() if response.success(): order.paid = True order.save() # códigos pra seguir o fluxo http ```
## Dúvidas?

Obrigado!


Bernardo Fontes


twitter.com/bbfontes

github.com/berinhard

soundcloud.com/2bonsai

pessoas.cc

bernardoxhc@gmail.com