diff --git a/README.md b/README.md index 46759ce..4e86ad2 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,11 @@ Detailed explanations: ## Current status -The project foundation and **Phases 1–8 are complete**. **Phase 9: External Libraries is in progress** with three reviewed-track chapters: [`pandas`](external-libraries/01-pandas/README.md), targeting pandas 3.0.x, [`openpyxl`](external-libraries/02-openpyxl/README.md), targeting openpyxl 3.1.x, and [`requests`](external-libraries/03-requests/README.md), targeting Requests 2.34.x. +The project foundation and **Phases 1–9 are complete**. **Phase 9: External Libraries** now contains four reviewed-track chapters: [`pandas`](external-libraries/01-pandas/README.md), [`openpyxl`](external-libraries/02-openpyxl/README.md), [`requests`](external-libraries/03-requests/README.md), and [`pytest`](external-libraries/04-pytest/README.md). -The phase now covers tabular-data transformation, Excel workbook automation, and HTTP/API consumption. Repository CI installs the explicit third-party contract in [`requirements-external.txt`](requirements-external.txt) before running approved examples. +The completed phase covers tabular-data transformation, Excel workbook automation, HTTP/API consumption, and automated testing. Repository CI installs the explicit third-party contract in [`requirements-external.txt`](requirements-external.txt) before running approved examples. -The next planned Phase 9 chapter is `pytest`. See the [External Libraries index](external-libraries/README.md), [roadmap](docs/roadmap.en.md), or [full learning path](docs/learning-path.en.md) for current status. +The next planned curriculum stage is **Phase 10: Practical Projects**. See the [External Libraries index](external-libraries/README.md), [roadmap](docs/roadmap.en.md), or [full learning path](docs/learning-path.en.md) for the completed Phase 9 record. ## Visual identity diff --git a/docs/learning-path.en.md b/docs/learning-path.en.md index 8cc64b6..177e5fa 100644 --- a/docs/learning-path.en.md +++ b/docs/learning-path.en.md @@ -115,16 +115,16 @@ Phase 7 is complete with five reviewed chapters. Chapters 01–02 establish exce Phase 8 is complete with nine reviewed chapters. The sequence moves from path modeling through time, structured data formats, runtime diagnostics, specialized containers, lazy iteration, decimal arithmetic, and finally operating-system and filesystem contracts. Chapter 09 closes the phase with environment state, path-like boundaries, deterministic scanning and traversal, metadata, copy/move/removal behavior, platform capabilities, and archive-safety decisions. -## Phase 9 · External Libraries 🚧 +## Phase 9 · External Libraries ✅ [Open the External Libraries section index](../external-libraries/README.md) 1. ✅ [`pandas`: Working with Tabular Data](../external-libraries/01-pandas/README.md) 2. ✅ [`openpyxl`: Automating Excel Workbooks](../external-libraries/02-openpyxl/README.md) 3. ✅ [`requests`: Consuming HTTP APIs](../external-libraries/03-requests/README.md) -4. ⏳ `pytest` +4. ✅ [`pytest`: Engineering Automated Tests](../external-libraries/04-pytest/README.md) -Phase 9 is in progress. Chapter 01 establishes labeled/tabular data contracts with pandas 3.0.x. Chapter 02 adds openpyxl 3.1.x workbook automation. Chapter 03 adds Requests 2.34.x HTTP/API contracts: methods, query parameters, headers, JSON, timeouts, exception boundaries, Sessions, TLS verification, streaming, retries, secret redaction, pagination, and deterministic local-server testing. +Phase 9 is complete with four reviewed chapters. The sequence moves from tabular-data contracts through Excel workbook automation and HTTP/API clients, then closes with pytest 9.1.x automated-testing contracts: discovery, assertions, parametrization, fixtures, temporary resources, monkeypatching, capture, marks, deterministic isolation, and CI behavior. ## Phase 10 · Practical Projects ⏳ diff --git a/docs/learning-path.es.md b/docs/learning-path.es.md index 9c3b19b..b78c6ee 100644 --- a/docs/learning-path.es.md +++ b/docs/learning-path.es.md @@ -115,16 +115,16 @@ La Fase 7 está completada con cinco capítulos revisados. Los Capítulos 01–0 La Fase 8 está completada con nueve capítulos revisados. La secuencia avanza desde modelado de rutas por tiempo, formatos estructurados de datos, diagnóstico en runtime, contenedores especializados, iteración lazy, aritmética decimal y finalmente contratos del sistema operativo y filesystem. El Capítulo 09 cierra la fase con estado del entorno, fronteras path-like, exploración y recorrido deterministas, metadatos, comportamiento de copia/movimiento/eliminación, capacidades de plataforma y decisiones de seguridad para archives. -## Fase 9 · Bibliotecas Externas 🚧 +## Fase 9 · Bibliotecas Externas ✅ [Abre el índice de la sección Bibliotecas Externas](../external-libraries/README.es.md) 1. ✅ [`pandas`: Trabajando con Datos Tabulares](../external-libraries/01-pandas/README.es.md) 2. ✅ [`openpyxl`: Automatizando Libros de Excel](../external-libraries/02-openpyxl/README.es.md) 3. ✅ [`requests`: Consumiendo APIs HTTP](../external-libraries/03-requests/README.es.md) -4. ⏳ `pytest` +4. ✅ [`pytest`: Ingeniería de Pruebas Automatizadas](../external-libraries/04-pytest/README.es.md) -La Fase 9 está en progreso. El Capítulo 01 establece contratos de datos etiquetados/tabulares con pandas 3.0.x. El Capítulo 02 añade automatización de libros con openpyxl 3.1.x. El Capítulo 03 añade contratos HTTP/API con Requests 2.34.x: métodos, parámetros de query, headers, JSON, timeouts, fronteras de excepción, Sessions, verificación TLS, streaming, retries, redacción de secretos, paginación y pruebas deterministas con servidor local. +La Fase 9 está completada con cuatro capítulos revisados. La secuencia avanza desde contratos de datos tabulares por automatización de libros de Excel y clientes HTTP/API, y cierra con contratos de pruebas automatizadas en pytest 9.1.x: descubrimiento, assertions, parametrización, fixtures, recursos temporales, monkeypatching, captura, marks, aislamiento determinista y comportamiento en CI. ## Fase 10 · Proyectos Prácticos ⏳ diff --git a/docs/learning-path.pt-BR.md b/docs/learning-path.pt-BR.md index 9d57ed1..f4dac2e 100644 --- a/docs/learning-path.pt-BR.md +++ b/docs/learning-path.pt-BR.md @@ -115,16 +115,16 @@ A Fase 7 está concluída com cinco capítulos revisados. Os Capítulos 01–02 A Fase 8 está concluída com nove capítulos revisados. A sequência avança de modelagem de caminhos por tempo, formatos estruturados de dados, diagnóstico em runtime, contêineres especializados, iteração lazy, aritmética decimal e finalmente contratos do sistema operacional e filesystem. O Capítulo 09 encerra a fase com estado do ambiente, fronteiras path-like, varredura e travessia determinísticas, metadados, comportamento de cópia/movimentação/remoção, capacidades de plataforma e decisões de segurança para archives. -## Fase 9 · Bibliotecas Externas 🚧 +## Fase 9 · Bibliotecas Externas ✅ [Abra o índice da seção Bibliotecas Externas](../external-libraries/README.pt-BR.md) 1. ✅ [`pandas`: Trabalhando com Dados Tabulares](../external-libraries/01-pandas/README.pt-BR.md) 2. ✅ [`openpyxl`: Automatizando Workbooks do Excel](../external-libraries/02-openpyxl/README.pt-BR.md) 3. ✅ [`requests`: Consumindo APIs HTTP](../external-libraries/03-requests/README.pt-BR.md) -4. ⏳ `pytest` +4. ✅ [`pytest`: Engenharia de Testes Automatizados](../external-libraries/04-pytest/README.pt-BR.md) -A Fase 9 está em andamento. O Capítulo 01 estabelece contratos de dados rotulados/tabulares com pandas 3.0.x. O Capítulo 02 acrescenta automação de workbooks com openpyxl 3.1.x. O Capítulo 03 acrescenta contratos HTTP/API com Requests 2.34.x: métodos, parâmetros de query, headers, JSON, timeouts, fronteiras de exceção, Sessions, verificação TLS, streaming, retries, redação de segredos, paginação e testes determinísticos com servidor local. +A Fase 9 está concluída com quatro capítulos revisados. A sequência avança de contratos de dados tabulares por automação de workbooks do Excel e clientes HTTP/API, encerrando com contratos de testes automatizados em pytest 9.1.x: descoberta, assertions, parametrização, fixtures, recursos temporários, monkeypatching, captura, marks, isolamento determinístico e comportamento em CI. ## Fase 10 · Projetos Práticos ⏳ diff --git a/docs/localized/README.es.md b/docs/localized/README.es.md index f2df644..c38aa68 100644 --- a/docs/localized/README.es.md +++ b/docs/localized/README.es.md @@ -74,11 +74,11 @@ Explicaciones detalladas: ## Estado actual -La base del proyecto y las **Fases 1–8 están completadas**. La **Fase 9: Bibliotecas Externas está en progreso** con tres capítulos revisados de la ruta: [`pandas`](../../external-libraries/01-pandas/README.es.md), apuntando a pandas 3.0.x, [`openpyxl`](../../external-libraries/02-openpyxl/README.es.md), apuntando a openpyxl 3.1.x, y [`requests`](../../external-libraries/03-requests/README.es.md), apuntando a Requests 2.34.x. +La base del proyecto y las **Fases 1–9 están completadas**. La **Fase 9: Bibliotecas Externas** ahora contiene cuatro capítulos revisados de la ruta: [`pandas`](../../external-libraries/01-pandas/README.es.md), [`openpyxl`](../../external-libraries/02-openpyxl/README.es.md), [`requests`](../../external-libraries/03-requests/README.es.md) y [`pytest`](../../external-libraries/04-pytest/README.es.md). -La fase ahora cubre transformación de datos tabulares, automatización de libros de Excel y consumo HTTP/API. El CI del repositorio instala el contrato explícito de terceros en [`requirements-external.txt`](../../requirements-external.txt) antes de ejecutar los ejemplos aprobados. +La fase completada cubre transformación de datos tabulares, automatización de libros de Excel, consumo HTTP/API y pruebas automatizadas. El CI del repositorio instala el contrato explícito de terceros en [`requirements-external.txt`](../../requirements-external.txt) antes de ejecutar los ejemplos aprobados. -El siguiente capítulo planificado de la Fase 9 es `pytest`. Consulta el [índice de Bibliotecas Externas](../../external-libraries/README.es.md), el [roadmap](../roadmap.es.md) o la [ruta completa de aprendizaje](../learning-path.es.md) para el estado actualizado. +La siguiente etapa planificada del currículo es **Fase 10: Proyectos Prácticos**. Consulta el [índice de Bibliotecas Externas](../../external-libraries/README.es.md), el [roadmap](../roadmap.es.md) o la [ruta completa de aprendizaje](../learning-path.es.md) para el registro de la Fase 9 completada. ## Identidad visual diff --git a/docs/localized/README.pt-BR.md b/docs/localized/README.pt-BR.md index 5d7feeb..eea39bb 100644 --- a/docs/localized/README.pt-BR.md +++ b/docs/localized/README.pt-BR.md @@ -74,11 +74,11 @@ Explicações detalhadas: ## Status atual -A fundação do projeto e as **Fases 1–8 estão concluídas**. A **Fase 9: Bibliotecas Externas está em andamento** com três capítulos revisados da trilha: [`pandas`](../../external-libraries/01-pandas/README.pt-BR.md), tendo pandas 3.0.x como alvo, [`openpyxl`](../../external-libraries/02-openpyxl/README.pt-BR.md), tendo openpyxl 3.1.x como alvo, e [`requests`](../../external-libraries/03-requests/README.pt-BR.md), tendo Requests 2.34.x como alvo. +A fundação do projeto e as **Fases 1–9 estão concluídas**. A **Fase 9: Bibliotecas Externas** agora contém quatro capítulos revisados da trilha: [`pandas`](../../external-libraries/01-pandas/README.pt-BR.md), [`openpyxl`](../../external-libraries/02-openpyxl/README.pt-BR.md), [`requests`](../../external-libraries/03-requests/README.pt-BR.md) e [`pytest`](../../external-libraries/04-pytest/README.pt-BR.md). -A fase agora cobre transformação de dados tabulares, automação de workbooks do Excel e consumo HTTP/API. O CI do repositório instala o contrato explícito de terceiros em [`requirements-external.txt`](../../requirements-external.txt) antes de executar os exemplos aprovados. +A fase concluída cobre transformação de dados tabulares, automação de workbooks do Excel, consumo HTTP/API e testes automatizados. O CI do repositório instala o contrato explícito de terceiros em [`requirements-external.txt`](../../requirements-external.txt) antes de executar os exemplos aprovados. -O próximo capítulo planejado da Fase 9 é `pytest`. Consulte o [índice de Bibliotecas Externas](../../external-libraries/README.pt-BR.md), o [roadmap](../roadmap.pt-BR.md) ou a [trilha completa de estudos](../learning-path.pt-BR.md) para o status atualizado. +A próxima etapa planejada do currículo é **Fase 10: Projetos Práticos**. Consulte o [índice de Bibliotecas Externas](../../external-libraries/README.pt-BR.md), o [roadmap](../roadmap.pt-BR.md) ou a [trilha completa de estudos](../learning-path.pt-BR.md) para o registro da Fase 9 concluída. ## Identidade visual diff --git a/docs/project-structure.en.md b/docs/project-structure.en.md index e7a8ee3..4a92534 100644 --- a/docs/project-structure.en.md +++ b/docs/project-structure.en.md @@ -213,16 +213,26 @@ python-study-guide/ │ │ ├── table_and_validation.py │ │ ├── workbook_basics.py │ │ └── write_only_export.py -│ └── 03-requests/ +│ ├── 03-requests/ +│ │ ├── README.md +│ │ ├── README.pt-BR.md +│ │ ├── README.es.md +│ │ └── examples/ +│ │ ├── get_with_query.py +│ │ ├── http_error_handling.py +│ │ ├── post_json.py +│ │ ├── session_defaults.py +│ │ └── stream_download.py +│ └── 04-pytest/ │ ├── README.md │ ├── README.pt-BR.md │ ├── README.es.md │ └── examples/ -│ ├── get_with_query.py -│ ├── http_error_handling.py -│ ├── post_json.py -│ ├── session_defaults.py -│ └── stream_download.py +│ ├── assertions_and_parametrize.py +│ ├── capture_output_and_logs.py +│ ├── exceptions_and_warnings.py +│ ├── fixtures_and_tmp_path.py +│ └── monkeypatch_environment.py ├── functions/ │ ├── README.md │ ├── README.pt-BR.md @@ -558,7 +568,7 @@ python-study-guide/ - `docs/`: master learning paths, roadmaps, project architecture, localized project documents, policies, and responsible AI-assisted development guidance. - `errors-files-and-modules/`: complete Phase 7 learning path. Chapters 01–05 cover runtime exception handling, deliberate raising and custom exceptions, safe text-file I/O with `open()` and `with`, TXT/CSV/JSON parsing and writing, and code organization through imports, modules, regular packages, execution context, and dependency design, in English, Brazilian Portuguese, and Spanish with deterministic executable examples. - `exercises/`: focused practice activities connected to learning chapters. -- `external-libraries/`: Phase 9 learning path for third-party packages. It currently contains reviewed multilingual chapters for pandas 3.0.x, openpyxl 3.1.x, and Requests 2.34.x with fifteen deterministic executable examples in total; `pytest` is planned next. +- `external-libraries/`: complete Phase 9 learning path for third-party packages. It contains reviewed multilingual chapters for pandas 3.0.x, openpyxl 3.1.x, Requests 2.34.x, and pytest 9.1.x, with twenty deterministic executable examples in total. The phase covers tabular transformations, Excel workbook automation, HTTP/API clients, and automated-testing contracts; Phase 10 practical projects come next. - `functions/`: complete Phase 5 learning path. Chapters 01–09 cover defining and calling functions, required inputs, returned values, scope and name lookup, type hints for function interfaces, default values including definition-time evaluation and mutable-default safety, variable-length positional and keyword argument collection with `*args` and `**kwargs`, composition through helper and coordinating functions with explicit dependencies and simple call graphs, and explicit data-flow tracing across calls including parameter bindings, rebinding versus mutation, `None`, tuple results, and return-based handoffs, in English, Brazilian Portuguese, and Spanish with deterministic executable examples. - `fundamentals/`: complete Phase 1 learning path. Its six chapters teach how Python runs a program, how to use `print()` and `input()`, how assignment and naming work, how to recognize and inspect common built-in data types, and how to convert compatible values deliberately, with aligned multilingual explanations and executable examples. - `practical-projects/`: future small projects combining several concepts. diff --git a/docs/project-structure.es.md b/docs/project-structure.es.md index 84754d5..41a4860 100644 --- a/docs/project-structure.es.md +++ b/docs/project-structure.es.md @@ -213,16 +213,26 @@ python-study-guide/ │ │ ├── table_and_validation.py │ │ ├── workbook_basics.py │ │ └── write_only_export.py -│ └── 03-requests/ +│ ├── 03-requests/ +│ │ ├── README.md +│ │ ├── README.pt-BR.md +│ │ ├── README.es.md +│ │ └── examples/ +│ │ ├── get_with_query.py +│ │ ├── http_error_handling.py +│ │ ├── post_json.py +│ │ ├── session_defaults.py +│ │ └── stream_download.py +│ └── 04-pytest/ │ ├── README.md │ ├── README.pt-BR.md │ ├── README.es.md │ └── examples/ -│ ├── get_with_query.py -│ ├── http_error_handling.py -│ ├── post_json.py -│ ├── session_defaults.py -│ └── stream_download.py +│ ├── assertions_and_parametrize.py +│ ├── capture_output_and_logs.py +│ ├── exceptions_and_warnings.py +│ ├── fixtures_and_tmp_path.py +│ └── monkeypatch_environment.py ├── functions/ │ ├── README.md │ ├── README.pt-BR.md @@ -558,7 +568,7 @@ python-study-guide/ - `docs/`: rutas completas de aprendizaje, roadmaps, arquitectura del proyecto, documentos localizados, políticas y guía de desarrollo responsable asistido por IA. - `errors-files-and-modules/`: ruta completa de la Fase 7. Los Capítulos 01–05 cubren manejo de excepciones en runtime, lanzamiento deliberado y excepciones personalizadas, I/O seguro de archivos de texto con `open()` y `with`, parsing y escritura de TXT/CSV/JSON y organización del código mediante imports, módulos, paquetes regulares, contexto de ejecución y diseño de dependencias, en inglés, portugués de Brasil y español con ejemplos ejecutables deterministas. - `exercises/`: actividades prácticas relacionadas con los capítulos. -- `external-libraries/`: ruta de la Fase 9 para paquetes de terceros. Actualmente contiene capítulos multilingües revisados de pandas 3.0.x, openpyxl 3.1.x y Requests 2.34.x con quince ejemplos ejecutables deterministas en total; `pytest` es el siguiente planificado. +- `external-libraries/`: ruta completa de la Fase 9 para paquetes de terceros. Contiene capítulos multilingües revisados de pandas 3.0.x, openpyxl 3.1.x, Requests 2.34.x y pytest 9.1.x, con veinte ejemplos ejecutables deterministas en total. La fase cubre transformaciones tabulares, automatización de libros de Excel, clientes HTTP/API y contratos de pruebas automatizadas; la Fase 10 de proyectos prácticos viene a continuación. - `functions/`: ruta completa de la Fase 5. Los Capítulos 01–09 cubren definición y llamada de funciones, entradas obligatorias, valores retornados, alcance y búsqueda de nombres, type hints para interfaces de funciones, valores predeterminados incluida la evaluación al definir la función y la seguridad con valores mutables, recolección de argumentos posicionales y por palabra clave de cantidad variable con `*args` y `**kwargs`, composición mediante funciones auxiliares y coordinadoras con dependencias explícitas y grafos simples de llamadas, y seguimiento explícito del flujo de datos entre llamadas, incluidos vínculos de parámetros, reasignación frente a mutación, `None`, resultados en tupla y traspasos mediante `return`, en inglés, portugués de Brasil y español, con ejemplos ejecutables determinísticos. - `fundamentals/`: ruta completa de la Fase 1. Sus seis capítulos enseñan cómo Python ejecuta un programa, cómo usar `print()` e `input()`, cómo funcionan la asignación y los nombres, cómo reconocer e inspeccionar tipos de datos incorporados comunes y cómo convertir valores compatibles de forma deliberada, con explicaciones multilingües alineadas y ejemplos ejecutables. - `practical-projects/`: futuros proyectos pequeños que combinarán varios conceptos. diff --git a/docs/project-structure.pt-BR.md b/docs/project-structure.pt-BR.md index 180bc2e..8f8efc9 100644 --- a/docs/project-structure.pt-BR.md +++ b/docs/project-structure.pt-BR.md @@ -213,16 +213,26 @@ python-study-guide/ │ │ ├── table_and_validation.py │ │ ├── workbook_basics.py │ │ └── write_only_export.py -│ └── 03-requests/ +│ ├── 03-requests/ +│ │ ├── README.md +│ │ ├── README.pt-BR.md +│ │ ├── README.es.md +│ │ └── examples/ +│ │ ├── get_with_query.py +│ │ ├── http_error_handling.py +│ │ ├── post_json.py +│ │ ├── session_defaults.py +│ │ └── stream_download.py +│ └── 04-pytest/ │ ├── README.md │ ├── README.pt-BR.md │ ├── README.es.md │ └── examples/ -│ ├── get_with_query.py -│ ├── http_error_handling.py -│ ├── post_json.py -│ ├── session_defaults.py -│ └── stream_download.py +│ ├── assertions_and_parametrize.py +│ ├── capture_output_and_logs.py +│ ├── exceptions_and_warnings.py +│ ├── fixtures_and_tmp_path.py +│ └── monkeypatch_environment.py ├── functions/ │ ├── README.md │ ├── README.pt-BR.md @@ -558,7 +568,7 @@ python-study-guide/ - `docs/`: trilhas completas de estudos, roadmaps, arquitetura do projeto, documentos localizados, políticas e guia de desenvolvimento responsável assistido por IA. - `errors-files-and-modules/`: trilha completa da Fase 7. Os Capítulos 01–05 cobrem tratamento de exceções em runtime, levantamento deliberado e exceções personalizadas, I/O seguro de arquivos de texto com `open()` e `with`, parsing e escrita de TXT/CSV/JSON e organização do código por imports, módulos, pacotes regulares, contexto de execução e design de dependências, em inglês, português brasileiro e espanhol com exemplos executáveis determinísticos. - `exercises/`: atividades práticas relacionadas aos capítulos. -- `external-libraries/`: trilha da Fase 9 para pacotes de terceiros. Atualmente contém capítulos multilíngues revisados de pandas 3.0.x, openpyxl 3.1.x e Requests 2.34.x com quinze exemplos executáveis determinísticos no total; `pytest` é o próximo planejado. +- `external-libraries/`: trilha completa da Fase 9 para pacotes de terceiros. Contém capítulos multilíngues revisados de pandas 3.0.x, openpyxl 3.1.x, Requests 2.34.x e pytest 9.1.x, com vinte exemplos executáveis determinísticos no total. A fase cobre transformações tabulares, automação de workbooks do Excel, clientes HTTP/API e contratos de testes automatizados; a Fase 10 de projetos práticos vem a seguir. - `functions/`: trilha completa da Fase 5. Os Capítulos 01–09 cobrem definição e chamada de funções, entradas obrigatórias, valores retornados, escopo e busca de nomes, type hints para interfaces de funções, valores padrão incluindo avaliação no momento da definição e segurança com padrões mutáveis, coleta de argumentos posicionais e nomeados de quantidade variável com `*args` e `**kwargs`, composição por funções auxiliares e coordenadoras com dependências explícitas e grafos simples de chamadas e rastreamento explícito do fluxo de dados entre chamadas, incluindo vínculos de parâmetros, reatribuição versus mutação, `None`, resultados em tupla e passagens por `return`, em inglês, português brasileiro e espanhol com exemplos executáveis determinísticos. - `fundamentals/`: trilha completa da Fase 1. Seus seis capítulos ensinam como o Python executa um programa, como usar `print()` e `input()`, como funcionam atribuição e nomes, como reconhecer e inspecionar tipos de dados embutidos comuns e como converter valores compatíveis de forma deliberada, com explicações multilíngues alinhadas e exemplos executáveis. - `practical-projects/`: futuros projetos pequenos combinando diversos conceitos. diff --git a/docs/roadmap.en.md b/docs/roadmap.en.md index 770e302..2a4b094 100644 --- a/docs/roadmap.en.md +++ b/docs/roadmap.en.md @@ -159,9 +159,9 @@ See the [section learning path](../external-libraries/README.md). - [x] [`pandas`](../external-libraries/01-pandas/README.md) - [x] [`openpyxl`](../external-libraries/02-openpyxl/README.md) - [x] [`requests`](../external-libraries/03-requests/README.md) -- [ ] `pytest` +- [x] [`pytest`](../external-libraries/04-pytest/README.md) -Phase 9 is in progress. Chapter 01 introduces pandas 3.0.x for labeled tabular data. Chapter 02 adds openpyxl 3.1.x for Excel workbook automation. Chapter 03 adds Requests 2.34.x for HTTP/API consumption with explicit methods, query/header/body contracts, timeouts, exception handling, Sessions, TLS verification, streaming, retry safety, pagination, secret redaction, and deterministic local HTTP tests. Executable external-library examples use the dependency contract declared in [`requirements-external.txt`](../requirements-external.txt). +Phase 9 is complete. Chapter 01 introduces pandas 3.0.x for labeled tabular data. Chapter 02 adds openpyxl 3.1.x workbook automation. Chapter 03 adds Requests 2.34.x HTTP/API contracts. Chapter 04 closes the phase with pytest 9.1.x automated-testing contracts covering discovery, assertions, fixtures, parametrization, temporary resources, monkeypatching, capture, marks, deterministic isolation, and CI. Executable external-library examples use the dependency contract declared in [`requirements-external.txt`](../requirements-external.txt). ## Phase 10: Practical projects diff --git a/docs/roadmap.es.md b/docs/roadmap.es.md index 0b610e1..6ef95b1 100644 --- a/docs/roadmap.es.md +++ b/docs/roadmap.es.md @@ -159,9 +159,9 @@ Consulta la [ruta de aprendizaje de la sección](../external-libraries/README.es - [x] [`pandas`](../external-libraries/01-pandas/README.es.md) - [x] [`openpyxl`](../external-libraries/02-openpyxl/README.es.md) - [x] [`requests`](../external-libraries/03-requests/README.es.md) -- [ ] `pytest` +- [x] [`pytest`](../external-libraries/04-pytest/README.es.md) -La Fase 9 está en progreso. El Capítulo 01 introduce pandas 3.0.x para datos tabulares etiquetados. El Capítulo 02 añade openpyxl 3.1.x para automatización de libros de Excel. El Capítulo 03 añade Requests 2.34.x para consumo HTTP/API con contratos explícitos de métodos, query/headers/cuerpos, timeouts, manejo de excepciones, Sessions, verificación TLS, streaming, seguridad de retries, paginación, redacción de secretos y pruebas HTTP locales deterministas. Los ejemplos ejecutables de bibliotecas externas usan el contrato declarado en [`requirements-external.txt`](../requirements-external.txt). +La Fase 9 está completada. El Capítulo 01 introduce pandas 3.0.x para datos tabulares etiquetados. El Capítulo 02 añade automatización de libros con openpyxl 3.1.x. El Capítulo 03 añade contratos HTTP/API con Requests 2.34.x. El Capítulo 04 cierra la fase con contratos de pruebas automatizadas en pytest 9.1.x, cubriendo descubrimiento, assertions, fixtures, parametrización, recursos temporales, monkeypatching, captura, marks, aislamiento determinista y CI. Los ejemplos ejecutables de bibliotecas externas usan el contrato declarado en [`requirements-external.txt`](../requirements-external.txt). ## Fase 10: Proyectos prácticos diff --git a/docs/roadmap.pt-BR.md b/docs/roadmap.pt-BR.md index cb9fa3b..bc42e31 100644 --- a/docs/roadmap.pt-BR.md +++ b/docs/roadmap.pt-BR.md @@ -159,9 +159,9 @@ Veja a [trilha de aprendizagem da seção](../external-libraries/README.pt-BR.md - [x] [`pandas`](../external-libraries/01-pandas/README.pt-BR.md) - [x] [`openpyxl`](../external-libraries/02-openpyxl/README.pt-BR.md) - [x] [`requests`](../external-libraries/03-requests/README.pt-BR.md) -- [ ] `pytest` +- [x] [`pytest`](../external-libraries/04-pytest/README.pt-BR.md) -A Fase 9 está em andamento. O Capítulo 01 introduz pandas 3.0.x para dados tabulares rotulados. O Capítulo 02 acrescenta openpyxl 3.1.x para automação de workbooks do Excel. O Capítulo 03 acrescenta Requests 2.34.x para consumo HTTP/API com contratos explícitos de métodos, query/headers/corpos, timeouts, tratamento de exceções, Sessions, verificação TLS, streaming, segurança de retries, paginação, redação de segredos e testes HTTP locais determinísticos. Os exemplos executáveis de bibliotecas externas usam o contrato declarado em [`requirements-external.txt`](../requirements-external.txt). +A Fase 9 está concluída. O Capítulo 01 introduz pandas 3.0.x para dados tabulares rotulados. O Capítulo 02 acrescenta automação de workbooks com openpyxl 3.1.x. O Capítulo 03 acrescenta contratos HTTP/API com Requests 2.34.x. O Capítulo 04 encerra a fase com contratos de testes automatizados em pytest 9.1.x, cobrindo descoberta, assertions, fixtures, parametrização, recursos temporários, monkeypatching, captura, marks, isolamento determinístico e CI. Os exemplos executáveis de bibliotecas externas usam o contrato declarado em [`requirements-external.txt`](../requirements-external.txt). ## Fase 10: Projetos práticos diff --git a/external-libraries/03-requests/README.es.md b/external-libraries/03-requests/README.es.md index cd251fd..66736c5 100644 --- a/external-libraries/03-requests/README.es.md +++ b/external-libraries/03-requests/README.es.md @@ -985,14 +985,15 @@ Cuando se preparó este capítulo, Requests 2.34.2 era la versión estable más ## 83. Próximo capítulo -La Fase 9 ahora conecta tres fronteras prácticas: +La Fase 9 ahora conecta cuatro fronteras prácticas: ```text pandas -> transform tabular data openpyxl -> construct and maintain Excel workbooks requests -> exchange data with HTTP services and APIs +pytest -> verify behavior repeatedly and automatically ``` -La siguiente biblioteca planificada es **`pytest`**, donde el enfoque pasa de usar bibliotecas externas a demostrar sistemáticamente que el comportamiento de Python continúa siendo correcto. +El siguiente capítulo ya está publicado: [`pytest`: Ingeniería de Pruebas Automatizadas](../04-pytest/README.es.md). Pasa del consumo de servicios externos a demostrar sistemáticamente que el comportamiento de Python continúa siendo correcto. Antes de continuar, construye al menos un cliente contra un servidor HTTP local. El código de API confiable comienza cuando el comportamiento de red se convierte en algo que puedes reproducir, inspeccionar y probar. diff --git a/external-libraries/03-requests/README.md b/external-libraries/03-requests/README.md index f91fbcb..ab57a88 100644 --- a/external-libraries/03-requests/README.md +++ b/external-libraries/03-requests/README.md @@ -985,14 +985,15 @@ At the time this chapter was prepared, Requests 2.34.2 was the latest stable rel ## 83. Next chapter -Phase 9 now connects three practical boundaries: +Phase 9 now connects four practical boundaries: ```text pandas -> transform tabular data openpyxl -> construct and maintain Excel workbooks requests -> exchange data with HTTP services and APIs +pytest -> verify behavior repeatedly and automatically ``` -The next planned library is **`pytest`**, where the focus moves from using external libraries to systematically proving that Python behavior remains correct. +The next chapter is now published: [`pytest`: Engineering Automated Tests](../04-pytest/README.md). It moves from consuming external services to systematically proving that Python behavior remains correct. Before moving on, build at least one client against a local HTTP server. Reliable API code starts when network behavior becomes something you can reproduce, inspect, and test. diff --git a/external-libraries/03-requests/README.pt-BR.md b/external-libraries/03-requests/README.pt-BR.md index 71dc19c..256c0db 100644 --- a/external-libraries/03-requests/README.pt-BR.md +++ b/external-libraries/03-requests/README.pt-BR.md @@ -985,14 +985,15 @@ Quando este capítulo foi preparado, Requests 2.34.2 era a versão estável mais ## 83. Próximo capítulo -A Fase 9 agora conecta três fronteiras práticas: +A Fase 9 agora conecta quatro fronteiras práticas: ```text pandas -> transform tabular data openpyxl -> construct and maintain Excel workbooks requests -> exchange data with HTTP services and APIs +pytest -> verify behavior repeatedly and automatically ``` -A próxima biblioteca planejada é **`pytest`**, quando o foco passa de usar bibliotecas externas para provar sistematicamente que o comportamento Python continua correto. +O próximo capítulo já está publicado: [`pytest`: Engenharia de Testes Automatizados](../04-pytest/README.pt-BR.md). Ele passa do consumo de serviços externos para a comprovação sistemática de que o comportamento Python continua correto. Antes de avançar, construa pelo menos um cliente contra um servidor HTTP local. Código confiável de API começa quando o comportamento da rede se torna algo que você consegue reproduzir, inspecionar e testar. diff --git a/external-libraries/04-pytest/README.es.md b/external-libraries/04-pytest/README.es.md new file mode 100644 index 0000000..9f1643d --- /dev/null +++ b/external-libraries/04-pytest/README.es.md @@ -0,0 +1,1279 @@ +
+ +# Ingeniería de Pruebas Automatizadas con `pytest` + +[🇺🇸 English](README.md) · [🇧🇷 Português](README.pt-BR.md) · [🇪🇸 Español](README.es.md) + +
+ +[← Volver a Bibliotecas Externas](../README.es.md) · [← Anterior: `requests`](../03-requests/README.es.md) + +El software resulta más fácil de cambiar cuando el comportamiento esperado puede verificarse de forma repetible y automática. `pytest` ofrece un modelo de pruebas conciso basado en funciones normales de Python, instrucciones `assert`, fixtures reutilizables, parametrización, informes de fallo detallados y un sistema extensible de plugins. + +Este capítulo apunta a **pytest 9.1.x** y fue investigado con la documentación y los metadatos de la versión estable actual, **pytest 9.1.1**. pytest 9.1.1 requiere Python 3.10 o posterior; este repositorio valida los ejemplos con Python 3.13. + +**Tiempo estimado de estudio:** 300–390 minutos. + +## Objetivos de aprendizaje + +Al finalizar este capítulo, deberías poder: + +- explicar qué demuestra una prueba automatizada y qué no demuestra; +- organizar pruebas para que pytest las descubra de forma predecible; +- escribir assertions legibles e interpretar la introspección de assertions; +- probar valores de punto flotante, excepciones y warnings de forma deliberada; +- reducir duplicación con parametrización; +- modelar setup, teardown y dependencias mediante fixtures; +- aislar filesystem y entorno con `tmp_path` y `monkeypatch`; +- capturar salida estándar y logs con `capsys` y `caplog`; +- usar marks, skips, fallos esperados y selección de pruebas intencionalmente; +- configurar pytest sin ocultar warnings ni omisiones accidentales de pruebas; +- distinguir fronteras de pruebas unitarias, de integración y end-to-end; +- evitar pruebas flaky causadas por tiempo, aleatoriedad, red, estado compartido o supuestos de orden; +- integrar pytest en CI como un contrato ejecutable de calidad. + +## 1. Por qué existen las pruebas automatizadas + +Una comprobación manual responde una pregunta una vez. Una prueba automatizada convierte esa pregunta en código ejecutable que puede repetirse después de cambios futuros. + +Una buena prueba describe un comportamiento, proporciona entradas controladas, observa una salida o efecto secundario y falla cuando el comportamiento observado viola el contrato esperado. + +Las pruebas reducen incertidumbre. No demuestran que el software esté libre de bugs. + +## 2. Qué añade `pytest` + +Python incluye `unittest` en la biblioteca estándar. `pytest` es un framework externo capaz de ejecutar funciones de prueba simples y añadir recursos como: + +- introspección de assertions; +- fixtures; +- parametrización; +- marks y selección de pruebas; +- rutas temporales; +- monkeypatch del entorno; +- captura de salida, warnings y logs; +- plugins y hooks. + +El objetivo no es hacer pruebas ingeniosas. El objetivo es hacer visible la intención y barata la repetición. + +## 3. Las bibliotecas externas necesitan un contrato de versión + +Este repositorio declara las dependencias de la Fase 9 en `requirements-external.txt`. + +Para este capítulo el contrato es: + +```text +pytest >= 9.1 and < 9.2 +``` + +El límite superior importa porque el changelog de pytest ya contiene un draft no publicado de la versión 9.2 con cambios incompatibles. Un currículo publicado debe describir comportamiento ya lanzado y no seguir silenciosamente una versión futura. + +## 4. Instala el conjunto de dependencias del repositorio + +Crea y activa un entorno virtual y luego instala: + +```bash +python -m pip install -r requirements-external.txt +``` + +Para experimentar de forma aislada: + +```bash +python -m pip install pytest +``` + +Aun así, un proyecto debería registrar qué rango de pytest soporta. + +## 5. Prefiere `python -m pytest` cuando importa la identidad del intérprete + +Una invocación común es: + +```bash +python -m pytest +``` + +Usar `python -m` hace explícito el intérprete. Esto resulta especialmente útil cuando existen varias instalaciones de Python o entornos virtuales en la misma máquina. + +El comando `pytest` también es válido cuando el entorno no es ambiguo. + +## 6. Una prueba es especificación ejecutable, no código de producción + +Considera una función pequeña: + +```python +def calculate_total(unit_price: int, quantity: int) -> int: + return unit_price * quantity +``` + +Una prueba puede declarar un comportamiento esperado: + +```python +def test_calculate_total_multiplies_price_by_quantity() -> None: + assert calculate_total(12, 3) == 36 +``` + +El nombre de la prueba comunica el contrato antes de leer la assertion. + +## 7. pytest descubre pruebas por convención + +Por defecto, pytest descubre módulos y funciones de prueba mediante convenciones de nombres. + +Una estructura común es: + +```text +project/ +├── src/ +│ └── calculator.py +└── tests/ + └── test_calculator.py +``` + +Dentro de `test_calculator.py`, las funciones llamadas `test_*` se recopilan como pruebas. + +## 8. La colección es una fase de la ejecución + +Antes de ejecutar pruebas, pytest primero las descubre y recopila. + +Puedes inspeccionar la colección sin ejecutar nada: + +```bash +python -m pytest --collect-only +``` + +Esto ayuda cuando una prueba esperada no aparece. + +Una suite verde que recopiló las pruebas equivocadas no es una señal confiable. + +## 9. Mantén los nombres de pruebas orientados al comportamiento + +Prefiere nombres que expliquen una regla observable: + +```python +def test_discount_is_zero_for_empty_cart() -> None: + ... +``` + +Evita nombres que solo reflejen detalles de implementación: + +```python +def test_function_2() -> None: + ... +``` + +Buenos nombres facilitan el diagnóstico de fallos en CI. + +## 10. El `assert` simple es el estilo normal de assertion en pytest + +```python +def test_status_is_ready() -> None: + status = "ready" + assert status == "ready" +``` + +pytest reescribe assertions durante la colección para producir diagnósticos más ricos que un `AssertionError` puro. + +## 11. La introspección de assertions ayuda a explicar fallos + +Una comparación como: + +```python +def test_summary() -> None: + actual = {"count": 2, "status": "ready"} + expected = {"count": 3, "status": "ready"} + assert actual == expected +``` + +puede mostrar los valores diferentes cuando falla. + +Por ello, expresiones directas suelen ser mejores que mensajes vagos construidos manualmente en todas las assertions. + +## 12. Añade un mensaje solo cuando aporte contexto de dominio + +```python +def test_inventory_never_becomes_negative() -> None: + remaining = 4 + assert remaining >= 0, "inventory contract requires a non-negative balance" +``` + +El mensaje debe explicar por qué importa la condición, no limitarse a repetir `remaining >= 0`. + +## 13. Usa Arrange, Act, Assert cuando aclare la prueba + +Una prueba legible suele tener tres etapas conceptuales: + +```python +def test_normalize_name_removes_outer_whitespace() -> None: + raw_name = " Nova " + + normalized = raw_name.strip() + + assert normalized == "Nova" +``` + +No toda prueba pequeña necesita comentarios nombrando las etapas. La propia estructura puede hacerlas evidentes. + +## 14. Prueba un comportamiento coherente + +Una prueba puede contener varias assertions cuando describen un único resultado, pero evita convertir una sola prueba en un recorrido por comportamientos no relacionados. + +Pruebas más pequeñas producen fallos más locales y fáciles de diagnosticar. + +## 15. Las pruebas deterministas son repetibles + +Una prueba determinista produce el mismo resultado cuando el código y las entradas relevantes no han cambiado. + +Amenazas comunes incluyen: + +- hora actual; +- valores aleatorios sin control; +- servicios de red; +- archivos o bases compartidos; +- variables de entorno; +- locale y zona horaria; +- dependencia del orden de ejecución. + +El aislamiento es una habilidad de diseño, no solo una función del framework. + +## 16. Compara resultados de punto flotante con tolerancia explícita + +Los valores binarios de punto flotante muchas veces no son adecuados para igualdad exacta después de cálculos. + +pytest ofrece `approx()`: + +```python +import pytest + + +def test_ratio() -> None: + result = 1 / 3 + assert result == pytest.approx(0.333333, rel=1e-5) +``` + +Elige tolerancias según el dominio y no copiando valores arbitrarios. + +## 17. Los valores exactos deben seguir usando assertions exactas + +No uses `pytest.approx()` cuando el contrato sea exacto. + +```python +def test_item_count() -> None: + assert len(["a", "b", "c"]) == 3 +``` + +Las herramientas de prueba deben aclarar contratos, no difuminarlos. + +## 18. Prueba excepciones esperadas con `pytest.raises` + +```python +import pytest + + +def parse_positive(value: str) -> int: + number = int(value) + if number <= 0: + raise ValueError("value must be positive") + return number + + +def test_zero_is_rejected() -> None: + with pytest.raises(ValueError): + parse_positive("0") +``` + +La prueba pasa solo si el tipo de excepción esperado se lanza dentro del context manager. + +## 19. Haz match del mensaje cuando forme parte del contrato + +```python +def test_zero_has_clear_message() -> None: + with pytest.raises(ValueError, match="must be positive"): + parse_positive("0") +``` + +`match` se interpreta como expresión regular. Escapa caracteres especiales cuando quieras una coincidencia literal. + +## 20. Inspecciona la información de la excepción capturada cuando sea necesario + +```python +def test_invalid_value_context() -> None: + with pytest.raises(ValueError) as exc_info: + parse_positive("-4") + + assert "positive" in str(exc_info.value) +``` + +Hazlo cuando el detalle adicional importe. No inspecciones internals solo porque pytest los expone. + +## 21. Prueba warnings explícitamente con `pytest.warns` + +```python +import warnings + +import pytest + + +def old_api() -> None: + warnings.warn("old API", DeprecationWarning, stacklevel=2) + + +def test_old_api_warns() -> None: + with pytest.warns(DeprecationWarning, match="old API"): + old_api() +``` + +Los warnings pueden representar contratos de migración que merecen pruebas propias. + +## 22. pytest 9.1 puede imponer un presupuesto de warnings + +pytest 9.1 añadió `--max-warnings`. + +Por ejemplo: + +```bash +python -m pytest --max-warnings=10 +``` + +Si todas las pruebas pasan pero la cantidad de warnings no filtrados supera el límite, pytest devuelve un estado dedicado distinto de cero. + +Un presupuesto de warnings ayuda a reducir deuda gradualmente en vez de ocultarla toda. + +## 23. La parametrización convierte variación de datos en casos de prueba + +Cuando el mismo comportamiento debe cumplirse para varias entradas, usa `@pytest.mark.parametrize`: + +```python +import pytest + + +@pytest.mark.parametrize( + ("raw", "expected"), + [(-5, 0), (40, 40), (130, 100)], +) +def test_normalize_score(raw: int, expected: int) -> None: + result = max(0, min(raw, 100)) + assert result == expected +``` + +pytest crea un caso recopilado separado para cada conjunto de parámetros. + +## 24. Separa lógica de prueba y datos de prueba + +La parametrización funciona mejor cuando el cuerpo expresa una regla y los datos representan casos interesantes. + +Incluye fronteras significativas, no solo muchos ejemplos aleatorios. + +Una tabla de diez filas no es automáticamente mejor que tres casos de frontera bien elegidos. + +## 25. Da IDs útiles a los parámetros cuando el informe los necesite + +```python +@pytest.mark.parametrize( + ("value", "expected"), + [(0, "empty"), (1, "single"), (2, "many")], + ids=["zero", "one", "multiple"], +) +def test_classification(value: int, expected: str) -> None: + result = "empty" if value == 0 else "single" if value == 1 else "many" + assert result == expected +``` + +IDs legibles mejoran los informes para valores de parámetros complejos. + +## 26. Usa `pytest.param` para metadatos por caso + +```python +@pytest.mark.parametrize( + "value", + [ + 1, + pytest.param(-1, marks=pytest.mark.xfail(reason="known limitation")), + ], +) +def test_positive_only(value: int) -> None: + assert value > 0 +``` + +Las marks por caso mantienen excepciones visibles sin duplicar toda la prueba. + +## 27. pytest 9.1 depreca iterables que no son collections en parametrización + +La documentación actual de pytest depreca pasar directamente un iterable que no sea `Collection`, como un generator, en `argvalues`. + +Prefiere una lista o tupla concreta en pruebas publicadas: + +```python +cases = [(1, 2), (2, 4), (3, 6)] +``` + +Esto también facilita revisar los datos de prueba. + +## 28. Las fixtures modelan dependencias de prueba + +Una fixture es un valor o recurso que pytest entrega a una prueba por nombre. + +```python +import pytest + + +@pytest.fixture +def sample_user() -> dict[str, str]: + return {"name": "Nova", "role": "reader"} + + +def test_user_role(sample_user: dict[str, str]) -> None: + assert sample_user["role"] == "reader" +``` + +La prueba solicita la fixture declarando un parámetro con ese nombre. + +## 29. Las fixtures pueden devolver objetos + +Las fixtures pueden devolver valores simples, estructuras, clientes configurados, repositorios temporales, conexiones u otros recursos. + +Mantén las fixtures enfocadas. Una fixture gigante que prepara toda la aplicación puede ocultar dependencias en vez de aclararlas. + +## 30. Las fixtures pueden hacer teardown con `yield` + +```python +import pytest + + +@pytest.fixture +def opened_resource(): + resource = {"open": True} + yield resource + resource["open"] = False +``` + +El código antes de `yield` hace setup. El código posterior hace teardown cuando pytest finaliza la fixture. + +Usa context managers reales cuando el recurso de producción ya proporcione uno. + +## 31. El scope de la fixture controla su duración + +Scopes habituales: + +```text +function -> one test invocation +class -> one test class +module -> one test module +package -> one test package +session -> the whole pytest session +``` + +El valor predeterminado es `function`. + +## 32. Un scope más amplio intercambia aislamiento por reutilización + +Un recurso de scope `session` puede ser más rápido al crearse una vez, pero también vive más tiempo y puede transportar estado mutable compartido entre pruebas. + +No amplíes el scope solo para acelerar la suite. Primero comprueba que la vida compartida conserva independencia. + +## 33. Las fixtures pueden depender de otras fixtures + +```python +import pytest + + +@pytest.fixture +def base_url() -> str: + return "https://example.invalid" + + +@pytest.fixture +def endpoint(base_url: str) -> str: + return f"{base_url}/items" +``` + +La composición de dependencias suele ser más clara que una fixture que conoce todo el setup. + +## 34. Evita dependencias ocultas con exceso de `autouse` + +Una fixture `autouse=True` se ejecuta sin aparecer en la firma de cada prueba. + +Puede ser útil para una invariante real de toda la suite, pero el uso excesivo dificulta rastrear el comportamiento. + +Prefiere parámetros explícitos salvo que la aplicación automática forme parte genuina del contrato del entorno. + +## 35. `conftest.py` comparte configuración local y fixtures + +Una estructura común es: + +```text +tests/ +├── conftest.py +├── test_api.py +└── test_reports.py +``` + +Fixtures definidas en `tests/conftest.py` pueden ser descubiertas por pruebas debajo de ese directorio sin importar `conftest` directamente. + +## 36. `conftest.py` sigue reglas de visibilidad por directorio + +Para una prueba, pytest consulta archivos `conftest.py` relevantes en el directorio de la prueba y en sus directorios padre. + +Eso hace jerárquica la visibilidad. + +Coloca fixtures compartidas en el nivel más estrecho que realmente las necesite. + +## 37. No importes desde `conftest.py` + +Trata `conftest.py` como configuración de pytest, no como módulo de aplicación. + +Si los helpers necesitan imports normales, colócalos en un módulo o paquete regular e importa ese módulo desde pruebas y fixtures. + +## 38. `tmp_path` proporciona un `Path` temporal a cada prueba + +```python +from pathlib import Path + + +def test_export(tmp_path: Path) -> None: + report = tmp_path / "report.txt" + report.write_text("ready\n", encoding="utf-8") + + assert report.read_text(encoding="utf-8") == "ready\n" +``` + +`tmp_path` es un `pathlib.Path` único para cada invocación de prueba. + +Esto evita ensuciar el repositorio con artefactos de pruebas. + +## 39. `tmp_path_factory` sirve para scopes más amplios + +Una fixture de scope `session` o `module` no puede depender de un `tmp_path` de scope función. + +Para recursos temporales de mayor duración, pytest ofrece `tmp_path_factory`. + +Elige una vida más amplia solo cuando forme parte del diseño. + +## 40. `monkeypatch` cambia estado y lo restaura automáticamente + +La fixture `monkeypatch` puede modificar temporalmente: + +- atributos de objetos; +- elementos de diccionarios; +- variables de entorno; +- `sys.path`; +- directorio de trabajo actual. + +Los cambios se revierten después de finalizar la prueba o fixture solicitante. + +## 41. Parchea variables de entorno con `setenv` y `delenv` + +```python +import os + + +def read_mode() -> str: + return os.getenv("STUDY_MODE", "default") + + +def test_configured_mode(monkeypatch) -> None: + monkeypatch.setenv("STUDY_MODE", "focused") + assert read_mode() == "focused" +``` + +El código dependiente del entorno se vuelve determinista cuando la prueba controla explícitamente el entorno. + +## 42. Parchea donde el código busca la dependencia + +Supón que `service.py` contiene: + +```python +from client import fetch_status + + +def is_ready() -> bool: + return fetch_status() == "ready" +``` + +La prueba normalmente necesita parchear `service.fetch_status`, porque ese es el nombre que `is_ready()` resuelve en runtime. + +Parchear la definición original en `client` puede no reemplazar una referencia ya importada en `service`. + +## 43. `monkeypatch.context()` puede limitar aún más la duración del parche + +Cuando una prueba necesita el parche solo dentro de un bloque pequeño, `monkeypatch.context()` crea un contexto anidado cuyos cambios se revierten al salir. + +Una vida más corta del parche reduce interacciones sorprendentes en pruebas complejas. + +## 44. Usa test doubles para sustituir fronteras, no todo + +Un test double puede representar un colaborador lento, no determinista, destructivo o no disponible. + +Categorías informales habituales: + +```text +stub -> returns controlled values +fake -> lightweight working implementation +spy -> records how it was used +mock -> verifies expected interactions +``` + +El vocabulario importa menos que dejar claro el propósito de la sustitución. + +## 45. `unittest.mock` de la biblioteca estándar funciona con pytest + +pytest no exige un estilo separado de mocking. + +Puedes combinar assertions y fixtures de pytest con `unittest.mock.Mock`, `MagicMock` o `patch` cuando encajen. + +No hagas mock de cálculos puros solo porque la herramienta existe. + +## 46. `capsys` captura stdout y stderr a nivel Python + +```python +def announce(topic: str) -> None: + print(f"Studying: {topic}") + + +def test_announce(capsys) -> None: + announce("pytest") + captured = capsys.readouterr() + assert captured.out == "Studying: pytest\n" + assert captured.err == "" +``` + +Resulta útil para interfaces de línea de comandos y funciones cuya salida forma parte del contrato. + +## 47. `capfd` captura a nivel de file descriptor + +`capsys` se centra en `sys.stdout` y `sys.stderr` de Python. + +`capfd` captura los file descriptors 1 y 2, útil cuando la salida proviene de código de nivel inferior que evita los streams normales de Python. + +Usa el mecanismo más estrecho que coincida con el comportamiento probado. + +## 48. `caplog` captura registros de logging + +```python +import logging + + +def test_log_message(caplog) -> None: + logger = logging.getLogger("study") + + with caplog.at_level(logging.INFO, logger="study"): + logger.info("session ready") + + assert "session ready" in caplog.text +``` + +Las pruebas también pueden inspeccionar registros estructurados en lugar de limitarse al texto renderizado. + +## 49. Cuidado al reconfigurar el root logger durante `caplog` + +La documentación de pytest advierte que cambiar handlers del root logger durante una prueba puede interferir con la captura. + +Prefiere configuración dirigida a un logger específico y evita reemplazar todo el conjunto de handlers salvo que la prueba valide precisamente esa configuración. + +## 50. Las marks añaden metadatos a las pruebas + +Las marks pueden clasificar o modificar comportamiento. + +```python +import pytest + + +@pytest.mark.slow +def test_large_report() -> None: + assert True +``` + +Las custom marks deben representar categorías útiles, no sustituir una organización clara de la suite. + +## 51. Registra las custom marks + +Las marks no registradas pueden generar warnings y los errores de escritura pueden crear categorías no deseadas silenciosamente. + +Un `pyproject.toml` puede registrarlas: + +```toml +[tool.pytest.ini_options] +markers = [ + "slow: tests that intentionally take longer", + "integration: tests that cross component boundaries", +] +``` + +Registrar nombres convierte las marks en un contrato documentado del proyecto. + +## 52. `strict_markers` puede convertir marks desconocidas en errores + +```toml +[tool.pytest.ini_options] +strict_markers = true +``` + +Esto resulta útil cuando una marca mal escrita debe fallar inmediatamente en lugar de convertirse en una nueva categoría. + +## 53. pytest 9 introdujo un strict mode más amplio + +pytest 9 ofrece la opción `strict`, que activa conjuntamente comprobaciones de configuración, markers, xfail e IDs de parametrización. + +La documentación avisa de que futuras versiones pueden añadir más opciones de strictness. Usa el modo global con una versión controlada o cuando el proyecto quiera adoptar nuevas verificaciones deliberadamente. + +## 54. Salta pruebas solo por una razón ambiental real + +```python +import sys + +import pytest + + +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only contract") +def test_posix_behavior() -> None: + assert True +``` + +Una prueba saltada no verifica el comportamiento. Demasiados skips pueden crear puntos ciegos. + +## 55. `xfail` registra un fallo esperado conocido + +```python +import pytest + + +@pytest.mark.xfail(reason="known parser limitation", strict=True) +def test_future_case() -> None: + assert False +``` + +Con `strict=True`, un pase inesperado falla la suite y obliga a notar que la limitación conocida puede haberse corregido. + +No uses `xfail` como estacionamiento permanente para pruebas rotas. + +## 56. Selecciona pruebas con `-k` + +`-k` filtra pruebas recopiladas mediante una expresión de nombre: + +```bash +python -m pytest -k "report and not slow" +``` + +Es cómodo durante desarrollo local, pero CI debería seguir ejecutando la suite completa pretendida o particiones documentadas explícitamente. + +## 57. Selecciona grupos marcados con `-m` + +```bash +python -m pytest -m "integration" +``` + +o: + +```bash +python -m pytest -m "not slow" +``` + +Las marks hacen explícitas las particiones cuando están registradas y mantenidas de forma consistente. + +## 58. Detén pronto con `-x` o `--maxfail` + +```bash +python -m pytest -x +``` + +se detiene tras el primer fallo. + +```bash +python -m pytest --maxfail=3 +``` + +se detiene después de tres fallos. + +Estas opciones aceleran feedback, pero no sustituyen ejecutar la suite completa antes de un release. + +## 59. Vuelve a ejecutar fallos anteriores con `--lf` + +```bash +python -m pytest --lf +``` + +pytest puede usar su caché para seleccionar pruebas que fallaron en la ejecución previa. + +Trátalo como acelerador local. Un job limpio de CI no debe depender del estado de la ejecución anterior de un desarrollador. + +## 60. La verbosidad cambia el informe, no la corrección + +Opciones comunes: + +```bash +python -m pytest -q +python -m pytest -v +``` + +La salida quiet puede ayudar en logs automatizados; la salida verbose ayuda a identificar casos parametrizados. + +El contrato de prueba no debe depender de la decoración del terminal. + +## 61. La configuración pertenece al control de versiones + +pytest soporta configuración en archivos como `pyproject.toml`, `pytest.ini` y otros documentados por el proyecto. + +Una configuración mínima en `pyproject.toml` podría ser: + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +strict_markers = true +``` + +La configuración debe volver la suite más predecible, no ocultar comportamiento que falla. + +## 62. `testpaths` limita la detección predeterminada + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +``` + +Cuando pytest se ejecuta sin rutas explícitas, esto indica dónde espera el proyecto encontrar pruebas. + +Si hay pruebas en otros lugares, configúralas o ejecútalas de forma intencional. + +## 63. Ten cuidado con `addopts` global + +Un proyecto puede configurar opciones predeterminadas, por ejemplo: + +```toml +[tool.pytest.ini_options] +addopts = "-ra" +``` + +Evita defaults que salten silenciosamente categorías importantes o supriman diagnósticos necesarios. + +## 64. Entiende el root que selecciona pytest + +pytest determina un directorio raíz y un contexto de configuración para la colección. + +Ejecutarlo desde un directorio inesperado puede cambiar qué configuración y qué archivos `conftest.py` son visibles. + +Al depurar problemas de descubrimiento, revisa el rootdir y el archivo de configuración reportados. + +## 65. Mantén imports predecibles + +Las pruebas siguen siendo módulos Python, por lo que las reglas de import importan. + +Un proyecto debería usar un layout deliberado y probar el código que pretende distribuir, sin depender de imports accidentales por el directorio actual. + +El layout `src/` puede ayudar a separar código instalado de rutas locales del repositorio. + +## 66. No des un `__init__` personalizado a clases de prueba de pytest + +Las clases de prueba se recopilan por convención y no deberían comportarse como objetos de aplicación que requieren argumentos de constructor. + +Usa fixtures para dependencias de pruebas en lugar de construcción personalizada de clases. + +Las funciones de prueba simples suelen ser el punto de partida más claro. + +## 67. Los valores de fixtures deben corresponder a su nombre + +Si una fixture se llama `authenticated_client`, debería proporcionar ese estado de forma confiable. + +Evita fixtures cuyo resultado cambie inesperadamente por configuraciones globales no relacionadas. Fixtures ambiguas convierten las pruebas en acertijos. + +## 68. Evita bosques de fixtures + +La composición de fixtures es potente, pero una prueba que depende de una fixture que depende de otras seis puede resultar difícil de entender. + +Si el setup se convierte en un laberinto, considera builders simples, helper functions o fronteras de integración más pequeñas. + +## 69. Las pruebas unitarias aíslan una pequeña unidad de comportamiento + +Una prueba unitaria normalmente ejercita una función, clase o pequeño componente con colaboradores controlados. + +Son valiosas para feedback rápido, pero la definición exacta de “unidad” depende de la arquitectura. + +No conviertas la etiqueta en una regla doctrinal. + +## 70. Las pruebas de integración cruzan fronteras reales + +Una prueba de integración puede ejercitar combinaciones como: + +```text +application code + database adapter +application code + local HTTP server +parser + real file format +repository + temporary filesystem +``` + +Estas pruebas validan contratos que los mocks no pueden demostrar por completo. + +## 71. Las pruebas end-to-end validan flujos mayores + +Las pruebas end-to-end recorren un camino amplio del sistema y pueden detectar problemas que las pruebas pequeñas no ven. + +También suelen ser más lentas, caras de diagnosticar y sensibles al estado del entorno. + +Una suite saludable suele combinar varias capas. + +## 72. Prueba comportamiento observable antes que detalles de implementación + +Si un refactor conserva el comportamiento público, las buenas pruebas normalmente siguen pasando. + +Las pruebas que verifican cada llamada a un helper privado encarecen innecesariamente la limpieza interna. + +Las assertions de interacción son apropiadas cuando la propia interacción forma parte del contrato, como “no envíes la solicitud dos veces”. + +## 73. Haz mock de servicios externos en la frontera correcta + +Una prueba unitaria no debe llamar a una API pública real. + +Para código HTTP, estrategias útiles incluyen: + +- parchear tu propia abstracción de cliente; +- usar un servidor de prueba local; +- usar un plugin específico de testing HTTP cuando el proyecto lo adopte. + +Las pruebas no deberían depender de internet pública salvo que sean pruebas deliberadas de sistema externo. + +## 74. Mantén secretos fuera de los datos de prueba + +Nunca pongas tokens, contraseñas, cookies, URLs privadas ni datos personales reales en pruebas. + +Usa valores ficticios como: + +```python +fake_token = "test-token-not-a-secret" +``` + +Las fixtures suelen terminar en logs e informes de fallo, por lo que merecen la misma disciplina de privacidad que el código de producción. + +## 75. Controla el tiempo en vez de competir con el reloj + +Evita pruebas como: + +```python +import time + + +def test_waits() -> None: + time.sleep(2) + assert True +``` + +Si el comportamiento depende del tiempo, inyecta un clock o parchea la fuente estrecha de tiempo que usa el código. + +Dormir vuelve lenta la suite y no garantiza que el estado asíncrono esté listo. + +## 76. Controla la aleatoriedad + +Para código aleatorio, algunas opciones son: + +- inyectar un generador de números aleatorios; +- usar una seed conocida cuando el contrato lo permita; +- probar invariantes con entradas controladas. + +Una prueba que falla solo en algunas ejecuciones aleatorias es difícil de reproducir y diagnosticar. + +## 77. Las pruebas no deben depender del orden de ejecución + +Una prueba no debería necesitar que otra se ejecute primero. + +El estado mutable compartido a nivel de módulo o sesión es una causa habitual de dependencia del orden. + +Si las pruebas fallan solo cuando cambia el orden, la suite ha revelado un problema real de aislamiento. + +## 78. Una prueba flaky es un defecto de confiabilidad + +Una prueba flaky alterna entre pasar y fallar sin un cambio relevante de código. + +Causas comunes: + +- carreras de timing; +- servicios externos; +- estado compartido; +- orden no determinista; +- limpieza insuficiente; +- agotamiento de recursos. + +Repetir hasta que quede verde oculta la señal en vez de repararla. + +## 79. Coverage y corrección son métricas distintas + +La cobertura de código puede revelar código que las pruebas nunca ejecutan. + +No demuestra que las assertions sean significativas, que las fronteras estén representadas o que los requisitos sean correctos. + +Trata coverage como evidencia de ejecución, no como sustituto del diseño de pruebas. + +## 80. Los plugins extienden pytest + +pytest tiene un gran ecosistema de plugins para coverage, código asíncrono, frameworks, ejecución paralela y pruebas HTTP. + +Los plugins también son dependencias. Limita versiones importantes, revisa compatibilidad y evita añadir un plugin cuando pytest core ya resuelve claramente el problema. + +## 81. pytest core no hace que cualquier `async def` funcione automáticamente + +Las funciones de prueba asíncronas suelen requerir un plugin o integración de framework apropiado. + +No supongas que instalar pytest define por sí solo la política de event loop que necesita la aplicación. + +El plugin pasa a formar parte del contrato de dependencias de testing. + +## 82. `required_plugins` puede exigir la presencia de plugins + +La configuración de pytest puede declarar plugins obligatorios para que la ejecución falle pronto si falta uno. + +Esto resulta útil cuando la suite de otro modo podría recopilarse mal o fallar después con errores confusos de fixtures ausentes. + +Usa requisitos reales del proyecto, no listas copiadas de otros repositorios. + +## 83. pytest puede ejecutar muchas pruebas estilo `unittest` + +Adoptar pytest no exige necesariamente reescribir inmediatamente una suite existente de `unittest`. + +pytest soporta muchas pruebas escritas con `unittest.TestCase` y permite una migración gradual. + +La migración debería mejorar mantenimiento, no crear churn por sí misma. + +## 84. Trata los códigos de salida como contratos de CI + +Un sistema CI debe fallar cuando el runner de pruebas reporta fallo. + +No escribas wrappers de shell que descarten el exit status de pytest. + +Los ejemplos ejecutables de este capítulo convierten el código de salida programático en entero solo para mostrar el resultado de forma determinista. + +## 85. Ejemplo ejecutable: assertions y parametrización + +[`examples/assertions_and_parametrize.py`](examples/assertions_and_parametrize.py) crea un módulo pytest temporal, lo ejecuta con el runner real y muestra solo un resumen determinista. + +Salida esperada: + +```text +exit code: 0 +passed: 4 +``` + +La suite temporal demuestra una assertion normal y tres casos de frontera parametrizados. + +## 86. Ejemplo ejecutable: fixtures y `tmp_path` + +[`examples/fixtures_and_tmp_path.py`](examples/fixtures_and_tmp_path.py) demuestra una fixture que depende de la fixture integrada `tmp_path`. + +Salida esperada: + +```text +exit code: 0 +passed: 2 +``` + +Cada prueba recibe su propia invocación de fixture y frontera temporal de filesystem. + +## 87. Ejemplo ejecutable: `monkeypatch` + +[`examples/monkeypatch_environment.py`](examples/monkeypatch_environment.py) controla una variable de entorno sin dejar estado global del proceso. + +Salida esperada: + +```text +exit code: 0 +passed: 2 +``` + +El ejemplo verifica tanto el estado fallback como un estado configurado explícitamente. + +## 88. Ejemplo ejecutable: excepciones y warnings + +[`examples/exceptions_and_warnings.py`](examples/exceptions_and_warnings.py) usa `pytest.raises` y `pytest.warns` para hacer explícito el comportamiento de fallo y migración. + +Salida esperada: + +```text +exit code: 0 +passed: 2 +``` + +Se verifican tipo/mensaje de excepción y categoría/mensaje del warning. + +## 89. Ejemplo ejecutable: captura de salida y logs + +[`examples/capture_output_and_logs.py`](examples/capture_output_and_logs.py) demuestra `capsys` y `caplog`. + +Salida esperada: + +```text +exit code: 0 +passed: 2 +``` + +La suite valida tanto salida de línea de comandos como un mensaje dirigido de logger. + +## 90. Errores comunes + +### Error 1: tratar una suite verde como prueba de ausencia de bugs + +Las pruebas solo cubren comportamientos e entradas que realmente ejercitan. + +### Error 2: probar detalles triviales de implementación + +Las pruebas demasiado acopladas encarecen refactors inofensivos. + +### Error 3: compartir estado mutable entre pruebas + +Esto genera dependencia de orden y flakiness. + +### Error 4: llamar a servicios públicos desde pruebas unitarias + +La disponibilidad de red y los datos remotos vuelven la suite no determinista. + +### Error 5: ocultar todos los warnings + +Los warnings suelen revelar migraciones que el proyecto debe realizar. + +### Error 6: abusar de mocks + +Una suite de mocks puede demostrar que los mocks se comportan exactamente como se configuraron y aun así perder errores reales de integración. + +### Error 7: crear fixtures gigantes + +Grafos enormes de setup ocultan lo que cada prueba realmente necesita. + +### Error 8: aceptar reruns flaky como normales + +Una prueba flaky es un defecto del sistema de feedback. + +## 91. Tabla de decisión + +| Necesidad | Herramienta útil | Principal cuidado | +| --- | --- | --- | +| Comparar valores normales | `assert` | mantén explícito el contrato esperado | +| Comparar floats | `pytest.approx()` | elige tolerancia apropiada al dominio | +| Esperar una excepción | `pytest.raises()` | no captures fallos no relacionados | +| Esperar un warning | `pytest.warns()` | prueba categoría/mensaje deliberadamente | +| Repetir una regla en varios casos | `@pytest.mark.parametrize` | elige datos de frontera significativos | +| Reutilizar setup | fixture | evita grafos ocultos y gigantes | +| Archivos temporales | `tmp_path` | no dependas de artefactos del repositorio | +| Cambios temporales de entorno | `monkeypatch` | parchea donde el código busca el nombre | +| Capturar stdout/stderr | `capsys` | verifica solo salida que forme parte del contrato | +| Capturar logs | `caplog` | evita alterar handlers del root logger | +| Clasificar pruebas | marks registradas | evita errores silenciosos de escritura | +| Fallo esperado conocido | `xfail` | prefiere strict y elimina cuando se corrija | + +## 92. Referencia rápida + +```bash +python -m pytest +python -m pytest -q +python -m pytest -v +python -m pytest --collect-only +python -m pytest -k "name_expression" +python -m pytest -m "marker_expression" +python -m pytest -x +python -m pytest --maxfail=3 +python -m pytest --lf +python -m pytest --max-warnings=10 +``` + +Patrones principales en Python: + +```python +assert actual == expected + +with pytest.raises(ValueError, match="message"): + operation() + +with pytest.warns(DeprecationWarning): + old_operation() +``` + +## 93. Checklist de revisión + +Antes de considerar confiable una suite, pregunta: + +- ¿Las pruebas previstas realmente se recopilan? +- ¿Los nombres explican comportamientos? +- ¿Las assertions son específicas para fallar por la razón correcta? +- ¿Están representados casos de frontera y error? +- ¿Los archivos se escriben en rutas temporales? +- ¿Los cambios de entorno se restauran automáticamente? +- ¿Red, reloj y aleatoriedad están controlados? +- ¿Las pruebas pueden ejecutarse independientemente y en cualquier orden? +- ¿Los warnings son visibles e intencionales? +- ¿Las custom marks están registradas? +- ¿Los fallos esperados se revisan y son temporales? +- ¿CI conserva el exit status de fallo de pytest? +- ¿Datos y logs de prueba están libres de secretos y datos personales? + +## 94. Ejercicio práctico + +Crea un pequeño paquete ficticio que valide registros de sesiones de estudio. + +Requisitos: + +1. Crea una función que reciba un tema y una duración en minutos. +2. Rechaza un tema vacío con `ValueError`. +3. Rechaza duración cero o negativa con `ValueError`. +4. Devuelve un diccionario normalizado para entrada válida. +5. Escribe pruebas de éxito con `assert` simple. +6. Parametriza al menos tres duraciones inválidas. +7. Usa `pytest.raises(..., match=...)` para un error de validación. +8. Escribe una fixture con datos válidos de ejemplo. +9. Añade una función que guarde una sesión como texto o JSON y pruébala con `tmp_path`. +10. Añade una función que lea una configuración de una variable de entorno y pruébala con `monkeypatch`. +11. Añade una función estilo CLI y valida su salida con `capsys`. +12. Añade un logger y valida con `caplog`. +13. Registra una custom marker para pruebas de integración. +14. Ejecuta `python -m pytest --collect-only` y confirma los casos esperados. +15. Ejecuta la suite completa desde un proceso limpio. + +Desafíos de extensión: + +- mueve fixtures compartidas a un `conftest.py` cuidadosamente limitado; +- añade un warning para una forma de entrada deprecada y pruébalo con `pytest.warns`; +- usa `pytest.approx` para una razón calculada con tolerancia documentada; +- construye una prueba de integración HTTP local usando conceptos del capítulo anterior de `requests`; +- añade CI que instale las dependencias declaradas y ejecute la suite desde cero. + +## 95. Conexiones con conceptos anteriores + +`pytest` conecta casi todas las fases anteriores: + +- **funciones:** prueba comportamiento mediante entradas y salidas explícitas; +- **colecciones:** construye tablas de parámetros y valores esperados estructurados; +- **flujo de programa:** ejercita ramas y condiciones de frontera; +- **excepciones:** valida contratos de fallo deliberado; +- **archivos:** aísla filesystem con rutas temporales; +- **módulos y paquetes:** organiza código e imports de forma predecible; +- **`pathlib`:** trabaja naturalmente con `tmp_path`; +- **`datetime`:** inyecta o parchea límites temporales en lugar de competir con el reloj real; +- **logging:** valida señales operativas con `caplog`; +- **`decimal`:** prueba reglas monetarias exactas sin tolerancia de float inapropiada; +- **pandas/openpyxl/requests:** convierte comportamiento de bibliotecas externas en regresiones repetibles. + +## 96. Referencias primarias + +- [pytest documentation](https://docs.pytest.org/) +- [Get Started](https://docs.pytest.org/en/stable/getting-started.html) +- [How to invoke pytest](https://docs.pytest.org/en/stable/how-to/usage.html) +- [Assertions](https://docs.pytest.org/en/stable/how-to/assert.html) +- [Fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html) +- [Parametrization](https://docs.pytest.org/en/stable/how-to/parametrize.html) +- [Temporary directories](https://docs.pytest.org/en/stable/how-to/tmp_path.html) +- [Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) +- [Logging](https://docs.pytest.org/en/stable/how-to/logging.html) +- [Warnings](https://docs.pytest.org/en/stable/how-to/capture-warnings.html) +- [Skip and xfail](https://docs.pytest.org/en/stable/how-to/skipping.html) +- [API reference](https://docs.pytest.org/en/stable/reference/reference.html) +- [pytest changelog](https://docs.pytest.org/en/stable/changelog.html) +- [pytest on PyPI](https://pypi.org/project/pytest/) + +Cuando se preparó este capítulo, PyPI listaba pytest 9.1.1 como la release estable más reciente. El currículo apunta a la serie 9.1.x en lugar del draft no publicado 9.2 o de una versión futura sin límite. + +## 97. Fase 9 completada + +La Fase 9 conecta ahora cuatro fronteras importantes de terceros: + +```text +pandas -> transform tabular data +openpyxl -> construct and maintain Excel workbooks +requests -> communicate with HTTP services +pytest -> verify behavior repeatedly and automatically +``` + +Esto cierra la **Fase 9: Bibliotecas Externas**. + +La siguiente fase deja las habilidades aisladas de bibliotecas y pasa al trabajo integrado de portafolio: **Fase 10: Proyectos Prácticos**. + +Antes de continuar, practica escribiendo pruebas que hagan informativos los fallos. Una suite es más valiosa cuando te da confianza para cambiar el código y no cuando solamente produce un número verde. diff --git a/external-libraries/04-pytest/README.md b/external-libraries/04-pytest/README.md new file mode 100644 index 0000000..e4f9b0b --- /dev/null +++ b/external-libraries/04-pytest/README.md @@ -0,0 +1,1279 @@ +
+ +# Engineering Automated Tests with `pytest` + +[🇺🇸 English](README.md) · [🇧🇷 Português](README.pt-BR.md) · [🇪🇸 Español](README.es.md) + +
+ +[← Back to External Libraries](../README.md) · [← Previous: `requests`](../03-requests/README.md) + +Software becomes easier to change when expected behavior can be checked repeatedly and automatically. `pytest` provides a concise testing model built around normal Python functions, plain `assert` statements, reusable fixtures, parametrization, rich failure reports, and an extensible plugin system. + +This chapter targets **pytest 9.1.x** and was researched against the current stable **pytest 9.1.1** documentation and release metadata. pytest 9.1.1 requires Python 3.10 or newer; this repository validates examples on Python 3.13. + +**Estimated study time:** 300–390 minutes. + +## Learning goals + +By the end of this chapter, you should be able to: + +- explain what an automated test proves and what it does not prove; +- organize tests so pytest can discover them predictably; +- write readable assertions and interpret assertion introspection; +- test floating-point values, exceptions, and warnings deliberately; +- reduce duplication with parametrization; +- model setup, teardown, and dependencies with fixtures; +- isolate filesystem and environment state with `tmp_path` and `monkeypatch`; +- capture standard output and logs with `capsys` and `caplog`; +- use marks, skips, expected failures, and test selection intentionally; +- configure pytest without hiding warnings or accidental test omissions; +- distinguish unit, integration, and end-to-end boundaries; +- avoid flaky tests caused by time, randomness, network access, shared state, or ordering assumptions; +- integrate pytest into CI as an executable quality contract. + +## 1. Why automated tests exist + +A manual check answers a question once. An automated test turns that question into executable code that can be repeated after future changes. + +A useful test describes a behavior, provides controlled inputs, observes an output or side effect, and fails when the observed behavior violates the expected contract. + +Tests reduce uncertainty. They do not prove that software has no bugs. + +## 2. What `pytest` adds + +Python includes `unittest` in the standard library. `pytest` is an external test framework that can run plain test functions while adding features such as: + +- assertion introspection; +- fixtures; +- parametrization; +- marks and test selection; +- temporary paths; +- environment monkeypatching; +- output, warning, and log capture; +- plugins and hooks. + +The goal is not to make tests clever. The goal is to make intent visible and repetition cheap. + +## 3. External libraries need a version contract + +This repository declares Phase 9 dependencies in `requirements-external.txt`. + +For this chapter the contract is: + +```text +pytest >= 9.1 and < 9.2 +``` + +The upper bound matters because the pytest changelog already contains an unreleased 9.2 draft with backward-incompatible changes. A published curriculum should describe released behavior rather than silently following a future version. + +## 4. Install the repository dependency set + +Create and activate a virtual environment, then install: + +```bash +python -m pip install -r requirements-external.txt +``` + +For isolated experimentation: + +```bash +python -m pip install pytest +``` + +A project should still record which pytest range it supports. + +## 5. Prefer `python -m pytest` when interpreter identity matters + +A common invocation is: + +```bash +python -m pytest +``` + +Using `python -m` makes the interpreter explicit. This is especially useful when several Python installations or virtual environments exist on the same machine. + +The `pytest` console command is also valid when the environment is unambiguous. + +## 6. A test is executable specification, not production code + +Consider a small function: + +```python +def calculate_total(unit_price: int, quantity: int) -> int: + return unit_price * quantity +``` + +A test can state one expected behavior: + +```python +def test_calculate_total_multiplies_price_by_quantity() -> None: + assert calculate_total(12, 3) == 36 +``` + +The test name communicates the contract before the assertion is even read. + +## 7. pytest discovers tests by convention + +By default, pytest discovers test modules and test functions according to naming conventions. + +A common layout is: + +```text +project/ +├── src/ +│ └── calculator.py +└── tests/ + └── test_calculator.py +``` + +Inside `test_calculator.py`, functions named `test_*` are collected as tests. + +## 8. Collection is a phase of the test run + +Before executing tests, pytest first discovers and collects them. + +You can inspect collection without executing tests: + +```bash +python -m pytest --collect-only +``` + +This is useful when a test you expected to run is missing. + +A green suite that accidentally collected the wrong tests is not a reliable signal. + +## 9. Keep test names behavioral + +Prefer names that explain an observable rule: + +```python +def test_discount_is_zero_for_empty_cart() -> None: + ... +``` + +Avoid names that only mirror implementation details: + +```python +def test_function_2() -> None: + ... +``` + +Good names make failures easier to triage in CI. + +## 10. Plain `assert` is the normal pytest assertion style + +```python +def test_status_is_ready() -> None: + status = "ready" + assert status == "ready" +``` + +pytest rewrites assertions during collection so failing expressions can produce richer diagnostics than a bare Python `AssertionError` normally provides. + +## 11. Assertion introspection helps explain failures + +A comparison such as: + +```python +def test_summary() -> None: + actual = {"count": 2, "status": "ready"} + expected = {"count": 3, "status": "ready"} + assert actual == expected +``` + +can show the differing values when it fails. + +This is one reason to prefer direct expressions over manually constructing vague failure messages everywhere. + +## 12. Add a message only when it adds domain context + +```python +def test_inventory_never_becomes_negative() -> None: + remaining = 4 + assert remaining >= 0, "inventory contract requires a non-negative balance" +``` + +The message should explain why the condition matters, not merely restate `remaining >= 0`. + +## 13. Use Arrange, Act, Assert when it clarifies the test + +A readable test often has three conceptual stages: + +```python +def test_normalize_name_removes_outer_whitespace() -> None: + raw_name = " Nova " + + normalized = raw_name.strip() + + assert normalized == "Nova" +``` + +Not every tiny test needs comments naming the stages. The structure itself can make them obvious. + +## 14. Test one coherent behavior + +A test may contain several assertions when they describe one result, but avoid turning a single test into a tour of unrelated behaviors. + +Smaller behavioral tests make failures more local and easier to diagnose. + +## 15. Deterministic tests are repeatable + +A deterministic test gives the same result when the relevant code and inputs have not changed. + +Common threats include: + +- current time; +- random values without control; +- network services; +- shared files or databases; +- environment variables; +- locale and timezone; +- dependency on test execution order. + +Isolation is a design skill, not just a test-framework feature. + +## 16. Compare floating-point results with an explicit tolerance + +Binary floating-point values are often unsuitable for exact equality after calculations. + +pytest provides `approx()`: + +```python +import pytest + + +def test_ratio() -> None: + result = 1 / 3 + assert result == pytest.approx(0.333333, rel=1e-5) +``` + +Choose tolerances according to the domain rather than copying arbitrary values. + +## 17. Exact values should still use exact assertions + +Do not reach for `pytest.approx()` when the contract is exact. + +```python +def test_item_count() -> None: + assert len(["a", "b", "c"]) == 3 +``` + +Testing tools should make contracts clearer, not blur them. + +## 18. Test expected exceptions with `pytest.raises` + +```python +import pytest + + +def parse_positive(value: str) -> int: + number = int(value) + if number <= 0: + raise ValueError("value must be positive") + return number + + +def test_zero_is_rejected() -> None: + with pytest.raises(ValueError): + parse_positive("0") +``` + +The test passes only if the expected exception type is raised inside the context manager. + +## 19. Match exception messages when the message is part of the contract + +```python +def test_zero_has_clear_message() -> None: + with pytest.raises(ValueError, match="must be positive"): + parse_positive("0") +``` + +`match` is interpreted as a regular expression. Escape special characters when you intend a literal match. + +## 20. Inspect captured exception information when necessary + +```python +def test_invalid_value_context() -> None: + with pytest.raises(ValueError) as exc_info: + parse_positive("-4") + + assert "positive" in str(exc_info.value) +``` + +Do this when the extra detail matters. Do not inspect internals merely because pytest exposes them. + +## 21. Test warnings explicitly with `pytest.warns` + +```python +import warnings + +import pytest + + +def old_api() -> None: + warnings.warn("old API", DeprecationWarning, stacklevel=2) + + +def test_old_api_warns() -> None: + with pytest.warns(DeprecationWarning, match="old API"): + old_api() +``` + +Warnings can represent migration contracts that deserve tests of their own. + +## 22. pytest 9.1 can enforce a warning budget + +pytest 9.1 added `--max-warnings`. + +For example: + +```bash +python -m pytest --max-warnings=10 +``` + +If all tests pass but the unfiltered warning count exceeds the threshold, pytest reports a dedicated non-zero exit status. + +A warning budget can help a project reduce warning debt gradually instead of suppressing everything. + +## 23. Parametrization turns data variation into test cases + +When the same behavior should hold for many inputs, use `@pytest.mark.parametrize`: + +```python +import pytest + + +@pytest.mark.parametrize( + ("raw", "expected"), + [(-5, 0), (40, 40), (130, 100)], +) +def test_normalize_score(raw: int, expected: int) -> None: + result = max(0, min(raw, 100)) + assert result == expected +``` + +pytest creates a separate collected case for each parameter set. + +## 24. Separate test logic from test data + +Parametrization works best when the test body expresses one rule and the data describes interesting cases. + +Include meaningful boundaries, not just many random examples. + +A ten-row parameter table is not automatically better than three carefully chosen boundary cases. + +## 25. Give parameter cases useful IDs when reports need them + +```python +@pytest.mark.parametrize( + ("value", "expected"), + [(0, "empty"), (1, "single"), (2, "many")], + ids=["zero", "one", "multiple"], +) +def test_classification(value: int, expected: str) -> None: + result = "empty" if value == 0 else "single" if value == 1 else "many" + assert result == expected +``` + +Readable IDs improve failure reports for complex parameter values. + +## 26. Use `pytest.param` for per-case metadata + +```python +@pytest.mark.parametrize( + "value", + [ + 1, + pytest.param(-1, marks=pytest.mark.xfail(reason="known limitation")), + ], +) +def test_positive_only(value: int) -> None: + assert value > 0 +``` + +Per-case marks can keep exceptional cases visible without duplicating the whole test. + +## 27. pytest 9.1 deprecates non-collection iterables for parametrization + +Current pytest documentation deprecates passing a non-`Collection` iterable such as a generator directly as `argvalues`. + +Prefer a concrete list or tuple for published tests: + +```python +cases = [(1, 2), (2, 4), (3, 6)] +``` + +This also makes the test data easier to inspect and review. + +## 28. Fixtures model test dependencies + +A fixture is a value or resource that pytest provides to a test by name. + +```python +import pytest + + +@pytest.fixture +def sample_user() -> dict[str, str]: + return {"name": "Nova", "role": "reader"} + + +def test_user_role(sample_user: dict[str, str]) -> None: + assert sample_user["role"] == "reader" +``` + +The test requests the fixture by declaring a parameter with the fixture name. + +## 29. Fixtures can return objects + +Fixtures can return plain values, data structures, configured clients, temporary repositories, database connections, or other resources. + +Keep fixtures focused. A giant fixture that prepares the entire application can hide dependencies instead of clarifying them. + +## 30. Fixtures can perform teardown with `yield` + +```python +import pytest + + +@pytest.fixture +def opened_resource(): + resource = {"open": True} + yield resource + resource["open"] = False +``` + +Code before `yield` performs setup. Code after `yield` performs teardown when pytest finalizes the fixture. + +Use actual context managers when the production resource already provides one. + +## 31. Fixture scope controls lifetime + +Common fixture scopes are: + +```text +function -> one test invocation +class -> one test class +module -> one test module +package -> one test package +session -> the whole pytest session +``` + +The default is `function` scope. + +## 32. Wider fixture scope trades isolation for reuse + +A session-scoped resource may be faster to create once, but it also lives longer and can carry shared mutable state between tests. + +Do not increase fixture scope only to make a suite faster. First understand whether the shared lifetime preserves test independence. + +## 33. Fixtures can depend on other fixtures + +```python +import pytest + + +@pytest.fixture +def base_url() -> str: + return "https://example.invalid" + + +@pytest.fixture +def endpoint(base_url: str) -> str: + return f"{base_url}/items" +``` + +Dependency composition is often cleaner than one fixture that knows every setup detail. + +## 34. Avoid hidden dependencies with excessive `autouse` + +An `autouse=True` fixture runs without appearing in every test signature. + +This can be useful for a true suite-wide invariant, but widespread autouse fixtures make behavior harder to trace. + +Prefer explicit fixture parameters unless automatic application is genuinely part of the test environment contract. + +## 35. `conftest.py` shares local pytest configuration and fixtures + +A common structure is: + +```text +tests/ +├── conftest.py +├── test_api.py +└── test_reports.py +``` + +Fixtures defined in `tests/conftest.py` can be discovered by tests below that directory without importing `conftest` directly. + +## 36. `conftest.py` follows directory visibility rules + +For a given test, pytest searches relevant `conftest.py` files in that test's directory and parent directories. + +This makes fixture visibility hierarchical. + +Place shared fixtures at the narrowest directory level that needs them rather than automatically putting everything at the test root. + +## 37. Do not import from `conftest.py` + +Treat `conftest.py` as pytest configuration, not as an application module. + +If helpers need normal Python imports, put them in a regular module or package and import that module from tests and fixtures. + +## 38. `tmp_path` gives each test a temporary `Path` + +```python +from pathlib import Path + + +def test_export(tmp_path: Path) -> None: + report = tmp_path / "report.txt" + report.write_text("ready\n", encoding="utf-8") + + assert report.read_text(encoding="utf-8") == "ready\n" +``` + +`tmp_path` is a `pathlib.Path` unique to the test invocation. + +This avoids polluting the repository with test artifacts. + +## 39. `tmp_path_factory` is useful for wider fixture scopes + +A session- or module-scoped fixture cannot depend on a function-scoped `tmp_path`. + +For broader temporary-resource lifetimes, pytest provides `tmp_path_factory`. + +Choose broader lifetime only when it is part of the test design. + +## 40. `monkeypatch` changes state and restores it automatically + +The `monkeypatch` fixture can temporarily modify: + +- object attributes; +- dictionary items; +- environment variables; +- `sys.path`; +- current working directory. + +Its changes are undone after the requesting test or fixture finishes. + +## 41. Patch environment variables with `setenv` and `delenv` + +```python +import os + + +def read_mode() -> str: + return os.getenv("STUDY_MODE", "default") + + +def test_configured_mode(monkeypatch) -> None: + monkeypatch.setenv("STUDY_MODE", "focused") + assert read_mode() == "focused" +``` + +Environment-dependent code becomes deterministic when the test controls the environment explicitly. + +## 42. Patch where the code looks up the dependency + +Suppose `service.py` contains: + +```python +from client import fetch_status + + +def is_ready() -> bool: + return fetch_status() == "ready" +``` + +A test usually needs to patch `service.fetch_status`, because that is the name `is_ready()` resolves at runtime. + +Patching the original definition in `client` may not replace an already imported reference in `service`. + +## 43. `monkeypatch.context()` can limit patch lifetime further + +When a test needs a patch only for a small block, `monkeypatch.context()` provides a nested context whose changes are undone when that block exits. + +Smaller patch lifetimes reduce surprising interactions inside complex tests. + +## 44. Use test doubles to replace boundaries, not everything + +A test double may stand in for a slow, nondeterministic, destructive, or unavailable collaborator. + +Common informal categories include: + +```text +stub -> returns controlled values +fake -> lightweight working implementation +spy -> records how it was used +mock -> verifies expected interactions +``` + +The vocabulary is less important than making the replacement's purpose clear. + +## 45. The standard library `unittest.mock` works with pytest + +pytest does not require a separate mocking style. + +You can combine pytest assertions and fixtures with `unittest.mock.Mock`, `MagicMock`, or `patch` when those tools fit the test. + +Do not mock pure calculations just because mocking is available. + +## 46. `capsys` captures Python-level stdout and stderr + +```python +def announce(topic: str) -> None: + print(f"Studying: {topic}") + + +def test_announce(capsys) -> None: + announce("pytest") + captured = capsys.readouterr() + assert captured.out == "Studying: pytest\n" + assert captured.err == "" +``` + +This is useful for command-line interfaces and functions whose output stream is part of the contract. + +## 47. `capfd` captures at the file-descriptor level + +`capsys` focuses on Python's `sys.stdout` and `sys.stderr` objects. + +`capfd` captures file descriptors 1 and 2, which can be useful when output comes from lower-level code or subprocess-adjacent components that bypass normal Python stream objects. + +Use the narrowest capture mechanism that matches the behavior being tested. + +## 48. `caplog` captures logging records + +```python +import logging + + +def test_log_message(caplog) -> None: + logger = logging.getLogger("study") + + with caplog.at_level(logging.INFO, logger="study"): + logger.info("session ready") + + assert "session ready" in caplog.text +``` + +Tests can also inspect structured log records instead of only matching rendered text. + +## 49. Be careful when reconfiguring the root logger during `caplog` + +pytest documentation warns that changing root logger handlers during a test can interfere with log capture. + +Prefer targeted logger configuration and avoid replacing the whole root-handler set unless the test specifically validates logging configuration. + +## 50. Marks attach metadata to tests + +Marks can classify or alter test behavior. + +```python +import pytest + + +@pytest.mark.slow +def test_large_report() -> None: + assert True +``` + +Custom marks should describe useful test categories, not become a substitute for clear test organization. + +## 51. Register custom marks + +Unregistered custom marks can produce warnings and spelling mistakes can silently create unintended categories. + +A `pyproject.toml` can register them: + +```toml +[tool.pytest.ini_options] +markers = [ + "slow: tests that intentionally take longer", + "integration: tests that cross component boundaries", +] +``` + +Registration turns mark names into a documented project contract. + +## 52. `strict_markers` can turn unknown marks into errors + +```toml +[tool.pytest.ini_options] +strict_markers = true +``` + +This is useful when a misspelled marker should fail immediately instead of being treated as a new marker. + +## 53. pytest 9 introduced broader strict mode + +pytest 9 provides a `strict` configuration option that enables several strictness checks together, currently including strict configuration, markers, xfail behavior, and parametrization IDs. + +The documentation cautions that future pytest versions may add more strictness options. Use global strict mode with a controlled pytest version or when the project deliberately wants to adopt new strict checks. + +## 54. Skip tests only for a real environmental reason + +```python +import sys + +import pytest + + +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only contract") +def test_posix_behavior() -> None: + assert True +``` + +A skipped test does not verify the behavior. Too many skips can create blind spots. + +## 55. `xfail` records a known expected failure + +```python +import pytest + + +@pytest.mark.xfail(reason="known parser limitation", strict=True) +def test_future_case() -> None: + assert False +``` + +With `strict=True`, an unexpected pass fails the suite, forcing the team to notice that the known limitation may have been fixed. + +Do not use `xfail` as a permanent parking lot for broken tests. + +## 56. Select tests with `-k` + +`-k` filters collected tests by name expression: + +```bash +python -m pytest -k "report and not slow" +``` + +This is convenient during local development, but CI should still run the intended complete suite or explicitly documented partitions. + +## 57. Select marked groups with `-m` + +```bash +python -m pytest -m "integration" +``` + +or: + +```bash +python -m pytest -m "not slow" +``` + +Marks make suite partitions explicit when they are registered and maintained consistently. + +## 58. Stop early with `-x` or `--maxfail` + +```bash +python -m pytest -x +``` + +stops after the first failure. + +```bash +python -m pytest --maxfail=3 +``` + +stops after three failures. + +These options are useful for feedback speed. They do not replace running the full suite before release. + +## 59. Re-run previous failures with `--lf` + +```bash +python -m pytest --lf +``` + +pytest can use its cache to select tests that failed in the previous run. + +Treat this as a local development accelerator. A clean CI job must not depend on state from a developer's previous run. + +## 60. Verbosity changes reporting, not correctness + +Common options include: + +```bash +python -m pytest -q +python -m pytest -v +``` + +Quiet output can be useful in automated logs; verbose output can help identify individual parameter cases. + +The test contract should not depend on terminal decoration. + +## 61. Configuration belongs in version control + +pytest supports project configuration through supported config files such as `pyproject.toml`, `pytest.ini`, and others documented by the project. + +A minimal `pyproject.toml` configuration might be: + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +strict_markers = true +``` + +Configuration should make the suite more predictable, not hide failing behavior. + +## 62. `testpaths` narrows default discovery + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +``` + +When pytest is invoked without explicit test paths, this tells it where the project expects tests to live. + +If tests also live elsewhere, configure or invoke them intentionally. + +## 63. Be cautious with global `addopts` + +A project may configure default command-line options, for example: + +```toml +[tool.pytest.ini_options] +addopts = "-ra" +``` + +Avoid defaults that quietly skip important test categories or suppress diagnostics developers need to see. + +## 64. Understand the project root pytest selects + +pytest determines a root directory and configuration context for collection. + +Running pytest from an unexpected directory can change which configuration and `conftest.py` files are visible. + +When debugging discovery problems, inspect the reported rootdir and configuration file. + +## 65. Keep imports predictable + +Tests are still Python modules, so import rules matter. + +A project should use a deliberate package layout and test against the code it intends to ship, rather than relying on accidental working-directory imports. + +The common `src/` layout can help distinguish installed application code from repository-local paths. + +## 66. Do not give pytest test classes a custom `__init__` + +pytest test classes are collected by convention and should not behave like application objects requiring constructor arguments. + +Use fixtures for test dependencies instead of custom test-class construction. + +Plain test functions are often the simplest starting point. + +## 67. Fixture values should match their declared meaning + +If a fixture is named `authenticated_client`, it should reliably provide that state. + +Avoid fixtures whose result changes unexpectedly according to unrelated global settings. Ambiguous fixtures make tests read like riddles. + +## 68. Avoid fixture forests + +Fixture composition is powerful, but a test that depends on one fixture that depends on six more fixtures can become difficult to understand. + +If setup feels like a dependency maze, consider simpler builders, helper functions, or smaller integration boundaries. + +## 69. Unit tests isolate a small behavioral unit + +A unit test usually exercises a function, class, or small component with controlled collaborators. + +Unit tests are valuable for fast feedback, but the exact definition of “unit” depends on architecture. + +Do not turn the label into a doctrinal rule. + +## 70. Integration tests cross real component boundaries + +An integration test may exercise combinations such as: + +```text +application code + database adapter +application code + local HTTP server +parser + real file format +repository + temporary filesystem +``` + +Integration tests intentionally validate contracts that mocks cannot fully prove. + +## 71. End-to-end tests validate larger workflows + +End-to-end tests exercise a broad path through the system and can detect integration problems that smaller tests miss. + +They are also usually slower, more expensive to diagnose, and more sensitive to environment state. + +A healthy suite commonly uses several layers rather than one test type for everything. + +## 72. Test observable behavior before implementation details + +If a refactor preserves the public behavior, good tests should usually keep passing. + +Tests that assert every private helper call often make internal cleanup unnecessarily expensive. + +Interaction assertions are appropriate when the interaction itself is part of the contract, such as “do not send the request twice.” + +## 73. Mock external services at the correct boundary + +A unit test should not make a live request to a public API. + +For HTTP code, useful strategies include: + +- patching your own client abstraction; +- using a local test server; +- using a purpose-built HTTP test plugin when the project adopts one. + +Tests should not depend on public network availability unless they are deliberately external-system tests. + +## 74. Keep secrets out of test data + +Never place real tokens, passwords, cookies, private URLs, or personal data in tests. + +Use fictional values such as: + +```python +fake_token = "test-token-not-a-secret" +``` + +Test fixtures often end up copied into logs and failure reports, so they deserve the same privacy discipline as production code. + +## 75. Control time instead of racing the clock + +Avoid tests like: + +```python +import time + + +def test_waits() -> None: + time.sleep(2) + assert True +``` + +If behavior depends on time, inject a clock or patch the narrow time source the code uses. + +Sleeping makes suites slow and does not guarantee that asynchronous state is ready. + +## 76. Control randomness + +For code that uses randomness, options include: + +- injecting a random-number generator; +- using a known seed when that contract is appropriate; +- testing invariants over controlled inputs. + +A test that fails only on some random runs is difficult to reproduce and diagnose. + +## 77. Tests should not depend on execution order + +A test should not require another test to run first. + +Shared mutable module or session state is a common cause of order dependence. + +If tests fail only when the suite order changes, the suite has exposed a real isolation problem. + +## 78. A flaky test is a reliability defect + +A flaky test alternates between pass and fail without a relevant code change. + +Common causes include: + +- timing races; +- external services; +- shared state; +- nondeterministic ordering; +- insufficient cleanup; +- resource exhaustion. + +Repeatedly rerunning until green hides the signal rather than repairing it. + +## 79. Coverage and correctness are different metrics + +Code coverage can reveal code that tests never execute. + +It cannot prove that assertions are meaningful, boundary cases are represented, or requirements are correct. + +Treat coverage as evidence about execution, not as a substitute for test design. + +## 80. Plugins extend pytest + +pytest has a large plugin ecosystem for domains such as coverage, asynchronous code, frameworks, parallel execution, and HTTP testing. + +Plugins are dependencies too. Pin or bound important plugin versions, review their compatibility, and avoid adding a plugin when core pytest already solves the problem clearly. + +## 81. Core pytest does not automatically make every `async def` test work + +Asynchronous test functions generally require an appropriate async testing plugin or framework integration. + +Do not assume that installing pytest alone defines the event-loop policy your application needs. + +The plugin becomes part of the test dependency contract. + +## 82. `required_plugins` can enforce plugin presence + +pytest configuration can declare required plugins so a run fails early when a necessary plugin is missing. + +This is useful when the suite would otherwise collect incorrectly or fail later with confusing missing-fixture errors. + +Use exact project requirements rather than copying plugin lists from unrelated repositories. + +## 83. pytest can run many `unittest`-style tests + +Adopting pytest does not necessarily require rewriting an existing `unittest` suite immediately. + +pytest supports running many tests written with `unittest.TestCase` while allowing gradual use of pytest features around the broader suite. + +Migration should improve maintainability, not create churn for its own sake. + +## 84. Treat test exit codes as CI contracts + +A CI system should fail when the test runner reports failure. + +Do not write shell wrappers that discard pytest's exit status. + +The repository's executable examples later in this chapter convert pytest's programmatic exit code to an integer only so the demonstration can report it deterministically. + +## 85. Executable example: assertions and parametrization + +[`examples/assertions_and_parametrize.py`](examples/assertions_and_parametrize.py) creates a temporary pytest module, runs it with the real pytest runner, and reports only deterministic summary data. + +Expected output: + +```text +exit code: 0 +passed: 4 +``` + +The temporary suite demonstrates one normal assertion plus three parametrized boundary cases. + +## 86. Executable example: fixtures and `tmp_path` + +[`examples/fixtures_and_tmp_path.py`](examples/fixtures_and_tmp_path.py) demonstrates a fixture that depends on pytest's built-in `tmp_path` fixture. + +Expected output: + +```text +exit code: 0 +passed: 2 +``` + +Each test receives its own fixture invocation and temporary filesystem boundary. + +## 87. Executable example: `monkeypatch` + +[`examples/monkeypatch_environment.py`](examples/monkeypatch_environment.py) controls an environment variable without leaving process-global test state behind. + +Expected output: + +```text +exit code: 0 +passed: 2 +``` + +The example verifies both the fallback state and an explicitly configured state. + +## 88. Executable example: exceptions and warnings + +[`examples/exceptions_and_warnings.py`](examples/exceptions_and_warnings.py) uses `pytest.raises` and `pytest.warns` to make failure and migration behavior explicit. + +Expected output: + +```text +exit code: 0 +passed: 2 +``` + +Both exception type/message behavior and warning category/message behavior are verified. + +## 89. Executable example: output and log capture + +[`examples/capture_output_and_logs.py`](examples/capture_output_and_logs.py) demonstrates `capsys` and `caplog`. + +Expected output: + +```text +exit code: 0 +passed: 2 +``` + +The test suite validates both command-line output and a targeted logger message. + +## 90. Common mistakes + +### Mistake 1: treating a green suite as proof of no bugs + +Tests only cover the behaviors and inputs they actually exercise. + +### Mistake 2: testing implementation trivia + +Overly coupled tests make harmless refactoring expensive. + +### Mistake 3: sharing mutable state between tests + +This creates order dependence and flakes. + +### Mistake 4: calling public services from unit tests + +Network availability and remote data changes make the suite nondeterministic. + +### Mistake 5: hiding all warnings + +Warnings often reveal migrations the project needs to make. + +### Mistake 6: overusing mocks + +A suite of mocks can prove that mocks behave exactly as configured while missing real integration errors. + +### Mistake 7: creating giant fixtures + +Large setup graphs hide what each test actually needs. + +### Mistake 8: accepting flaky reruns as normal + +A flaky test is a defect in the feedback system. + +## 91. Decision table + +| Need | Useful pytest tool | Main caution | +| --- | --- | --- | +| Compare normal values | `assert` | keep the expected contract explicit | +| Compare floats | `pytest.approx()` | choose domain-appropriate tolerance | +| Expect an exception | `pytest.raises()` | do not catch unrelated failures | +| Expect a warning | `pytest.warns()` | test warning category/message deliberately | +| Repeat one rule over cases | `@pytest.mark.parametrize` | choose meaningful boundary data | +| Reuse setup | fixture | avoid hidden, oversized dependency graphs | +| Temporary files | `tmp_path` | do not depend on repository artifacts | +| Temporary environment changes | `monkeypatch` | patch where the code looks up the name | +| Capture stdout/stderr | `capsys` | assert only output that is part of the contract | +| Capture logs | `caplog` | avoid disrupting root logger handlers | +| Classify tests | registered marks | avoid silent spelling mistakes | +| Known expected failure | `xfail` | prefer strict handling and remove when fixed | + +## 92. Quick reference + +```bash +python -m pytest +python -m pytest -q +python -m pytest -v +python -m pytest --collect-only +python -m pytest -k "name_expression" +python -m pytest -m "marker_expression" +python -m pytest -x +python -m pytest --maxfail=3 +python -m pytest --lf +python -m pytest --max-warnings=10 +``` + +Core Python patterns: + +```python +assert actual == expected + +with pytest.raises(ValueError, match="message"): + operation() + +with pytest.warns(DeprecationWarning): + old_operation() +``` + +## 93. Review checklist + +Before calling a test suite reliable, ask: + +- Are the intended tests actually collected? +- Do test names explain behaviors? +- Are assertions specific enough to fail for the right reason? +- Are boundary and error cases represented? +- Are files written under temporary paths? +- Are environment changes restored automatically? +- Are network, clock, and randomness boundaries controlled? +- Can tests run independently and in any order? +- Are warnings visible and intentional? +- Are custom marks registered? +- Are expected failures reviewed and temporary? +- Does CI preserve pytest's failing exit status? +- Are test data and logs free of secrets and personal data? + +## 94. Practice exercise + +Create a small fictional package that validates study-session records. + +Requirements: + +1. Create a function that receives a topic and a duration in minutes. +2. Reject an empty topic with `ValueError`. +3. Reject zero or negative duration with `ValueError`. +4. Return a normalized dictionary for valid input. +5. Write normal success tests using plain `assert`. +6. Parametrize at least three invalid duration cases. +7. Use `pytest.raises(..., match=...)` for one validation error. +8. Write a fixture that returns valid sample data. +9. Add a function that saves a session as text or JSON and test it with `tmp_path`. +10. Add a function that reads one configuration value from an environment variable and test it with `monkeypatch`. +11. Add one CLI-style function and validate output with `capsys`. +12. Add one logger call and validate it with `caplog`. +13. Register one custom marker for integration tests. +14. Run `python -m pytest --collect-only` and confirm the expected cases appear. +15. Run the complete suite from a clean process. + +Extension challenges: + +- move shared fixtures into a carefully scoped `conftest.py`; +- add a warning for a deprecated input form and test it with `pytest.warns`; +- use `pytest.approx` for a calculated ratio with a documented tolerance; +- build a local HTTP integration test using concepts from the previous `requests` chapter; +- add CI that installs the declared dependencies and runs the suite from scratch. + +## 95. Connections to earlier concepts + +`pytest` connects almost every earlier phase: + +- **functions:** test behavior through explicit inputs and outputs; +- **collections:** build parameter tables and structured expected values; +- **program flow:** exercise branches and boundary conditions; +- **exceptions:** validate deliberate failure contracts; +- **files:** isolate filesystem behavior with temporary paths; +- **modules and packages:** organize application code and test imports predictably; +- **`pathlib`:** work naturally with `tmp_path`; +- **`datetime`:** inject or patch time boundaries instead of racing real time; +- **logging:** validate operational signals with `caplog`; +- **`decimal`:** test exact monetary rules without inappropriate float tolerance; +- **pandas/openpyxl/requests:** turn external-library behavior into repeatable regression tests. + +## 96. Primary references + +- [pytest documentation](https://docs.pytest.org/) +- [Get Started](https://docs.pytest.org/en/stable/getting-started.html) +- [How to invoke pytest](https://docs.pytest.org/en/stable/how-to/usage.html) +- [Assertions](https://docs.pytest.org/en/stable/how-to/assert.html) +- [Fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html) +- [Parametrization](https://docs.pytest.org/en/stable/how-to/parametrize.html) +- [Temporary directories](https://docs.pytest.org/en/stable/how-to/tmp_path.html) +- [Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) +- [Logging](https://docs.pytest.org/en/stable/how-to/logging.html) +- [Warnings](https://docs.pytest.org/en/stable/how-to/capture-warnings.html) +- [Skip and xfail](https://docs.pytest.org/en/stable/how-to/skipping.html) +- [API reference](https://docs.pytest.org/en/stable/reference/reference.html) +- [pytest changelog](https://docs.pytest.org/en/stable/changelog.html) +- [pytest on PyPI](https://pypi.org/project/pytest/) + +At the time this chapter was prepared, PyPI listed pytest 9.1.1 as the latest stable release. The curriculum targets the 9.1.x series rather than the unreleased 9.2 draft or an unbounded future version. + +## 97. Phase 9 complete + +Phase 9 now connects four important third-party boundaries: + +```text +pandas -> transform tabular data +openpyxl -> construct and maintain Excel workbooks +requests -> communicate with HTTP services +pytest -> verify behavior repeatedly and automatically +``` + +That closes **Phase 9: External Libraries**. + +The next phase moves from isolated library skills to integrated portfolio work: **Phase 10: Practical Projects**. + +Before moving on, practice writing tests that make failures informative. A test suite is most valuable when it gives you confidence to change the code, not when it merely produces a green number. diff --git a/external-libraries/04-pytest/README.pt-BR.md b/external-libraries/04-pytest/README.pt-BR.md new file mode 100644 index 0000000..8d1902b --- /dev/null +++ b/external-libraries/04-pytest/README.pt-BR.md @@ -0,0 +1,1279 @@ +
+ +# Engenharia de Testes Automatizados com `pytest` + +[🇺🇸 English](README.md) · [🇧🇷 Português](README.pt-BR.md) · [🇪🇸 Español](README.es.md) + +
+ +[← Voltar para Bibliotecas Externas](../README.pt-BR.md) · [← Anterior: `requests`](../03-requests/README.pt-BR.md) + +Software fica mais fácil de alterar quando o comportamento esperado pode ser verificado repetidamente e de forma automática. O `pytest` fornece um modelo de testes conciso baseado em funções Python normais, instruções `assert`, fixtures reutilizáveis, parametrização, relatórios de falha ricos e um sistema extensível de plugins. + +Este capítulo tem como alvo **pytest 9.1.x** e foi pesquisado com base na documentação e nos metadados da versão estável atual, **pytest 9.1.1**. O pytest 9.1.1 exige Python 3.10 ou mais recente; este repositório valida os exemplos em Python 3.13. + +**Tempo estimado de estudo:** 300–390 minutos. + +## Objetivos de aprendizagem + +Ao final deste capítulo, você deverá ser capaz de: + +- explicar o que um teste automatizado comprova e o que ele não comprova; +- organizar testes para que o pytest os descubra de forma previsível; +- escrever assertions legíveis e interpretar a introspecção de assertions; +- testar valores de ponto flutuante, exceções e warnings de forma deliberada; +- reduzir duplicação com parametrização; +- modelar setup, teardown e dependências com fixtures; +- isolar filesystem e ambiente com `tmp_path` e `monkeypatch`; +- capturar saída padrão e logs com `capsys` e `caplog`; +- usar marks, skips, falhas esperadas e seleção de testes intencionalmente; +- configurar pytest sem esconder warnings ou omissões acidentais de testes; +- distinguir fronteiras de testes unitários, de integração e end-to-end; +- evitar testes flaky causados por tempo, aleatoriedade, rede, estado compartilhado ou dependência de ordem; +- integrar pytest ao CI como um contrato executável de qualidade. + +## 1. Por que testes automatizados existem + +Uma verificação manual responde a uma pergunta uma vez. Um teste automatizado transforma essa pergunta em código executável que pode ser repetido depois de alterações futuras. + +Um bom teste descreve um comportamento, fornece entradas controladas, observa uma saída ou efeito colateral e falha quando o comportamento observado viola o contrato esperado. + +Testes reduzem incerteza. Eles não provam que o software não possui bugs. + +## 2. O que o `pytest` acrescenta + +Python inclui `unittest` na biblioteca padrão. O `pytest` é um framework externo capaz de executar funções de teste simples e acrescentar recursos como: + +- introspecção de assertions; +- fixtures; +- parametrização; +- marks e seleção de testes; +- caminhos temporários; +- monkeypatch de ambiente; +- captura de saída, warnings e logs; +- plugins e hooks. + +O objetivo não é deixar os testes sofisticados. O objetivo é tornar a intenção visível e a repetição barata. + +## 3. Bibliotecas externas precisam de contrato de versão + +Este repositório declara as dependências da Fase 9 em `requirements-external.txt`. + +Para este capítulo, o contrato é: + +```text +pytest >= 9.1 and < 9.2 +``` + +O limite superior importa porque o changelog do pytest já contém um draft não lançado da versão 9.2 com mudanças incompatíveis. Um currículo publicado deve descrever comportamento lançado, e não seguir silenciosamente uma versão futura. + +## 4. Instale o conjunto de dependências do repositório + +Crie e ative um ambiente virtual e então instale: + +```bash +python -m pip install -r requirements-external.txt +``` + +Para experimentação isolada: + +```bash +python -m pip install pytest +``` + +Mesmo assim, um projeto deve registrar qual faixa de pytest suporta. + +## 5. Prefira `python -m pytest` quando a identidade do interpretador importa + +Uma invocação comum é: + +```bash +python -m pytest +``` + +Usar `python -m` torna o interpretador explícito. Isso é especialmente útil quando existem várias instalações do Python ou ambientes virtuais na mesma máquina. + +O comando `pytest` também é válido quando o ambiente não é ambíguo. + +## 6. Um teste é especificação executável, não código de produção + +Considere uma função pequena: + +```python +def calculate_total(unit_price: int, quantity: int) -> int: + return unit_price * quantity +``` + +Um teste pode declarar um comportamento esperado: + +```python +def test_calculate_total_multiplies_price_by_quantity() -> None: + assert calculate_total(12, 3) == 36 +``` + +O nome do teste comunica o contrato antes mesmo da leitura da assertion. + +## 7. pytest descobre testes por convenção + +Por padrão, o pytest descobre módulos e funções de teste por convenções de nome. + +Uma estrutura comum é: + +```text +project/ +├── src/ +│ └── calculator.py +└── tests/ + └── test_calculator.py +``` + +Dentro de `test_calculator.py`, funções chamadas `test_*` são coletadas como testes. + +## 8. Coleta é uma fase da execução + +Antes de executar os testes, o pytest primeiro os descobre e coleta. + +Você pode inspecionar a coleta sem executar nada: + +```bash +python -m pytest --collect-only +``` + +Isso é útil quando um teste esperado não aparece. + +Uma suíte verde que coletou os testes errados não é um sinal confiável. + +## 9. Mantenha nomes de testes orientados a comportamento + +Prefira nomes que expliquem uma regra observável: + +```python +def test_discount_is_zero_for_empty_cart() -> None: + ... +``` + +Evite nomes que apenas espelham detalhes de implementação: + +```python +def test_function_2() -> None: + ... +``` + +Bons nomes facilitam a triagem de falhas no CI. + +## 10. `assert` simples é o estilo normal de assertion no pytest + +```python +def test_status_is_ready() -> None: + status = "ready" + assert status == "ready" +``` + +O pytest reescreve assertions durante a coleta para produzir diagnósticos mais ricos do que um `AssertionError` puro normalmente forneceria. + +## 11. A introspecção de assertion ajuda a explicar falhas + +Uma comparação como: + +```python +def test_summary() -> None: + actual = {"count": 2, "status": "ready"} + expected = {"count": 3, "status": "ready"} + assert actual == expected +``` + +pode mostrar os valores diferentes quando falha. + +Por isso, expressões diretas costumam ser melhores do que mensagens vagas construídas manualmente em toda assertion. + +## 12. Adicione mensagem apenas quando ela trouxer contexto de domínio + +```python +def test_inventory_never_becomes_negative() -> None: + remaining = 4 + assert remaining >= 0, "inventory contract requires a non-negative balance" +``` + +A mensagem deve explicar por que a condição importa, e não apenas repetir `remaining >= 0`. + +## 13. Use Arrange, Act, Assert quando isso deixar o teste mais claro + +Um teste legível costuma ter três etapas conceituais: + +```python +def test_normalize_name_removes_outer_whitespace() -> None: + raw_name = " Nova " + + normalized = raw_name.strip() + + assert normalized == "Nova" +``` + +Nem todo teste pequeno precisa de comentários nomeando as etapas. A própria estrutura pode deixá-las evidentes. + +## 14. Teste um comportamento coerente + +Um teste pode ter várias assertions quando elas descrevem um único resultado, mas evite transformar um único teste em um passeio por comportamentos não relacionados. + +Testes menores tornam falhas mais locais e fáceis de diagnosticar. + +## 15. Testes determinísticos são repetíveis + +Um teste determinístico produz o mesmo resultado quando o código e as entradas relevantes não mudaram. + +Ameaças comuns incluem: + +- horário atual; +- valores aleatórios sem controle; +- serviços de rede; +- arquivos ou bancos compartilhados; +- variáveis de ambiente; +- locale e timezone; +- dependência da ordem de execução. + +Isolamento é uma habilidade de design, não apenas uma funcionalidade do framework. + +## 16. Compare resultados de ponto flutuante com tolerância explícita + +Valores de ponto flutuante binário muitas vezes não são adequados para igualdade exata depois de cálculos. + +O pytest fornece `approx()`: + +```python +import pytest + + +def test_ratio() -> None: + result = 1 / 3 + assert result == pytest.approx(0.333333, rel=1e-5) +``` + +Escolha tolerâncias de acordo com o domínio, em vez de copiar valores arbitrários. + +## 17. Valores exatos devem continuar usando assertions exatas + +Não use `pytest.approx()` quando o contrato é exato. + +```python +def test_item_count() -> None: + assert len(["a", "b", "c"]) == 3 +``` + +Ferramentas de teste devem tornar contratos mais claros, não mais vagos. + +## 18. Teste exceções esperadas com `pytest.raises` + +```python +import pytest + + +def parse_positive(value: str) -> int: + number = int(value) + if number <= 0: + raise ValueError("value must be positive") + return number + + +def test_zero_is_rejected() -> None: + with pytest.raises(ValueError): + parse_positive("0") +``` + +O teste passa somente se o tipo de exceção esperado for levantado dentro do context manager. + +## 19. Faça match da mensagem quando ela faz parte do contrato + +```python +def test_zero_has_clear_message() -> None: + with pytest.raises(ValueError, match="must be positive"): + parse_positive("0") +``` + +`match` é interpretado como expressão regular. Escape caracteres especiais quando quiser correspondência literal. + +## 20. Inspecione informações da exceção capturada quando necessário + +```python +def test_invalid_value_context() -> None: + with pytest.raises(ValueError) as exc_info: + parse_positive("-4") + + assert "positive" in str(exc_info.value) +``` + +Faça isso quando o detalhe adicional importar. Não inspecione internals apenas porque o pytest os expõe. + +## 21. Teste warnings explicitamente com `pytest.warns` + +```python +import warnings + +import pytest + + +def old_api() -> None: + warnings.warn("old API", DeprecationWarning, stacklevel=2) + + +def test_old_api_warns() -> None: + with pytest.warns(DeprecationWarning, match="old API"): + old_api() +``` + +Warnings podem representar contratos de migração que merecem testes próprios. + +## 22. pytest 9.1 pode impor um orçamento de warnings + +O pytest 9.1 adicionou `--max-warnings`. + +Por exemplo: + +```bash +python -m pytest --max-warnings=10 +``` + +Se todos os testes passarem, mas a quantidade de warnings não filtrados ultrapassar o limite, o pytest retorna um status dedicado diferente de zero. + +Um orçamento de warnings ajuda um projeto a reduzir dívida gradualmente em vez de simplesmente esconder tudo. + +## 23. Parametrização transforma variação de dados em casos de teste + +Quando o mesmo comportamento deve valer para várias entradas, use `@pytest.mark.parametrize`: + +```python +import pytest + + +@pytest.mark.parametrize( + ("raw", "expected"), + [(-5, 0), (40, 40), (130, 100)], +) +def test_normalize_score(raw: int, expected: int) -> None: + result = max(0, min(raw, 100)) + assert result == expected +``` + +O pytest cria um caso coletado separado para cada conjunto de parâmetros. + +## 24. Separe lógica de teste de dados de teste + +Parametrização funciona melhor quando o corpo do teste expressa uma regra e os dados representam casos interessantes. + +Inclua fronteiras significativas, e não apenas muitos exemplos aleatórios. + +Uma tabela com dez linhas não é automaticamente melhor que três casos de fronteira bem escolhidos. + +## 25. Dê IDs úteis aos parâmetros quando o relatório precisar deles + +```python +@pytest.mark.parametrize( + ("value", "expected"), + [(0, "empty"), (1, "single"), (2, "many")], + ids=["zero", "one", "multiple"], +) +def test_classification(value: int, expected: str) -> None: + result = "empty" if value == 0 else "single" if value == 1 else "many" + assert result == expected +``` + +IDs legíveis melhoram relatórios para valores de parâmetro complexos. + +## 26. Use `pytest.param` para metadados por caso + +```python +@pytest.mark.parametrize( + "value", + [ + 1, + pytest.param(-1, marks=pytest.mark.xfail(reason="known limitation")), + ], +) +def test_positive_only(value: int) -> None: + assert value > 0 +``` + +Marks por caso mantêm exceções visíveis sem duplicar o teste inteiro. + +## 27. pytest 9.1 deprecia iteráveis que não são collections na parametrização + +A documentação atual de pytest deprecia passar diretamente um iterável que não seja `Collection`, como um generator, em `argvalues`. + +Prefira uma lista ou tupla concreta em testes publicados: + +```python +cases = [(1, 2), (2, 4), (3, 6)] +``` + +Isso também facilita a inspeção e revisão dos dados de teste. + +## 28. Fixtures modelam dependências de teste + +Uma fixture é um valor ou recurso que o pytest fornece a um teste pelo nome. + +```python +import pytest + + +@pytest.fixture +def sample_user() -> dict[str, str]: + return {"name": "Nova", "role": "reader"} + + +def test_user_role(sample_user: dict[str, str]) -> None: + assert sample_user["role"] == "reader" +``` + +O teste solicita a fixture declarando um parâmetro com o nome dela. + +## 29. Fixtures podem retornar objetos + +Fixtures podem retornar valores simples, estruturas, clientes configurados, repositórios temporários, conexões ou outros recursos. + +Mantenha fixtures focadas. Uma fixture gigante que prepara a aplicação inteira pode esconder dependências em vez de esclarecê-las. + +## 30. Fixtures podem fazer teardown com `yield` + +```python +import pytest + + +@pytest.fixture +def opened_resource(): + resource = {"open": True} + yield resource + resource["open"] = False +``` + +Código antes de `yield` faz setup. Código depois de `yield` faz teardown quando o pytest finaliza a fixture. + +Use context managers reais quando o recurso de produção já oferecer um. + +## 31. O scope da fixture controla seu tempo de vida + +Scopes comuns são: + +```text +function -> one test invocation +class -> one test class +module -> one test module +package -> one test package +session -> the whole pytest session +``` + +O padrão é scope `function`. + +## 32. Scope mais amplo troca isolamento por reutilização + +Um recurso de scope `session` pode ser mais rápido por ser criado uma vez, mas também vive mais tempo e pode carregar estado mutável compartilhado entre testes. + +Não amplie scope apenas para acelerar a suíte. Primeiro entenda se a vida compartilhada preserva independência. + +## 33. Fixtures podem depender de outras fixtures + +```python +import pytest + + +@pytest.fixture +def base_url() -> str: + return "https://example.invalid" + + +@pytest.fixture +def endpoint(base_url: str) -> str: + return f"{base_url}/items" +``` + +Composição de dependências costuma ser mais clara que uma fixture que conhece todo o setup. + +## 34. Evite dependências escondidas com excesso de `autouse` + +Uma fixture `autouse=True` roda sem aparecer na assinatura de cada teste. + +Isso pode servir para uma invariante real da suíte, mas uso excessivo torna o comportamento difícil de rastrear. + +Prefira parâmetros explícitos de fixture, salvo quando a aplicação automática fizer parte genuína do contrato do ambiente. + +## 35. `conftest.py` compartilha configuração local e fixtures + +Uma estrutura comum é: + +```text +tests/ +├── conftest.py +├── test_api.py +└── test_reports.py +``` + +Fixtures definidas em `tests/conftest.py` podem ser descobertas por testes abaixo desse diretório sem importar `conftest` diretamente. + +## 36. `conftest.py` segue regras de visibilidade por diretório + +Para um teste, o pytest consulta arquivos `conftest.py` relevantes no diretório do teste e em diretórios pais. + +Isso torna a visibilidade hierárquica. + +Coloque fixtures compartilhadas no nível mais estreito que precisa delas. + +## 37. Não importe de `conftest.py` + +Trate `conftest.py` como configuração do pytest, não como módulo da aplicação. + +Se helpers precisarem de imports normais, coloque-os em módulo ou pacote regular e importe esse módulo dos testes e fixtures. + +## 38. `tmp_path` fornece um `Path` temporário para cada teste + +```python +from pathlib import Path + + +def test_export(tmp_path: Path) -> None: + report = tmp_path / "report.txt" + report.write_text("ready\n", encoding="utf-8") + + assert report.read_text(encoding="utf-8") == "ready\n" +``` + +`tmp_path` é um `pathlib.Path` único por invocação de teste. + +Isso evita poluir o repositório com artefatos de teste. + +## 39. `tmp_path_factory` serve para scopes mais amplos + +Uma fixture de scope `session` ou `module` não pode depender de `tmp_path`, que tem scope de função. + +Para recursos temporários mais duradouros, pytest oferece `tmp_path_factory`. + +Escolha uma vida mais ampla somente quando fizer parte do design. + +## 40. `monkeypatch` altera estado e o restaura automaticamente + +A fixture `monkeypatch` pode modificar temporariamente: + +- atributos de objetos; +- itens de dicionários; +- variáveis de ambiente; +- `sys.path`; +- diretório de trabalho atual. + +As mudanças são desfeitas depois que o teste ou fixture solicitante termina. + +## 41. Faça patch de variáveis de ambiente com `setenv` e `delenv` + +```python +import os + + +def read_mode() -> str: + return os.getenv("STUDY_MODE", "default") + + +def test_configured_mode(monkeypatch) -> None: + monkeypatch.setenv("STUDY_MODE", "focused") + assert read_mode() == "focused" +``` + +Código dependente de ambiente fica determinístico quando o teste controla o ambiente explicitamente. + +## 42. Faça patch onde o código procura a dependência + +Suponha que `service.py` contenha: + +```python +from client import fetch_status + + +def is_ready() -> bool: + return fetch_status() == "ready" +``` + +O teste normalmente precisa fazer patch de `service.fetch_status`, pois é esse nome que `is_ready()` resolve em runtime. + +Fazer patch da definição original em `client` pode não substituir uma referência já importada em `service`. + +## 43. `monkeypatch.context()` pode limitar ainda mais a duração do patch + +Quando um teste precisa do patch apenas em um bloco pequeno, `monkeypatch.context()` cria um contexto aninhado cujas alterações são desfeitas ao sair do bloco. + +Tempos menores de patch reduzem interações surpreendentes em testes complexos. + +## 44. Use test doubles para substituir fronteiras, não tudo + +Um test double pode representar um colaborador lento, não determinístico, destrutivo ou indisponível. + +Categorias informais comuns incluem: + +```text +stub -> returns controlled values +fake -> lightweight working implementation +spy -> records how it was used +mock -> verifies expected interactions +``` + +O vocabulário importa menos do que deixar claro o propósito da substituição. + +## 45. `unittest.mock` da biblioteca padrão funciona com pytest + +pytest não exige um estilo separado de mocking. + +Você pode combinar assertions e fixtures do pytest com `unittest.mock.Mock`, `MagicMock` ou `patch` quando forem adequados. + +Não faça mock de cálculos puros apenas porque a ferramenta existe. + +## 46. `capsys` captura stdout e stderr no nível Python + +```python +def announce(topic: str) -> None: + print(f"Studying: {topic}") + + +def test_announce(capsys) -> None: + announce("pytest") + captured = capsys.readouterr() + assert captured.out == "Studying: pytest\n" + assert captured.err == "" +``` + +Isso é útil para interfaces de linha de comando e funções cuja saída faz parte do contrato. + +## 47. `capfd` captura no nível de file descriptor + +`capsys` foca nos objetos `sys.stdout` e `sys.stderr` do Python. + +`capfd` captura os file descriptors 1 e 2, útil quando a saída vem de código de nível mais baixo que contorna streams normais do Python. + +Use o mecanismo mais estreito que corresponde ao comportamento testado. + +## 48. `caplog` captura registros de logging + +```python +import logging + + +def test_log_message(caplog) -> None: + logger = logging.getLogger("study") + + with caplog.at_level(logging.INFO, logger="study"): + logger.info("session ready") + + assert "session ready" in caplog.text +``` + +Os testes também podem inspecionar registros estruturados em vez de apenas texto renderizado. + +## 49. Cuidado ao reconfigurar o root logger durante `caplog` + +A documentação do pytest alerta que alterar handlers do root logger durante um teste pode interferir na captura de logs. + +Prefira configuração de logger direcionada e evite substituir o conjunto inteiro de handlers, salvo se o teste validar justamente essa configuração. + +## 50. Marks adicionam metadados aos testes + +Marks podem classificar ou alterar comportamento. + +```python +import pytest + + +@pytest.mark.slow +def test_large_report() -> None: + assert True +``` + +Custom marks devem representar categorias úteis, e não substituir organização clara da suíte. + +## 51. Registre custom marks + +Marks não registradas podem gerar warnings, e erros de digitação podem criar categorias indesejadas silenciosamente. + +Um `pyproject.toml` pode registrá-las: + +```toml +[tool.pytest.ini_options] +markers = [ + "slow: tests that intentionally take longer", + "integration: tests that cross component boundaries", +] +``` + +O registro transforma nomes de marks em contrato documentado do projeto. + +## 52. `strict_markers` pode transformar marks desconhecidas em erros + +```toml +[tool.pytest.ini_options] +strict_markers = true +``` + +Isso é útil quando uma mark digitada incorretamente deve falhar imediatamente em vez de ser tratada como nova mark. + +## 53. pytest 9 introduziu um strict mode mais amplo + +O pytest 9 fornece a opção de configuração `strict`, que ativa em conjunto verificações de configuração, markers, xfail e IDs de parametrização. + +A documentação alerta que versões futuras podem adicionar novas opções de strictness. Use o modo global com uma versão de pytest controlada ou quando o projeto quiser adotar novas verificações proativamente. + +## 54. Pule testes somente por uma razão ambiental real + +```python +import sys + +import pytest + + +@pytest.mark.skipif(sys.platform == "win32", reason="POSIX-only contract") +def test_posix_behavior() -> None: + assert True +``` + +Um teste pulado não verifica o comportamento. Muitos skips podem criar pontos cegos. + +## 55. `xfail` registra uma falha esperada conhecida + +```python +import pytest + + +@pytest.mark.xfail(reason="known parser limitation", strict=True) +def test_future_case() -> None: + assert False +``` + +Com `strict=True`, um passe inesperado falha a suíte, obrigando o time a perceber que a limitação conhecida pode ter sido corrigida. + +Não use `xfail` como estacionamento permanente de testes quebrados. + +## 56. Selecione testes com `-k` + +`-k` filtra testes coletados por expressão de nome: + +```bash +python -m pytest -k "report and not slow" +``` + +Isso é conveniente no desenvolvimento local, mas o CI ainda deve executar a suíte completa pretendida ou partições documentadas explicitamente. + +## 57. Selecione grupos marcados com `-m` + +```bash +python -m pytest -m "integration" +``` + +ou: + +```bash +python -m pytest -m "not slow" +``` + +Marks tornam partições explícitas quando são registradas e mantidas consistentemente. + +## 58. Pare cedo com `-x` ou `--maxfail` + +```bash +python -m pytest -x +``` + +para após a primeira falha. + +```bash +python -m pytest --maxfail=3 +``` + +para após três falhas. + +São opções úteis para velocidade de feedback, mas não substituem a suíte completa antes de um release. + +## 59. Reexecute falhas anteriores com `--lf` + +```bash +python -m pytest --lf +``` + +O pytest pode usar o cache para selecionar testes que falharam na execução anterior. + +Trate isso como acelerador local. Um job limpo de CI não deve depender do estado de uma execução anterior do desenvolvedor. + +## 60. Verbosidade muda o relatório, não a correção + +Opções comuns incluem: + +```bash +python -m pytest -q +python -m pytest -v +``` + +Saída quiet pode ajudar em logs automatizados; saída verbose pode ajudar a identificar casos parametrizados. + +O contrato de teste não deve depender da decoração do terminal. + +## 61. Configuração deve ficar no controle de versão + +pytest suporta configuração de projeto em arquivos suportados como `pyproject.toml`, `pytest.ini` e outros documentados pelo projeto. + +Uma configuração mínima em `pyproject.toml` pode ser: + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +strict_markers = true +``` + +Configuração deve tornar a suíte previsível, não esconder comportamento que falha. + +## 62. `testpaths` restringe a descoberta padrão + +```toml +[tool.pytest.ini_options] +testpaths = ["tests"] +``` + +Quando pytest é chamado sem caminhos explícitos, isso indica onde o projeto espera encontrar testes. + +Se houver testes em outros locais, configure ou invoque-os intencionalmente. + +## 63. Tenha cautela com `addopts` global + +Um projeto pode configurar opções padrão, por exemplo: + +```toml +[tool.pytest.ini_options] +addopts = "-ra" +``` + +Evite defaults que silenciosamente pulem categorias importantes ou escondam diagnósticos necessários. + +## 64. Entenda o root selecionado pelo pytest + +pytest determina um diretório raiz e um contexto de configuração para a coleta. + +Executar pytest de um diretório inesperado pode alterar quais configurações e arquivos `conftest.py` ficam visíveis. + +Ao investigar problemas de descoberta, inspecione o rootdir e o arquivo de configuração reportados. + +## 65. Mantenha imports previsíveis + +Testes continuam sendo módulos Python, então regras de import importam. + +Um projeto deve usar layout de pacote deliberado e testar o código que pretende distribuir, em vez de depender de imports acidentais pelo diretório de trabalho. + +O layout comum `src/` pode ajudar a separar código instalado de caminhos locais do repositório. + +## 66. Não dê `__init__` customizado a classes de teste do pytest + +Classes de teste do pytest são coletadas por convenção e não devem se comportar como objetos de aplicação que exigem argumentos de construtor. + +Use fixtures para dependências de testes em vez de construção customizada da classe. + +Funções de teste simples costumam ser o melhor ponto de partida. + +## 67. Valores de fixtures devem corresponder ao nome declarado + +Se uma fixture se chama `authenticated_client`, ela deve fornecer esse estado de forma confiável. + +Evite fixtures cujo resultado muda inesperadamente por configurações globais não relacionadas. Fixtures ambíguas transformam testes em enigmas. + +## 68. Evite florestas de fixtures + +Composição de fixtures é poderosa, mas um teste dependente de uma fixture que depende de mais seis pode ficar difícil de entender. + +Se o setup virar um labirinto, considere builders simples, helper functions ou fronteiras de integração menores. + +## 69. Testes unitários isolam uma pequena unidade de comportamento + +Um teste unitário normalmente exercita função, classe ou componente pequeno com colaboradores controlados. + +São valiosos para feedback rápido, mas a definição exata de “unidade” depende da arquitetura. + +Não transforme o rótulo em regra doutrinária. + +## 70. Testes de integração cruzam fronteiras reais de componentes + +Um teste de integração pode exercitar combinações como: + +```text +application code + database adapter +application code + local HTTP server +parser + real file format +repository + temporary filesystem +``` + +Esses testes validam contratos que mocks não conseguem provar completamente. + +## 71. Testes end-to-end validam fluxos maiores + +Testes end-to-end exercitam um caminho amplo pelo sistema e podem detectar problemas que testes menores não enxergam. + +Também costumam ser mais lentos, caros para diagnosticar e sensíveis ao ambiente. + +Uma suíte saudável normalmente usa várias camadas. + +## 72. Teste comportamento observável antes de detalhes de implementação + +Se um refactor preserva o comportamento público, bons testes normalmente continuam passando. + +Testes que verificam cada chamada de helper privado tornam limpeza interna desnecessariamente cara. + +Assertions de interação são adequadas quando a própria interação faz parte do contrato, como “não envie a requisição duas vezes”. + +## 73. Faça mock de serviços externos na fronteira correta + +Um teste unitário não deve chamar uma API pública real. + +Para código HTTP, estratégias úteis incluem: + +- fazer patch da sua própria abstração de cliente; +- usar servidor de teste local; +- usar plugin específico de testes HTTP quando o projeto adotar um. + +Testes não devem depender da internet pública, salvo quando forem testes deliberados de sistema externo. + +## 74. Mantenha segredos fora dos dados de teste + +Nunca coloque tokens, senhas, cookies, URLs privadas ou dados pessoais reais nos testes. + +Use valores fictícios como: + +```python +fake_token = "test-token-not-a-secret" +``` + +Fixtures de teste frequentemente aparecem em logs e relatórios de falha, então merecem a mesma disciplina de privacidade do código de produção. + +## 75. Controle o tempo em vez de competir com o relógio + +Evite testes como: + +```python +import time + + +def test_waits() -> None: + time.sleep(2) + assert True +``` + +Se o comportamento depende do tempo, injete um clock ou faça patch da fonte de tempo estreita usada pelo código. + +`sleep` deixa a suíte lenta e não garante que um estado assíncrono esteja pronto. + +## 76. Controle aleatoriedade + +Para código aleatório, opções incluem: + +- injetar um gerador de números aleatórios; +- usar seed conhecido quando o contrato permitir; +- testar invariantes com entradas controladas. + +Um teste que falha apenas em algumas execuções aleatórias é difícil de reproduzir e diagnosticar. + +## 77. Testes não devem depender da ordem de execução + +Um teste não deve precisar que outro rode primeiro. + +Estado mutável compartilhado em módulo ou sessão é uma causa comum de dependência de ordem. + +Se testes falham apenas quando a ordem muda, a suíte revelou um problema real de isolamento. + +## 78. Um teste flaky é um defeito de confiabilidade + +Um teste flaky alterna entre passar e falhar sem mudança relevante de código. + +Causas comuns incluem: + +- corridas de timing; +- serviços externos; +- estado compartilhado; +- ordenação não determinística; +- limpeza insuficiente; +- esgotamento de recursos. + +Reexecutar até ficar verde esconde o sinal em vez de reparar o problema. + +## 79. Coverage e correção são métricas diferentes + +Cobertura de código pode revelar código que testes nunca executam. + +Ela não prova que assertions são significativas, casos de fronteira estão representados ou requisitos estão corretos. + +Trate coverage como evidência de execução, não substituto do design de testes. + +## 80. Plugins estendem pytest + +pytest possui um grande ecossistema de plugins para coverage, código assíncrono, frameworks, execução paralela e testes HTTP. + +Plugins também são dependências. Limite versões importantes, revise compatibilidade e evite adicionar plugin quando o core já resolve o problema claramente. + +## 81. pytest core não faz todo `async def` funcionar automaticamente + +Funções de teste assíncronas normalmente exigem plugin ou integração de framework apropriado. + +Não assuma que instalar pytest sozinho define a política de event loop necessária pela aplicação. + +O plugin passa a fazer parte do contrato de dependências de teste. + +## 82. `required_plugins` pode exigir a presença de plugins + +A configuração do pytest pode declarar plugins obrigatórios para que a execução falhe cedo quando um deles estiver ausente. + +Isso é útil quando a suíte poderia coletar incorretamente ou falhar depois com erros confusos de fixture ausente. + +Use requisitos reais do projeto, não listas copiadas de repositórios alheios. + +## 83. pytest pode executar muitos testes no estilo `unittest` + +Adotar pytest não exige necessariamente reescrever uma suíte existente de `unittest` imediatamente. + +pytest suporta muitos testes escritos com `unittest.TestCase` e permite uma migração gradual. + +Migração deve melhorar manutenção, não criar churn por si só. + +## 84. Trate códigos de saída como contratos de CI + +Um sistema de CI deve falhar quando o runner de testes reporta falha. + +Não escreva wrappers de shell que descartem o exit status do pytest. + +Os exemplos executáveis deste capítulo convertem o código de saída programático para inteiro apenas para apresentar o resultado de forma determinística. + +## 85. Exemplo executável: assertions e parametrização + +[`examples/assertions_and_parametrize.py`](examples/assertions_and_parametrize.py) cria um módulo pytest temporário, executa-o com o runner real e reporta somente um resumo determinístico. + +Saída esperada: + +```text +exit code: 0 +passed: 4 +``` + +A suíte temporária demonstra uma assertion normal e três casos de fronteira parametrizados. + +## 86. Exemplo executável: fixtures e `tmp_path` + +[`examples/fixtures_and_tmp_path.py`](examples/fixtures_and_tmp_path.py) demonstra uma fixture que depende da fixture built-in `tmp_path`. + +Saída esperada: + +```text +exit code: 0 +passed: 2 +``` + +Cada teste recebe sua própria invocação de fixture e fronteira temporária de filesystem. + +## 87. Exemplo executável: `monkeypatch` + +[`examples/monkeypatch_environment.py`](examples/monkeypatch_environment.py) controla uma variável de ambiente sem deixar estado global do processo para trás. + +Saída esperada: + +```text +exit code: 0 +passed: 2 +``` + +O exemplo verifica tanto o fallback quanto um estado configurado explicitamente. + +## 88. Exemplo executável: exceções e warnings + +[`examples/exceptions_and_warnings.py`](examples/exceptions_and_warnings.py) usa `pytest.raises` e `pytest.warns` para tornar comportamento de falha e migração explícito. + +Saída esperada: + +```text +exit code: 0 +passed: 2 +``` + +São verificados tipo/mensagem da exceção e categoria/mensagem do warning. + +## 89. Exemplo executável: captura de saída e logs + +[`examples/capture_output_and_logs.py`](examples/capture_output_and_logs.py) demonstra `capsys` e `caplog`. + +Saída esperada: + +```text +exit code: 0 +passed: 2 +``` + +A suíte valida saída de linha de comando e mensagem de logger direcionado. + +## 90. Erros comuns + +### Erro 1: tratar uma suíte verde como prova de ausência de bugs + +Testes cobrem apenas comportamentos e entradas que realmente exercitam. + +### Erro 2: testar trivia de implementação + +Testes acoplados demais tornam refactors inofensivos caros. + +### Erro 3: compartilhar estado mutável entre testes + +Isso cria dependência de ordem e flakiness. + +### Erro 4: chamar serviços públicos em testes unitários + +Disponibilidade de rede e mudanças remotas tornam a suíte não determinística. + +### Erro 5: esconder todos os warnings + +Warnings frequentemente revelam migrações necessárias. + +### Erro 6: usar mocks em excesso + +Uma suíte de mocks pode provar que os mocks se comportam exatamente como configurados e ainda perder erros reais de integração. + +### Erro 7: criar fixtures gigantes + +Grafos enormes de setup escondem o que cada teste realmente precisa. + +### Erro 8: aceitar reruns flaky como normais + +Um teste flaky é um defeito no sistema de feedback. + +## 91. Tabela de decisão + +| Necessidade | Ferramenta útil | Principal cuidado | +| --- | --- | --- | +| Comparar valores normais | `assert` | mantenha explícito o contrato esperado | +| Comparar floats | `pytest.approx()` | escolha tolerância apropriada ao domínio | +| Esperar uma exceção | `pytest.raises()` | não capture falhas não relacionadas | +| Esperar um warning | `pytest.warns()` | teste categoria/mensagem deliberadamente | +| Repetir uma regra em vários casos | `@pytest.mark.parametrize` | escolha dados de fronteira significativos | +| Reutilizar setup | fixture | evite grafos ocultos e gigantes | +| Arquivos temporários | `tmp_path` | não dependa de artefatos no repositório | +| Alterações temporárias de ambiente | `monkeypatch` | faça patch onde o código procura o nome | +| Capturar stdout/stderr | `capsys` | verifique apenas saída que faz parte do contrato | +| Capturar logs | `caplog` | evite bagunçar handlers do root logger | +| Classificar testes | marks registradas | evite erros silenciosos de digitação | +| Falha conhecida esperada | `xfail` | prefira tratamento strict e remova quando corrigido | + +## 92. Referência rápida + +```bash +python -m pytest +python -m pytest -q +python -m pytest -v +python -m pytest --collect-only +python -m pytest -k "name_expression" +python -m pytest -m "marker_expression" +python -m pytest -x +python -m pytest --maxfail=3 +python -m pytest --lf +python -m pytest --max-warnings=10 +``` + +Padrões Python principais: + +```python +assert actual == expected + +with pytest.raises(ValueError, match="message"): + operation() + +with pytest.warns(DeprecationWarning): + old_operation() +``` + +## 93. Checklist de revisão + +Antes de chamar uma suíte de confiável, pergunte: + +- Os testes pretendidos estão realmente sendo coletados? +- Os nomes explicam comportamentos? +- As assertions são específicas o bastante para falhar pela razão certa? +- Casos de fronteira e erro estão representados? +- Arquivos são escritos em caminhos temporários? +- Mudanças de ambiente são restauradas automaticamente? +- Rede, relógio e aleatoriedade estão controlados? +- Os testes rodam de forma independente e em qualquer ordem? +- Warnings são visíveis e intencionais? +- Custom marks estão registradas? +- Falhas esperadas são revisadas e temporárias? +- O CI preserva o exit status de falha do pytest? +- Dados e logs de teste estão livres de segredos e dados pessoais? + +## 94. Exercício prático + +Crie um pequeno pacote fictício que valida registros de sessões de estudo. + +Requisitos: + +1. Crie uma função que receba um tópico e duração em minutos. +2. Rejeite tópico vazio com `ValueError`. +3. Rejeite duração zero ou negativa com `ValueError`. +4. Retorne um dicionário normalizado para entrada válida. +5. Escreva testes de sucesso usando `assert` simples. +6. Parametrize pelo menos três durações inválidas. +7. Use `pytest.raises(..., match=...)` para um erro de validação. +8. Escreva uma fixture que retorne dados válidos de exemplo. +9. Adicione uma função que salve uma sessão como texto ou JSON e teste com `tmp_path`. +10. Adicione uma função que leia uma configuração de variável de ambiente e teste com `monkeypatch`. +11. Adicione uma função estilo CLI e valide saída com `capsys`. +12. Adicione um logger e valide com `caplog`. +13. Registre uma custom marker para testes de integração. +14. Execute `python -m pytest --collect-only` e confirme os casos esperados. +15. Execute a suíte completa em um processo limpo. + +Desafios de extensão: + +- mova fixtures compartilhadas para um `conftest.py` cuidadosamente escopado; +- adicione um warning para uma forma de entrada deprecada e teste com `pytest.warns`; +- use `pytest.approx` para uma razão calculada com tolerância documentada; +- construa um teste de integração HTTP local usando conceitos do capítulo anterior de `requests`; +- adicione CI que instale dependências declaradas e execute a suíte do zero. + +## 95. Conexões com conceitos anteriores + +`pytest` conecta quase todas as fases anteriores: + +- **funções:** teste comportamento por entradas e saídas explícitas; +- **coleções:** construa tabelas de parâmetros e valores esperados estruturados; +- **fluxo de programa:** exercite branches e condições de fronteira; +- **exceções:** valide contratos de falha deliberada; +- **arquivos:** isole filesystem com caminhos temporários; +- **módulos e pacotes:** organize código e imports previsivelmente; +- **`pathlib`:** trabalhe naturalmente com `tmp_path`; +- **`datetime`:** injete ou faça patch de tempo em vez de disputar com o relógio real; +- **logging:** valide sinais operacionais com `caplog`; +- **`decimal`:** teste regras monetárias exatas sem tolerância de float inadequada; +- **pandas/openpyxl/requests:** transforme comportamento de bibliotecas externas em regressões repetíveis. + +## 96. Referências primárias + +- [pytest documentation](https://docs.pytest.org/) +- [Get Started](https://docs.pytest.org/en/stable/getting-started.html) +- [How to invoke pytest](https://docs.pytest.org/en/stable/how-to/usage.html) +- [Assertions](https://docs.pytest.org/en/stable/how-to/assert.html) +- [Fixtures](https://docs.pytest.org/en/stable/how-to/fixtures.html) +- [Parametrization](https://docs.pytest.org/en/stable/how-to/parametrize.html) +- [Temporary directories](https://docs.pytest.org/en/stable/how-to/tmp_path.html) +- [Monkeypatching](https://docs.pytest.org/en/stable/how-to/monkeypatch.html) +- [Logging](https://docs.pytest.org/en/stable/how-to/logging.html) +- [Warnings](https://docs.pytest.org/en/stable/how-to/capture-warnings.html) +- [Skip and xfail](https://docs.pytest.org/en/stable/how-to/skipping.html) +- [API reference](https://docs.pytest.org/en/stable/reference/reference.html) +- [pytest changelog](https://docs.pytest.org/en/stable/changelog.html) +- [pytest on PyPI](https://pypi.org/project/pytest/) + +No momento em que este capítulo foi preparado, o PyPI listava pytest 9.1.1 como release estável mais recente. O currículo mira a série 9.1.x em vez do draft não lançado 9.2 ou de uma versão futura sem limite. + +## 97. Fase 9 concluída + +A Fase 9 agora conecta quatro fronteiras importantes de terceiros: + +```text +pandas -> transform tabular data +openpyxl -> construct and maintain Excel workbooks +requests -> communicate with HTTP services +pytest -> verify behavior repeatedly and automatically +``` + +Isso encerra a **Fase 9: Bibliotecas Externas**. + +A próxima fase sai de habilidades isoladas de bibliotecas e entra em trabalho integrado de portfólio: **Fase 10: Projetos Práticos**. + +Antes de avançar, pratique escrevendo testes que tornem falhas informativas. Uma suíte é mais valiosa quando dá confiança para alterar o código, e não quando apenas produz um número verde. diff --git a/external-libraries/04-pytest/examples/assertions_and_parametrize.py b/external-libraries/04-pytest/examples/assertions_and_parametrize.py new file mode 100644 index 0000000..070407d --- /dev/null +++ b/external-libraries/04-pytest/examples/assertions_and_parametrize.py @@ -0,0 +1,56 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + + +TEST_SOURCE = ''' +import pytest + + +def normalize_score(score: int) -> int: + return max(0, min(score, 100)) + + +def test_normalize_score_keeps_valid_value() -> None: + assert normalize_score(82) == 82 + + +@pytest.mark.parametrize( + ("raw", "expected"), + [(-5, 0), (40, 40), (130, 100)], +) +def test_normalize_score_boundaries(raw: int, expected: int) -> None: + assert normalize_score(raw) == expected +''' + + +class ResultCounter: + def __init__(self) -> None: + self.passed = 0 + + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + if report.when == "call" and report.passed: + self.passed += 1 + + +def run_suite() -> tuple[int, int]: + with TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test_scores.py" + test_file.write_text(TEST_SOURCE, encoding="utf-8") + counter = ResultCounter() + captured = StringIO() + with redirect_stdout(captured), redirect_stderr(captured): + exit_code = pytest.main( + [str(test_file), "-q", "-p", "no:cacheprovider"], + plugins=[counter], + ) + return int(exit_code), counter.passed + + +if __name__ == "__main__": + exit_code, passed = run_suite() + print(f"exit code: {exit_code}") + print(f"passed: {passed}") diff --git a/external-libraries/04-pytest/examples/capture_output_and_logs.py b/external-libraries/04-pytest/examples/capture_output_and_logs.py new file mode 100644 index 0000000..6bbb978 --- /dev/null +++ b/external-libraries/04-pytest/examples/capture_output_and_logs.py @@ -0,0 +1,62 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + + +TEST_SOURCE = ''' +import logging + + +def announce(topic: str) -> None: + print(f"Studying: {topic}") + + +def record_status(logger: logging.Logger) -> None: + logger.info("study session ready") + + +def test_stdout_capture(capsys) -> None: + announce("pytest") + captured = capsys.readouterr() + assert captured.out == "Studying: pytest\\n" + assert captured.err == "" + + +def test_log_capture(caplog) -> None: + logger = logging.getLogger("study.example") + with caplog.at_level(logging.INFO, logger="study.example"): + record_status(logger) + assert "study session ready" in caplog.text +''' + + +class ResultCounter: + def __init__(self) -> None: + self.passed = 0 + + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + if report.when == "call" and report.passed: + self.passed += 1 + + +def run_suite() -> tuple[int, int]: + with TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test_capture.py" + test_file.write_text(TEST_SOURCE, encoding="utf-8") + counter = ResultCounter() + captured = StringIO() + with redirect_stdout(captured), redirect_stderr(captured): + exit_code = pytest.main( + [str(test_file), "-q", "-p", "no:cacheprovider"], + plugins=[counter], + ) + return int(exit_code), counter.passed + + +if __name__ == "__main__": + exit_code, passed = run_suite() + print(f"exit code: {exit_code}") + print(f"passed: {passed}") diff --git a/external-libraries/04-pytest/examples/exceptions_and_warnings.py b/external-libraries/04-pytest/examples/exceptions_and_warnings.py new file mode 100644 index 0000000..8a2eacd --- /dev/null +++ b/external-libraries/04-pytest/examples/exceptions_and_warnings.py @@ -0,0 +1,64 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + + +TEST_SOURCE = ''' +import warnings + +import pytest + + +def parse_positive(value: str) -> int: + number = int(value) + if number <= 0: + raise ValueError("value must be positive") + return number + + +def legacy_parser() -> str: + warnings.warn("legacy parser", DeprecationWarning, stacklevel=2) + return "ok" + + +def test_invalid_value_raises() -> None: + with pytest.raises(ValueError, match="must be positive"): + parse_positive("0") + + +def test_warning_is_explicit() -> None: + with pytest.warns(DeprecationWarning, match="legacy parser"): + assert legacy_parser() == "ok" +''' + + +class ResultCounter: + def __init__(self) -> None: + self.passed = 0 + + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + if report.when == "call" and report.passed: + self.passed += 1 + + +def run_suite() -> tuple[int, int]: + with TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test_failures.py" + test_file.write_text(TEST_SOURCE, encoding="utf-8") + counter = ResultCounter() + captured = StringIO() + with redirect_stdout(captured), redirect_stderr(captured): + exit_code = pytest.main( + [str(test_file), "-q", "-p", "no:cacheprovider"], + plugins=[counter], + ) + return int(exit_code), counter.passed + + +if __name__ == "__main__": + exit_code, passed = run_suite() + print(f"exit code: {exit_code}") + print(f"passed: {passed}") diff --git a/external-libraries/04-pytest/examples/fixtures_and_tmp_path.py b/external-libraries/04-pytest/examples/fixtures_and_tmp_path.py new file mode 100644 index 0000000..46c237f --- /dev/null +++ b/external-libraries/04-pytest/examples/fixtures_and_tmp_path.py @@ -0,0 +1,60 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + + +TEST_SOURCE = ''' +from pathlib import Path + +import pytest + + +@pytest.fixture +def study_file(tmp_path: Path) -> Path: + path = tmp_path / "topics.txt" + path.write_text("functions\\npytest\\n", encoding="utf-8") + return path + + +def test_fixture_creates_file(study_file: Path) -> None: + assert study_file.exists() + + +def test_fixture_content(study_file: Path) -> None: + assert study_file.read_text(encoding="utf-8").splitlines() == [ + "functions", + "pytest", + ] +''' + + +class ResultCounter: + def __init__(self) -> None: + self.passed = 0 + + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + if report.when == "call" and report.passed: + self.passed += 1 + + +def run_suite() -> tuple[int, int]: + with TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test_files.py" + test_file.write_text(TEST_SOURCE, encoding="utf-8") + counter = ResultCounter() + captured = StringIO() + with redirect_stdout(captured), redirect_stderr(captured): + exit_code = pytest.main( + [str(test_file), "-q", "-p", "no:cacheprovider"], + plugins=[counter], + ) + return int(exit_code), counter.passed + + +if __name__ == "__main__": + exit_code, passed = run_suite() + print(f"exit code: {exit_code}") + print(f"passed: {passed}") diff --git a/external-libraries/04-pytest/examples/monkeypatch_environment.py b/external-libraries/04-pytest/examples/monkeypatch_environment.py new file mode 100644 index 0000000..d87b19f --- /dev/null +++ b/external-libraries/04-pytest/examples/monkeypatch_environment.py @@ -0,0 +1,54 @@ +from contextlib import redirect_stderr, redirect_stdout +from io import StringIO +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + + +TEST_SOURCE = ''' +import os + + +def read_mode() -> str: + return os.getenv("STUDY_MODE", "default") + + +def test_default_mode(monkeypatch) -> None: + monkeypatch.delenv("STUDY_MODE", raising=False) + assert read_mode() == "default" + + +def test_configured_mode(monkeypatch) -> None: + monkeypatch.setenv("STUDY_MODE", "focused") + assert read_mode() == "focused" +''' + + +class ResultCounter: + def __init__(self) -> None: + self.passed = 0 + + def pytest_runtest_logreport(self, report: pytest.TestReport) -> None: + if report.when == "call" and report.passed: + self.passed += 1 + + +def run_suite() -> tuple[int, int]: + with TemporaryDirectory() as temp_dir: + test_file = Path(temp_dir) / "test_environment.py" + test_file.write_text(TEST_SOURCE, encoding="utf-8") + counter = ResultCounter() + captured = StringIO() + with redirect_stdout(captured), redirect_stderr(captured): + exit_code = pytest.main( + [str(test_file), "-q", "-p", "no:cacheprovider"], + plugins=[counter], + ) + return int(exit_code), counter.passed + + +if __name__ == "__main__": + exit_code, passed = run_suite() + print(f"exit code: {exit_code}") + print(f"passed: {passed}") diff --git a/external-libraries/README.es.md b/external-libraries/README.es.md index 32d2be5..fe77ced 100644 --- a/external-libraries/README.es.md +++ b/external-libraries/README.es.md @@ -14,23 +14,23 @@ Las bibliotecas externas agregan una nueva responsabilidad de ingeniería: **con ## Estado -> 🚧 **En progreso** +> ✅ **Completada** ## Ruta de aprendizaje 1. ✅ [`pandas`: Trabajando con Datos Tabulares](01-pandas/README.es.md) 2. ✅ [`openpyxl`: Automatizando Libros de Excel](02-openpyxl/README.es.md) 3. ✅ [`requests`: Consumiendo APIs HTTP](03-requests/README.es.md) -4. ⏳ `pytest`: pruebas automatizadas +4. ✅ [`pytest`: Ingeniería de Pruebas Automatizadas](04-pytest/README.es.md) ## Contrato de dependencias Los ejemplos ejecutables publicados en esta fase usan las dependencias declaradas en [`requirements-external.txt`](../requirements-external.txt). El CI del repositorio instala ese archivo antes de ejecutar los ejemplos aprobados de bibliotecas externas. -Los contratos actuales apuntan a **pandas 3.0.x**, **openpyxl 3.1.x** y **Requests 2.34.x**. pandas 3.0 soporta Python 3.11+, PyPI declara Python 3.8+ para openpyxl 3.1.5 y Requests 2.34.2 requiere Python 3.10+. Este repositorio valida los ejemplos en Python 3.13. +Los contratos publicados apuntan a **pandas 3.0.x**, **openpyxl 3.1.x**, **Requests 2.34.x** y **pytest 9.1.x**. pandas 3.0 soporta Python 3.11+, PyPI declara Python 3.8+ para openpyxl 3.1.5, y Requests 2.34.2 y pytest 9.1.1 requieren Python 3.10+. Este repositorio valida los ejemplos con Python 3.13. -## Por qué esta fase viene ahora +## Lo que estableció esta fase -Las fases anteriores establecieron colecciones, funciones, errores, archivos, módulos, CSV/JSON, fechas, rutas, logging, iteración, aritmética decimal y contratos de filesystem. Las bibliotecas externas deben construir sobre esas habilidades, no sustituirlas. +La Fase 9 pasó de la biblioteca estándar a cuatro fronteras de ingeniería con terceros: transformación de datos tabulares, automatización de libros de Excel, clientes HTTP/API y pruebas automatizadas. Cada capítulo trata la biblioteca como una dependencia versionada con contratos explícitos de comportamiento, seguridad y validación. -El próximo capítulo planificado es **`pytest`**. +La siguiente fase es **Fase 10: Proyectos Prácticos**. diff --git a/external-libraries/README.md b/external-libraries/README.md index 4121237..cbea540 100644 --- a/external-libraries/README.md +++ b/external-libraries/README.md @@ -14,23 +14,23 @@ External libraries add a new engineering responsibility: **dependency contracts* ## Status -> 🚧 **In progress** +> ✅ **Complete** ## Learning path 1. ✅ [`pandas`: Working with Tabular Data](01-pandas/README.md) 2. ✅ [`openpyxl`: Automating Excel Workbooks](02-openpyxl/README.md) 3. ✅ [`requests`: Consuming HTTP APIs](03-requests/README.md) -4. ⏳ `pytest`: automated testing +4. ✅ [`pytest`: Engineering Automated Tests](04-pytest/README.md) ## Dependency contract Published executable examples from this phase use the dependencies declared in [`requirements-external.txt`](../requirements-external.txt). Repository CI installs that file before executing approved external-library examples. -The current contracts target **pandas 3.0.x**, **openpyxl 3.1.x**, and **Requests 2.34.x**. pandas 3.0 supports Python 3.11+, PyPI declares Python 3.8+ for openpyxl 3.1.5, and Requests 2.34.2 requires Python 3.10+. This repository validates the examples on Python 3.13. +The published contracts target **pandas 3.0.x**, **openpyxl 3.1.x**, **Requests 2.34.x**, and **pytest 9.1.x**. pandas 3.0 supports Python 3.11+, PyPI declares Python 3.8+ for openpyxl 3.1.5, and Requests 2.34.2 plus pytest 9.1.1 require Python 3.10+. This repository validates the examples on Python 3.13. -## Why this phase comes now +## What this phase established -The earlier phases established collections, functions, errors, files, modules, CSV/JSON, dates, paths, logging, iteration, decimal arithmetic, and filesystem contracts. External libraries should build on those skills rather than replace them. +Phase 9 moved from the Python standard library into four third-party engineering boundaries: tabular-data transformation, Excel workbook automation, HTTP/API clients, and automated testing. Each chapter treats the library as a versioned dependency with explicit behavior, safety, and validation contracts. -The next planned chapter is **`pytest`**. +The next phase is **Phase 10: Practical Projects**. diff --git a/external-libraries/README.pt-BR.md b/external-libraries/README.pt-BR.md index 8bdad01..58a5382 100644 --- a/external-libraries/README.pt-BR.md +++ b/external-libraries/README.pt-BR.md @@ -14,23 +14,23 @@ Bibliotecas externas acrescentam uma nova responsabilidade de engenharia: **cont ## Status -> 🚧 **Em andamento** +> ✅ **Concluída** ## Trilha de aprendizagem 1. ✅ [`pandas`: Trabalhando com Dados Tabulares](01-pandas/README.pt-BR.md) 2. ✅ [`openpyxl`: Automatizando Workbooks do Excel](02-openpyxl/README.pt-BR.md) 3. ✅ [`requests`: Consumindo APIs HTTP](03-requests/README.pt-BR.md) -4. ⏳ `pytest`: testes automatizados +4. ✅ [`pytest`: Engenharia de Testes Automatizados](04-pytest/README.pt-BR.md) ## Contrato de dependências Os exemplos executáveis publicados nesta fase usam as dependências declaradas em [`requirements-external.txt`](../requirements-external.txt). O CI do repositório instala esse arquivo antes de executar os exemplos aprovados de bibliotecas externas. -Os contratos atuais têm como alvo **pandas 3.0.x**, **openpyxl 3.1.x** e **Requests 2.34.x**. O pandas 3.0 suporta Python 3.11+, o PyPI declara Python 3.8+ para openpyxl 3.1.5 e Requests 2.34.2 exige Python 3.10+. Este repositório valida os exemplos em Python 3.13. +Os contratos publicados têm como alvo **pandas 3.0.x**, **openpyxl 3.1.x**, **Requests 2.34.x** e **pytest 9.1.x**. O pandas 3.0 suporta Python 3.11+, o PyPI declara Python 3.8+ para openpyxl 3.1.5, e Requests 2.34.2 e pytest 9.1.1 exigem Python 3.10+. Este repositório valida os exemplos em Python 3.13. -## Por que esta fase vem agora +## O que esta fase estabeleceu -As fases anteriores estabeleceram coleções, funções, erros, arquivos, módulos, CSV/JSON, datas, caminhos, logging, iteração, aritmética decimal e contratos de filesystem. Bibliotecas externas devem construir sobre essas habilidades, não substituí-las. +A Fase 9 saiu da biblioteca padrão e entrou em quatro fronteiras de engenharia com terceiros: transformação de dados tabulares, automação de workbooks do Excel, clientes HTTP/API e testes automatizados. Cada capítulo trata a biblioteca como uma dependência versionada com contratos explícitos de comportamento, segurança e validação. -O próximo capítulo planejado é **`pytest`**. +A próxima fase é **Fase 10: Projetos Práticos**. diff --git a/requirements-external.txt b/requirements-external.txt index 931a56a..5aa86e2 100644 --- a/requirements-external.txt +++ b/requirements-external.txt @@ -3,3 +3,4 @@ pandas>=3.0,<3.1 openpyxl>=3.1,<3.2 requests>=2.34,<2.35 +pytest>=9.1,<9.2 diff --git a/scripts/example_manifest.txt b/scripts/example_manifest.txt index 61fae5d..0c59437 100644 --- a/scripts/example_manifest.txt +++ b/scripts/example_manifest.txt @@ -173,3 +173,8 @@ external-libraries/03-requests/examples/http_error_handling.py external-libraries/03-requests/examples/post_json.py external-libraries/03-requests/examples/session_defaults.py external-libraries/03-requests/examples/stream_download.py +external-libraries/04-pytest/examples/assertions_and_parametrize.py +external-libraries/04-pytest/examples/capture_output_and_logs.py +external-libraries/04-pytest/examples/exceptions_and_warnings.py +external-libraries/04-pytest/examples/fixtures_and_tmp_path.py +external-libraries/04-pytest/examples/monkeypatch_environment.py