01 maio 2022

Armadilhas das variáveis de ambiente em testes unitários Python

Atualmente com o grande uso de computação em nuvem, também, é comum nós desenvolvedores criamos ferramentas que precisam de um certo dinamismo em alguns pontos do software, como por exemplo, chaves de acesso, informações do “host” do banco de dados e exposição de porta. Para ter essa fácil troca de valores o caminho mais fácil é usar variáveis de ambiente. Porém em um cenário de testes com Python e UnitTest (pacote encontrado na biblioteca padrão), alguns problemas podem aparecer se não utilizada os recursos corretos para se compor os testes.

Tomemos como exemplo o seguinte código para teste:

import os
from urllib.parse import urlparse
from unittest import TestCase


def resolver_service():
    url = os.getenv('SERVICE_URL', 'https://duck.com/')
    service = urlparse(url)
    return service.scheme, service.netloc


class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Ao ser executado, ele retorna os valores (padrões) esperado. Porém precisamos alterar esses mesmo valores para validar seu real funcionamento. Uma das formas de inserir valores de variávies de ambiente no Python é injetando diretamento no os.environ com o nome da variável. Extendendo o exemplo com mais 2 unidades de teste.

class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

    def test_service_ftp(self):
        os.environ['SERVICE_URL'] = 'ftp://example.tld'
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'ftp')
        self.assertEqual(name, 'example.tld')

    def test_service_https(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Ao executar temos um erro no último teste:

..F
======================================================================
FAIL: test_service_https (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/example_erro_testing.py", line 29, in test_service_https
    self.assertEqual(protocol, 'https')
AssertionError: 'ftp' != 'https'
- ftp
+ https

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

O último teste está falhando, pois está carregando o valor do teste anterior. O esperado era pegar o valor padrão que seria https. Ou seja, o Python não limpa os valores por padrão para a próxima execução. Mas ele tem ferramentas adequadas para esse tipo de trabalho, que seriam os mocks. No caso iremos usar o patch para ser mais exato, patch.dict que após a execução dos testes, restaura os valores padrões do dicionário que é a estrutura que estamos manipulando no Python para ter os valores da variável de ambiente.

class Test(TestCase):
    def test_service_default(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

    @patch.dict('os.environ', {'SERVICE_URL': 'ftp://example.tld'})
    def test_service_ftp(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'ftp')
        self.assertEqual(name, 'example.tld')

    def test_service_https(self):
        protocol, name = resolver_service()

        self.assertEqual(protocol, 'https')
        self.assertEqual(name, 'duck.com')

Com isso já teremos nossos testes funcionando e escrito da forma correta quando se quer manipular valores de variáveis de ambiente.