Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
MongoDBchevron-right

Teste e empacotamento de uma biblioteca Python

Mark Smith8 min read • Published Jan 04, 2024 • Updated Aug 14, 2024
MongoDBPython
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

Testando e empacotando uma biblioteca Python

Este tutorial mostrará como construir alguns conjuntos depytestdoúteis para testar o código que interage com um MongoDB database. Além disso, mostrarei como empacotar uma Python usando a popular bibliotecaHatchling e publicá-la no PyPI.
Este é o segundo tutorial de uma série! Sinta-se à vontade para conferir o primeiro tutorial, se quiser, mas ele não é necessário se você quiser apenas continuar lendo.

Codificando com Mark?

Este tutorial é vagamente baseado no segundo episódio de uma nova transmissão ao vivo que apresento, chamada "Coding with Mark". Estou transmitindo às quartas-feiras às 2 GMT (ou seja, 9 horário do leste ou 6 do Pacífico, se você acordar cedo!). Se esse tempo não funcionar para você, você sempre pode acompanhar assistindo à gravação!
Atualmente, estou criando uma biblioteca experimental de camada de acesso a dados que deve fornecer um kit de ferramentas para abstrair modelos de documentos complexos da camada de lógica comercial do aplicativo que os está usando.
Você pode conferir o código no repositório do GitHub do projeto!

O problema com os dados de teste

O teste é mais fácil quando o código que você está testando é relativamente autônomo e pode ser testado isoladamente. Tristemente, o código que funciona com dados dentro do MongoDB está no outro lado do esquema — é um teste de integração por definição porque você está testando sua integração com o MongoDB.
Você tem duas opções ao escrever um teste que funcione com o MongoDB:
  • Elimine o MongoDB, então, em vez de trabalhar com o MongoDB, seu código funciona com um objeto que se parece com o MongoDB, mas que não armazena dados. mongomock é uma boa solução se você estiver seguindo esta técnica.
  • Trabalhe diretamente com o MongoDB, mas certifique-se de que o banco de dados esteja em um estado conhecido antes da execução dos testes (carregando dados de teste em um banco de dados vazio) e, em seguida, limpe todas as alterações feitas depois que os testes forem executados.
A primeira abordagem é arquiteturalmente mais simples — seus testes não são executados no MongoDB, então você não precisa configurar ou executar um servidor MongoDB real. Por outro lado, você precisa gerenciar um objeto que finge ser um MongoClientou um Databaseou um Collection, para que ele responda de maneiras precisas a quaisquer chamadas feitas contra ele. E como não é uma conexão real do MongoDB, é fácil usar esses objetos de maneiras que não refletem com precisão uma conexão real do MongoDB.
minha abordagem preferida é a última: meus testes serão executados em uma instância real do MongoDB e a estrutura de teste limpará meu banco de dados após cada execução usando transações. Isso torna mais difícil executar os testes e eles podem ser executados mais lentamente, mas deve fazer um trabalho melhor ao destacar problemas reais que interagem com o próprio MongoDB.

Algumas abordagens alternativas

Antes de correr e decidir escrever meu próprio plugin para o pytest, decidimos ver o que outros fizeram antes de eu. Apesar de tudo, estou construindo meu próprio ODM — só há um limite para o Não Inventado Aqui. na minha vida. Há duas integrações de pytest razoáveis para uso com o MongoDB: pytest-mongo e pytest-mongodb. Infelizmente, nenhum dos dois fez exatamente o que eu queria. Mas ambos parecem bons – se eles fizerem o que você deseja, recomendamos usá-los.

pytest-mongo

Pytest-mongo é um plug-in pytest que permite testar código que depende de um MongoDB database em execução. Ele permite que você especifique fixtures para o processo e o cliente do MongoDB e iniciará um processo MongoDB para executar testes, se você o configurar para fazer isso.

pytest-mongodb

Pytest-mongo é um plug- plugin pytest que permite testar código que depende de uma conexão de banco de dados com um MongoDB e espera que determinados dados estejam presentes. Permite a você especificar acessórios para coleções de banco de dados no formato JSON/BSON ou YAML. Internamente, ele usa o mongomock para simular uma conexão MongoDB, ou você pode usar uma conexão MongoDB, se preferir.
Ambos oferecem recursos úteis — especialmente a capacidade de fornecer dados de fixtures especificados em arquivos no disco. O Pytest-mongo oferece até mesmo a capacidade de limpar o banco de dados após cada teste! No entanto, quando examinei um pouco mais, ele faz isso excluindo todas as collection no banco de dados de teste, que não é o comportamento que eu estava procurando.
Pretendo usar transações do MongoDB para reverter automaticamente todas as alterações feitas em cada teste. Dessa forma, o teste não confirmará nenhuma alteração no MongoDB, e apenas as alterações que ele teria feito serão revertidas, de modo que o banco de dados será eficientemente deixado no estado correto após cada execução de teste.

Dispositivos Pytest para MongoDB

Vou usar o recursode fixtures do pytest para fornecer um objeto de conexão MongoDB e uma sessão de transação para cada teste que os exija. Nos bastidores, cada objeto de fixação será limpo quando terminar.

Como funcionam os acessórios

As fixtures no pytest são definidas como funções, geralmente em um arquivo chamado conftest.py. O que muitas vezes surpreende as pessoas que não conhecem as fixtures, no entanto, é que o pytest as fornecerá magicamente a qualquer função de teste com um parâmetro com o mesmo nome da fixture. É uma forma de injeção de dependência e provavelmente é mais fácil de mostrar do que de descrever:
1# conftest.py
2def sample_fixture():
3
4    assert sample_fixture == "Hello, World"
Além de pytest fornecer valores de fixture para testar funções, ele também fará o mesmo com outras funções de fixture. Farei uso disso no segundo fixture que escrevo.
Os fixtures são chamados uma vez para seu escopo e, por padrão, o escopo de um fixture é "function", o que significa que ele será chamado uma vez para cada função de teste. Quero que meu fixture de "sessão" seja chamado (e revertido) para cada função, mas será muito mais eficiente para meu fixture de cliente "mongodb" ser chamado uma vez por sessão - ou seja, no início de todo o meu teste.
A última parte da teoria de fixação do pytest que quero explicar é que, se você quiser que algo seja limpo após o término de um escopo - por exemplo, quando a função de teste for concluída -, a maneira mais fácil de fazer isso é escrever uma função geradora usando yield em vez de return, assim:
1def sample_fixture():
2    # Any code here will be executed *before* the test run
3    yield "Hello, World"
4    # Any code here will be executed *after* the test run
Não sei quanto a você, mas, apesar da magia, eu realmente gosto dessa configuração. É bom e consistente, quando você sabe como usá-lo.

Um dispositivo MongoClient

O primeiro dispositivo de que preciso é um que retorne uma instância MongoClient que está conectada a um cluster MongoDB.
A propósito, os clustersMongoDB Atlas Serverless são perfeitas para isso, pois não custam nada quando você não as está usando. Se você estiver executando seus testes apenas algumas vezes por dia, ou até menos, essa pode ser uma boa maneira de economizar nos custos de hospedagem da infraestrutura de testes.
Quero fornecer configuração ao executor de teste por meio de uma variável de ambiente, MDB_URI, que será a string de conexão fornecida pelo Atlas. No futuro, talvez eu queira fornecer a string de conexão por meio de um sinalizador de linha de comando, que é algo que você pode fazer com o pytest, mas vou deixar isso para depois.
Como mencionei antes, o escopo do dispositivo deve ser "sessão" para que o cliente seja configurado uma vez no início da execução de teste e fechado no final. Na verdade, vamos deixar a limpeza para o Python, então não faria isso explicitamente.
Aqui está o acessório:
1import pytest
2import pymongo
3import os
4
5@pytest.fixture(scope="session")
6def mongodb():
7    client = pymongo.MongoClient(os.environ["MDB_URI"])
8    assert client.admin.command("ping")["ok"] != 0.0  # Check that the connection is okay.
9    return client
O código acima significa que posso escrever um teste que lê de um cluster MongoDB:
1# test_fixtures.py
2
3def test_mongodb_fixture(mongodb):
4        """ This test will pass if MDB_URI is set to a valid connection string. """
5        assert mongodb.admin.command("ping")["ok"] > 0

Transações no MongoDB

Como mencionei, o acessório acima é adequado para leitura de um banco de dados existente, mas quaisquer alterações feitas nos dados seriam mantidas após a conclusão dos testes. Para limpar corretamente após a execução do teste, preciso iniciar uma transação antes da execução do teste e, em seguida, anular a transação após a execução do teste para que todas as alterações sejam revertidas. É assim que o executor de testes do Django funciona com bancos de dados relacionais!
No MongoDB, para criar uma transação, primeiro você precisa iniciar uma sessão, o que é feito com o métodostart_session no objeto MongoClient. Depois de ter uma sessão, você pode chamar seu métodostart_transaction para iniciar uma transação e seu métodoabort_transaction para reverter quaisquer atualizações de banco de dados que foram executadas entre as duas chamadas.
Um aviso aqui: Você deve fornecer o objeto de sessão a todas as suas consultas ou elas não serão consideradas parte da sessão que você iniciou. Tudo isso junto tem a seguinte aparência:
1session = mongodb.start_session()
2session.start_transaction()
3my_collection.insert_one(
4    {"this document": "will be erased"},
5    session=session,
6)
7session.abort_transaction()
Isso não é tão ruim. Agora, vou mostrar como encerrar essa lógica em um fixture.

Concluindo uma transação em um fixture

O fixture pega o código acima, substitui o meio por uma instruçãoyield e o envolve em uma função de fixture:
1@pytest.fixture
2def rollback_session(mongodb):
3    session = mongodb.start_session()
4    session.start_transaction()
5    try:
6        yield session
7    finally:
8        session.abort_transaction()
Desta vez, não especifiquei o escopo do acessório, portanto, o padrão é "function", o que significa que a chamadaabort_transactionserá feita depois que cada função de teste for executada.
Apenas para ter certeza de que o dispositivo de teste reverte as alterações e também permite que as consultas subsequentes acessem os dados inseridos durante a transação, tenho um teste em meu arquivotest_docbridge.py:
1def test_update_mongodb(mongodb, rollback_session):
2    mongodb.docbridge.tests.insert_one(
3        {
4            "_id": "bad_document",
5            "description": "If this still exists, then transactions aren't working.",
6        },
7        session=rollback_session,
8    )
9    assert (
10        mongodb.docbridge.tests.find_one(
11            {"_id": "bad_document"}, session=rollback_session
12        )
13        != None
14    )
Observe que as chamadas para insert_one e find_one fornecem o valor de fixação rollback_sessioncomo um argumentosession . Se você esquecer, coisas inesperadas acontecerão!

Empacotando uma biblioteca Python

Empacotar uma biblioteca Python sempre foi um pouco trabalhoso, e isso é ainda mais comum pelo fato de que, atualmente, o ecossistema de empacotamento muda bastante. No momento em que escrevemos, um bom backend para criar pacotes Python está sendo criado no projeto Hatch.
Em termos gerais, para um pacote Python simples, as etapas para publicar seu pacote são estas:
  • Descreva seu pacote.
  • Crie seu pacote.
  • Envie o pacote para o PyPI.
Antes de Go por essas etapas, vale a pena instalar os seguintes pacotes em seu ambiente de desenvolvimento:
  • build - usado para instalar suas dependências de compilação e empacotar seu projeto
  • stringe - usado para enviar seus pacotes com segurança para o PyPI
Você pode instalar ambos com:
1python -m pip install –upgrade build twine

Descrevendo o pacote

Primeiro, você precisa descrever seu projeto. Anteriormente, isso exigiria um arquivosetup.py. Atualmente, pyproject.toml é o caminho a percorrer. Só direi um link para o arquivopyproject.toml no Github. Você verá que o arquivo descreve o projeto. Ela lista pymongo como uma dependência. Ele também afirma que "hatchling.build" é o backend de construção em algumas linhas no topo do arquivo.
Não é muito interessante, mas permite que você siga para a próxima etapa...

Construindo o pacote

Depois de descrever seu projeto, você pode criar uma distribuição a partir dele executando o seguinte comando:
1$ python -m build
2* Creating venv isolated environment...
3* Installing packages in isolated environment... (hatchling)
4* Getting build dependencies for sdist...
5* Building sdist...
6* Building wheel from sdist
7* Creating venv isolated environment...
8* Installing packages in isolated environment... (hatchling)
9* Getting build dependencies for wheel...
10* Building wheel...
11Successfully built docbridge-0.0.1.tar.gz and docbridge-0.0.1-py3-none-any.whl

Publicando no PyPI

Depois que os tarballs wheel e gzip forem criados, eles poderão ser publicados no PyPI (supondo que o nome da biblioteca ainda seja exclusivo!) executando o Twine:
1$ python -m twine upload dist/*
2Uploading distributions to https://upload.pypi.org/legacy/
3Enter your username: bedmondmark
4Enter your password:
5Uploading docbridge-0.0.1-py3-none-any.whl
6100% ━━━━━━━━━━━━━━━━━━━━ 6.6/6.6 kB • 00:00 • ?
7Uploading docbridge-0.0.1.tar.gz
8100% ━━━━━━━━━━━━━━━━━━━━8.5/8.5 kB • 00:00 • ?
9View at:
10https://pypi.org/project/docbridge/0.0.1/
E é isso! Não saberia você, mas sempre Go para ver se realmente funcionou.
Estou no PyPI!

Para frente e para cima

O trabalho desta semana foi super satisfativo. Sei que esse trabalho feito antecipadamente para fazer com que os testes sejam executados em transações e para publicar a biblioteca, embora ainda seja relativamente simples, se pagará com o tempo.
No meu próximo tutorial, começarei a observar dados incorporados em documentos. Estou estendendo a estrutura do docbridge para que ela possa lidar com arrays incorporadas, como trabalho para o que realmente deseja ser feito a seguir. Tentarei mostrar ao docbridge que os arrays nem sempre estão inteiramente contidos em um documento -- às vezes são subconjuntose, às vezes, são referências estendidas!
Estou muito ansioso com alguns dos blocos de construção de abstração que planejei, então certifique-se de ler meu próximo tutorial ou, se preferir, acompanhe-me na transmissão ao vivo às 2 tarde GMT às quartas-feiras!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Migração fácil: do banco de dados relacional para o MongoDB com o MongoDB Relational Migrator


Jan 04, 2024 | 6 min read
Tutorial

Codificação com Mark: abstraindo junções e subconjuntos em Python


Mar 19, 2024 | 11 min read
Início rápido

Reagindo às mudanças do banco de dados com o MongoDB Change Streams e Go


Feb 03, 2023 | 5 min read
Tutorial

Adição de notificações em tempo real ao Ghost CMS usando MongoDB e eventos enviados pelo servidor


Aug 14, 2023 | 7 min read
Sumário
  • Testando e empacotando uma biblioteca Python