Python

Artigo 43 — Testes com pytest: fixtures, mocks e cobertura Já leu

16 min de leitura

Artigo 43 — Testes com pytest: fixtures, mocks e cobertura
Código sem testes é código que você não confia. Testes automatizados são a rede de segurança que permite refatorar, adicionar funcionalidades e corrigir bugs

Artigo 43 — Testes com pytest: fixtures, mocks e cobertura

Prof. Ricardo Matos Módulo 8 · Testes, Qualidade e Boas Práticas · Artigo 43 de 52


Introdução

Código sem testes é código que você não confia. Testes automatizados são a rede de segurança que permite refatorar, adicionar funcionalidades e corrigir bugs com confiança. pytest é o framework de testes dominante em Python — mais expressivo que o unittest da biblioteca padrão, com fixtures poderosas, plugins ricos e uma sintaxe que torna os testes legíveis como documentação viva do sistema.


Instalação

pip install pytest pytest-cov pytest-mock pytest-asyncio faker

Estrutura de um Projeto com Testes

projeto/
├── src/
│   └── escola/
│       ├── __init__.py
│       ├── aluno.py
│       ├── turma.py
│       ├── calculadora.py
│       └── servicos.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py          ← fixtures compartilhadas
│   ├── unit/
│   │   ├── test_aluno.py
│   │   ├── test_turma.py
│   │   └── test_calculadora.py
│   ├── integration/
│   │   └── test_servicos.py
│   └── e2e/
│       └── test_api.py
├── pyproject.toml
└── pytest.ini
# pytest.ini
[pytest]
testpaths      = tests
python_files   = test_*.py
python_classes = Test*
python_functions = test_*
addopts        = -v --tb=short
markers        =
    unit: testes unitários
    integration: testes de integração
    slow: testes lentos

Código a ser testado

# src/escola/calculadora.py
from typing import List


def calcular_media(notas: List[float]) -> float:
    """Calcula a média aritmética de uma lista de notas."""
    if not notas:
        raise ValueError("A lista de notas não pode ser vazia.")
    if any(n < 0 or n > 10 for n in notas):
        raise ValueError("Notas devem estar entre 0 e 10.")
    return sum(notas) / len(notas)


def calcular_conceito(media: float) -> str:
    """Retorna o conceito baseado na média."""
    if media < 0 or media > 10:
        raise ValueError(f"Média inválida: {media}")
    if media >= 9:   return "A"
    if media >= 7:   return "B"
    if media >= 6:   return "C"
    if media >= 5:   return "D"
    return "F"


def calcular_situacao(notas: List[float], min_faltas: int = 25) -> dict:
    """Determina a situação do aluno."""
    media      = calcular_media(notas)
    conceito   = calcular_conceito(media)
    aprovado   = media >= 6.0 and min_faltas < 25

    return {
        "media":    round(media, 2),
        "conceito": conceito,
        "aprovado": aprovado,
        "situacao": "Aprovado" if aprovado else "Reprovado"
    }
# src/escola/aluno.py
from dataclasses import dataclass, field
from typing import List
from datetime import datetime


@dataclass
class Aluno:
    nome:    str
    email:   str
    turma:   str
    notas:   List[float] = field(default_factory=list)
    ativo:   bool = True
    criado_em: datetime = field(default_factory=datetime.utcnow)

    def __post_init__(self):
        if not self.nome or len(self.nome) < 2:
            raise ValueError("Nome deve ter ao menos 2 caracteres.")
        if "@" not in self.email:
            raise ValueError("E-mail inválido.")
        self.nome = self.nome.strip().title()

    def adicionar_nota(self, nota: float):
        if not 0 <= nota <= 10:
            raise ValueError(f"Nota inválida: {nota}")
        self.notas.append(nota)

    @property
    def media(self) -> float:
        if not self.notas:
            return 0.0
        return sum(self.notas) / len(self.notas)

    @property
    def aprovado(self) -> bool:
        return self.media >= 6.0

    def desativar(self):
        self.ativo = False
# src/escola/servicos.py
from typing import List, Optional
from .aluno import Aluno


class RepositorioAlunos:
    """Repositório em memória — em produção usaria banco de dados."""

    def __init__(self):
        self._dados: dict = {}
        self._proximo_id: int = 1

    def salvar(self, aluno: Aluno) -> int:
        aluno_id = self._proximo_id
        self._dados[aluno_id] = aluno
        self._proximo_id += 1
        return aluno_id

    def buscar(self, aluno_id: int) -> Optional[Aluno]:
        return self._dados.get(aluno_id)

    def buscar_por_email(self, email: str) -> Optional[Aluno]:
        return next(
            (a for a in self._dados.values() if a.email == email),
            None
        )

    def listar_todos(self) -> List[Aluno]:
        return list(self._dados.values())

    def deletar(self, aluno_id: int) -> bool:
        if aluno_id in self._dados:
            del self._dados[aluno_id]
            return True
        return False


class ServicoNotificacao:
    """Serviço de notificação — interface para injeção de dependência."""

    def notificar(self, email: str, mensagem: str) -> bool:
        raise NotImplementedError


class ServicoAluno:
    def __init__(
        self,
        repositorio: RepositorioAlunos,
        notificacao: ServicoNotificacao
    ):
        self.repo   = repositorio
        self.notify = notificacao

    def cadastrar(self, nome: str, email: str, turma: str) -> int:
        if self.repo.buscar_por_email(email):
            raise ValueError(f"E-mail já cadastrado: {email}")
        aluno    = Aluno(nome=nome, email=email, turma=turma)
        aluno_id = self.repo.salvar(aluno)
        self.notify.notificar(email, f"Bem-vindo(a), {aluno.nome}!")
        return aluno_id

    def lancar_nota(self, aluno_id: int, nota: float):
        aluno = self.repo.buscar(aluno_id)
        if not aluno:
            raise ValueError(f"Aluno {aluno_id} não encontrado.")
        aluno.adicionar_nota(nota)

        if aluno.aprovado:
            self.notify.notificar(aluno.email, "Parabéns! Você está aprovado.")

    def relatorio_turma(self, turma: str) -> dict:
        alunos     = [a for a in self.repo.listar_todos() if a.turma == turma]
        aprovados  = [a for a in alunos if a.aprovado]
        return {
            "turma":      turma,
            "total":      len(alunos),
            "aprovados":  len(aprovados),
            "reprovados": len(alunos) - len(aprovados),
            "media_geral": round(
                sum(a.media for a in alunos) / len(alunos), 2
            ) if alunos else 0.0
        }

Testes Básicos

# tests/unit/test_calculadora.py
import pytest
from escola.calculadora import calcular_media, calcular_conceito, calcular_situacao


class TestCalcularMedia:

    def test_media_simples(self):
        assert calcular_media([8.0, 9.0, 7.0]) == pytest.approx(8.0)

    def test_media_unico_valor(self):
        assert calcular_media([7.5]) == 7.5

    def test_media_zeros(self):
        assert calcular_media([0.0, 0.0, 0.0]) == 0.0

    def test_media_maxima(self):
        assert calcular_media([10.0, 10.0]) == 10.0

    def test_lista_vazia_levanta_erro(self):
        with pytest.raises(ValueError, match="não pode ser vazia"):
            calcular_media([])

    def test_nota_negativa_levanta_erro(self):
        with pytest.raises(ValueError, match="entre 0 e 10"):
            calcular_media([-1.0, 8.0])

    def test_nota_acima_de_10_levanta_erro(self):
        with pytest.raises(ValueError):
            calcular_media([11.0])

    def test_precisao_decimal(self):
        resultado = calcular_media([7.1, 7.2, 7.3])
        assert resultado == pytest.approx(7.2, rel=1e-3)


class TestCalcularConceito:

    @pytest.mark.parametrize("media,conceito_esperado", [
        (9.5, "A"),
        (9.0, "A"),
        (8.9, "B"),
        (7.0, "B"),
        (6.9, "C"),
        (6.0, "C"),
        (5.9, "D"),
        (5.0, "D"),
        (4.9, "F"),
        (0.0, "F"),
    ])
    def test_conceitos(self, media, conceito_esperado):
        assert calcular_conceito(media) == conceito_esperado

    def test_media_invalida_negativa(self):
        with pytest.raises(ValueError):
            calcular_conceito(-0.1)

    def test_media_invalida_acima_10(self):
        with pytest.raises(ValueError):
            calcular_conceito(10.1)

Fixtures

# tests/conftest.py
import pytest
from faker import Faker
from escola.aluno import Aluno
from escola.servicos import RepositorioAlunos, ServicoNotificacao, ServicoAluno

fake = Faker("pt_BR")


@pytest.fixture
def aluno_simples():
    """Aluno básico para testes."""
    return Aluno(
        nome="Ana Silva",
        email="ana@email.com",
        turma="A"
    )


@pytest.fixture
def aluno_com_notas():
    """Aluno com notas já lançadas."""
    aluno = Aluno(nome="Bruno Costa", email="bruno@email.com", turma="B")
    for nota in [7.0, 8.5, 9.0, 6.5]:
        aluno.adicionar_nota(nota)
    return aluno


@pytest.fixture
def repositorio():
    """Repositório limpo para cada teste."""
    return RepositorioAlunos()


@pytest.fixture
def notificacao_mock(mocker):
    """Mock do serviço de notificação."""
    mock = mocker.MagicMock(spec=ServicoNotificacao)
    mock.notificar.return_value = True
    return mock


@pytest.fixture
def servico(repositorio, notificacao_mock):
    """Serviço de aluno com dependências mockadas."""
    return ServicoAluno(repositorio, notificacao_mock)


@pytest.fixture
def turma_populada(repositorio, notificacao_mock):
    """Repositório com múltiplos alunos."""
    servico = ServicoAluno(repositorio, notificacao_mock)
    dados = [
        ("Ana Silva",   "ana@email.com",   "A", [9.0, 8.5, 9.5]),
        ("Bruno Costa", "bruno@email.com", "A", [5.0, 6.0, 5.5]),
        ("Carla Souza", "carla@email.com", "B", [7.0, 8.0, 7.5]),
        ("Diego Lima",  "diego@email.com", "B", [4.0, 3.5, 5.0]),
    ]
    ids = []
    for nome, email, turma, notas in dados:
        aid = servico.cadastrar(nome, email, turma)
        for nota in notas:
            servico.lancar_nota(aid, nota)
        ids.append(aid)
    return servico, ids


@pytest.fixture
def aluno_faker():
    """Aluno com dados gerados aleatoriamente."""
    return Aluno(
        nome=fake.name(),
        email=fake.email(),
        turma=fake.random_element(["A", "B", "C"])
    )

Testes com Mocks

# tests/unit/test_servicos.py
import pytest
from escola.servicos import ServicoAluno
from escola.aluno import Aluno


class TestServicoAluno:

    def test_cadastrar_aluno(self, servico, notificacao_mock):
        aluno_id = servico.cadastrar("Ana Silva", "ana@email.com", "A")

        assert aluno_id == 1
        aluno = servico.repo.buscar(aluno_id)
        assert aluno.nome  == "Ana Silva"
        assert aluno.email == "ana@email.com"

        # Verifica que notificação foi enviada
        notificacao_mock.notificar.assert_called_once_with(
            "ana@email.com", "Bem-vindo(a), Ana Silva!"
        )

    def test_cadastrar_email_duplicado(self, servico):
        servico.cadastrar("Ana Silva", "ana@email.com", "A")

        with pytest.raises(ValueError, match="já cadastrado"):
            servico.cadastrar("Ana Outra", "ana@email.com", "B")

    def test_lancar_nota_aprovado_notifica(self, servico, notificacao_mock):
        aluno_id = servico.cadastrar("Ana Silva", "ana@email.com", "A")
        notificacao_mock.reset_mock()

        servico.lancar_nota(aluno_id, 9.5)

        # Deve notificar aprovação
        notificacao_mock.notificar.assert_called_with(
            "ana@email.com", "Parabéns! Você está aprovado."
        )

    def test_lancar_nota_reprovado_nao_notifica(self, servico, notificacao_mock):
        aluno_id = servico.cadastrar("Bruno Costa", "bruno@email.com", "A")
        notificacao_mock.reset_mock()

        servico.lancar_nota(aluno_id, 3.0)

        # Só foi chamada no cadastro — não na nota baixa
        notificacao_mock.notificar.assert_not_called()

    def test_lancar_nota_aluno_inexistente(self, servico):
        with pytest.raises(ValueError, match="não encontrado"):
            servico.lancar_nota(999, 8.0)

    def test_relatorio_turma(self, turma_populada):
        servico, _ = turma_populada
        relatorio  = servico.relatorio_turma("A")

        assert relatorio["turma"]      == "A"
        assert relatorio["total"]      == 2
        assert relatorio["aprovados"]  == 1
        assert relatorio["reprovados"] == 1

    def test_relatorio_turma_vazia(self, servico):
        relatorio = servico.relatorio_turma("Z")
        assert relatorio["total"]       == 0
        assert relatorio["media_geral"] == 0.0


class TestAluno:

    def test_nome_normalizado(self):
        aluno = Aluno(nome="  ana silva  ", email="ana@email.com", turma="A")
        assert aluno.nome == "Ana Silva"

    def test_nome_invalido(self):
        with pytest.raises(ValueError, match="2 caracteres"):
            Aluno(nome="A", email="a@email.com", turma="A")

    def test_email_invalido(self):
        with pytest.raises(ValueError, match="E-mail inválido"):
            Aluno(nome="Ana Silva", email="invalido", turma="A")

    def test_media_sem_notas(self, aluno_simples):
        assert aluno_simples.media == 0.0

    def test_media_com_notas(self, aluno_com_notas):
        assert aluno_com_notas.media == pytest.approx(7.75)

    def test_aprovado_com_media_suficiente(self, aluno_com_notas):
        assert aluno_com_notas.aprovado is True

    def test_reprovado_com_media_insuficiente(self, aluno_simples):
        aluno_simples.adicionar_nota(4.0)
        assert aluno_simples.aprovado is False

    def test_nota_invalida(self, aluno_simples):
        with pytest.raises(ValueError):
            aluno_simples.adicionar_nota(11.0)

    def test_desativar(self, aluno_simples):
        aluno_simples.desativar()
        assert aluno_simples.ativo is False

Testes Parametrizados Avançados

# tests/unit/test_parametrizados.py
import pytest
from escola.calculadora import calcular_situacao


class TestCalcularSituacao:

    @pytest.mark.parametrize("notas,faltas,aprovado_esperado", [
        ([8.0, 9.0, 7.0], 5,  True),   # nota ok, faltas ok
        ([8.0, 9.0, 7.0], 25, False),  # nota ok, muitas faltas
        ([4.0, 5.0, 3.0], 5,  False),  # nota baixa, faltas ok
        ([4.0, 5.0, 3.0], 30, False),  # nota baixa, muitas faltas
        ([6.0, 6.0, 6.0], 0,  True),   # nota mínima, sem faltas
        ([5.9, 6.0, 6.1], 10, True),   # média exatamente 6.0
    ])
    def test_situacao_aprovacao(self, notas, faltas, aprovado_esperado):
        resultado = calcular_situacao(notas, min_faltas=faltas)
        assert resultado["aprovado"] == aprovado_esperado

    @pytest.mark.parametrize("notas,conceito_esperado", [
        ([10.0, 10.0, 10.0], "A"),
        ([9.0,  8.0,  8.5],  "B"),
        ([6.5,  6.0,  6.0],  "C"),
        ([5.5,  5.0,  5.0],  "D"),
        ([3.0,  2.0,  4.0],  "F"),
    ])
    def test_conceito_calculado(self, notas, conceito_esperado):
        resultado = calcular_situacao(notas)
        assert resultado["conceito"] == conceito_esperado

    @pytest.mark.parametrize("notas_invalidas", [
        [],
        [-1.0, 5.0],
        [11.0, 5.0],
    ])
    def test_entradas_invalidas(self, notas_invalidas):
        with pytest.raises(ValueError):
            calcular_situacao(notas_invalidas)

Testes de API com TestClient

# tests/integration/test_api.py
import pytest
from fastapi.testclient import TestClient
from unittest.mock import MagicMock, patch


# Assumindo que a API do artigo 25 está em app.main
@pytest.fixture
def client():
    from app.main import criar_app
    app = criar_app(ambiente="testes")
    return TestClient(app)


class TestAPIAlunos:

    def test_listar_alunos_vazio(self, client):
        resp = client.get("/alunos")
        assert resp.status_code == 200
        assert resp.json()["total"] == 0

    def test_criar_aluno(self, client):
        resp = client.post("/alunos", json={
            "nome":  "Ana Silva",
            "email": "ana@email.com",
            "nota":  9.5
        })
        assert resp.status_code == 201
        dados = resp.json()
        assert dados["nome"]  == "Ana Silva"
        assert dados["email"] == "ana@email.com"
        assert "id" in dados

    def test_criar_aluno_dados_invalidos(self, client):
        resp = client.post("/alunos", json={
            "nome":  "A",
            "email": "invalido",
            "nota":  15.0
        })
        assert resp.status_code == 422

    def test_buscar_aluno_inexistente(self, client):
        resp = client.get("/alunos/9999")
        assert resp.status_code == 404

    def test_atualizar_nota(self, client):
        # Cria
        resp_criar = client.post("/alunos", json={
            "nome": "Bruno Costa", "email": "bruno@email.com", "nota": 5.0
        })
        aluno_id = resp_criar.json()["id"]

        # Atualiza
        resp_patch = client.patch(
            f"/alunos/{aluno_id}",
            json={"nota": 8.5}
        )
        assert resp_patch.status_code == 200
        assert resp_patch.json()["nota"] == 8.5

    def test_deletar_aluno(self, client):
        resp_criar = client.post("/alunos", json={
            "nome": "Carla Souza", "email": "carla@email.com", "nota": 7.0
        })
        aluno_id = resp_criar.json()["id"]

        resp_del = client.delete(f"/alunos/{aluno_id}")
        assert resp_del.status_code == 204

        resp_get = client.get(f"/alunos/{aluno_id}")
        assert resp_get.status_code == 404

Testes Assíncronos

# tests/integration/test_async.py
import pytest
import pytest_asyncio
import httpx
from unittest.mock import AsyncMock, patch


@pytest.mark.asyncio
async def test_buscar_cep():
    """Testa cliente HTTP assíncrono com mock."""
    resposta_mock = {
        "cep":        "01310-100",
        "logradouro": "Avenida Paulista",
        "localidade": "São Paulo",
        "uf":         "SP"
    }

    with patch("httpx.AsyncClient.get") as mock_get:
        mock_get.return_value = AsyncMock()
        mock_get.return_value.json.return_value = resposta_mock
        mock_get.return_value.status_code = 200

        async with httpx.AsyncClient() as client:
            resp = await client.get("https://viacep.com.br/ws/01310100/json/")
            dados = resp.json()

        assert dados["localidade"] == "São Paulo"
        assert dados["uf"]         == "SP"


@pytest.mark.asyncio
async def test_tarefa_concorrente():
    """Testa execução de múltiplas corrotinas."""
    import asyncio

    async def tarefa(n: int, delay: float) -> int:
        await asyncio.sleep(delay)
        return n * 2

    resultados = await asyncio.gather(
        tarefa(1, 0.01),
        tarefa(2, 0.01),
        tarefa(3, 0.01),
    )
    assert resultados == [2, 4, 6]

Cobertura de Testes

# Executando testes com cobertura
pytest --cov=src/escola --cov-report=term-missing --cov-report=html

# Saída esperada:
# Name                          Stmts   Miss  Cover
# -------------------------------------------------
# src/escola/aluno.py              28      0   100%
# src/escola/calculadora.py        18      0   100%
# src/escola/servicos.py           35      2    94%
# -------------------------------------------------
# TOTAL                            81      2    97%

# Gerando relatório HTML
pytest --cov=src --cov-report=html:htmlcov
# Abrir: htmlcov/index.html
# pyproject.toml — configuração de cobertura
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

[tool.coverage.run]
source = ["src"]
omit   = ["*/migrations/*", "*/tests/*", "*/__init__.py"]

[tool.coverage.report]
fail_under = 85        # falha se cobertura < 85%
show_missing = true
skip_covered = false

Boas Práticas de Testes

# Padrão AAA — Arrange, Act, Assert
def test_lancar_nota_padrao_aaa(servico, notificacao_mock):
    # ARRANGE — prepara o estado
    aluno_id = servico.cadastrar("Ana Silva", "ana@email.com", "A")
    notificacao_mock.reset_mock()

    # ACT — executa a ação
    servico.lancar_nota(aluno_id, 9.5)

    # ASSERT — verifica o resultado
    aluno = servico.repo.buscar(aluno_id)
    assert aluno.notas == [9.5]
    assert aluno.aprovado is True


# Testes independentes — nunca dependem da ordem
def test_independente_1(repositorio):
    assert len(repositorio.listar_todos()) == 0   # sempre começa vazio

def test_independente_2(repositorio):
    assert len(repositorio.listar_todos()) == 0   # idem — fixture é recriada


# Nomes descritivos — o nome é a documentação
class TestServicoAluno:
    def test_cadastrar_aluno_com_email_duplicado_levanta_valor_error(self, servico):
        servico.cadastrar("Ana", "ana@email.com", "A")
        with pytest.raises(ValueError):
            servico.cadastrar("Ana 2", "ana@email.com", "B")

    def test_media_de_aluno_sem_notas_retorna_zero(self, aluno_simples):
        assert aluno_simples.media == 0.0

    def test_nota_acima_do_maximo_nao_e_aceita(self, aluno_simples):
        with pytest.raises(ValueError):
            aluno_simples.adicionar_nota(10.1)


# Markers para organizar e pular testes
@pytest.mark.slow
@pytest.mark.integration
def test_integracao_banco(db_real):
    pass   # só roda com --run-slow


@pytest.mark.skip(reason="Feature ainda não implementada")
def test_feature_futura():
    pass


@pytest.mark.skipif(
    condition=True,
    reason="Só roda em Linux"
)
def test_especifico_linux():
    pass

Executando os Testes

# Todos os testes
pytest

# Com verbosidade
pytest -v

# Arquivo específico
pytest tests/unit/test_calculadora.py

# Teste específico
pytest tests/unit/test_calculadora.py::TestCalcularMedia::test_media_simples

# Filtrando por marker
pytest -m unit
pytest -m "not slow"

# Parando no primeiro erro
pytest -x

# Mostrando prints
pytest -s

# Reruns automáticos em falhas
pip install pytest-rerunfailures
pytest --reruns 3 --reruns-delay 1

# Executando em paralelo
pip install pytest-xdist
pytest -n auto   # usa todos os núcleos disponíveis

# Relatório completo
pytest --tb=long -v --cov=src --cov-report=html

Resumo

  • pytest usa descoberta automática — arquivos test_*.py, classes Test*, funções test_*
  • Fixtures são injetadas por nome nos parâmetros — escopo pode ser function, class, module ou session
  • mocker.MagicMock e mocker.patch isolam dependências externas — banco, e-mail, APIs
  • pytest.mark.parametrize elimina código repetido em testes com múltiplas entradas
  • O padrão AAA (Arrange, Act, Assert) torna os testes legíveis e auditáveis
  • pytest-cov mede cobertura — configure fail_under no pyproject.toml para garantir mínimo
  • Testes devem ser independentes, rápidos, repetíveis e autoexplicativos pelo nome
  • pytest-asyncio testa código assíncrono com a mesma elegância dos testes síncronos

Referências e Leituras Complementares

  • pytest — documentação oficial — https://docs.pytest.org/en/stable/
  • pytest-cov — https://pytest-cov.readthedocs.io/en/latest/
  • pytest-mock — https://pytest-mock.readthedocs.io/en/latest/
  • pytest-asyncio — https://pytest-asyncio.readthedocs.io/en/latest/
  • Faker — geração de dados falsos — https://faker.readthedocs.io/en/master/
  • PERCIVAL, Harry. Test-Driven Development with Python. 2. ed. O'Reilly Media, 2017. — TDD de ponta a ponta com Python, Django e pytest.
  • OKKEN, Brian. Python Testing with pytest. 2. ed. Pragmatic Bookshelf, 2022. — referência definitiva e prática sobre pytest.

Prof. Ricardo Matos — Dominando o Python em 1 Ano · Artigo 43 de 52 Próximo: Artigo 44 — Qualidade de Código: linting, type hints e mypy


you asked

Sim


claude response

Dominando o Python em 1 Ano

Comentários

Mais em Python

Dominando o Python
Dominando o Python

Se voc&ecirc; j&aacute; pesquisou sobre programa&ccedil;&atilde;o nos &uacute...

WebSockets e Comunicação em Tempo Real em Python (Ver se é duplicidade)
WebSockets e Comunicação em Tempo Real em Python (Ver se é duplicidade)

HTTP é excelente para o modelo requisição-resposta — o cliente pergunta, o se...

Tratamento de Exceções e Erros
Tratamento de Exceções e Erros

Todo programa que interage com o mundo real encontra situa&ccedil;&otilde;es...