Testes de brokers

Testando MQTT em CI com GitHub Actions

Rode testes MQTT no GitHub Actions: suba um container Mosquitto que realmente aceite conexões e teste contra um broker hospedado usando secrets do repositório.

Atualizado · 9 min de leitura

Testando MQTT em CI com GitHub Actions

Para testar MQTT no GitHub Actions, suba um broker dentro do job, espere até ele aceitar conexões e rode sua suíte de testes contra ele — depois adicione um segundo job que roda os mesmos testes contra um broker hospedado, com credenciais armazenadas como secrets do repositório. O primeiro job pega bugs de lógica a cada commit; o segundo pega problemas de TLS, autenticação e ACL antes dos seus dispositivos.

Este guia traz workflows prontos para copiar e colar para os dois casos e explica o padrão do Mosquitto que quebra a maioria das primeiras tentativas.

A armadilha do "local only mode"

O primeiro instinto é usar um service container:

Parece certo, mas não funciona
services:
  mosquitto:
    image: eclipse-mosquitto:2
    ports:
      - 1883:1883

O container sobe, a porta é mapeada e todos os testes falham com Connection refused ou ECONNRESET. O log do broker explica o motivo:

docker logs
mosquitto version 2.0.x starting
Config loaded from /mosquitto/config/mosquitto.conf.
Starting in local only mode. Connections will only be possible from clients running on this machine.
Create a configuration file which defines a listener to allow remote access.

Desde a versão 2.0, o Mosquitto sem nenhum listener na configuração escuta apenas na interface loopback dentro do próprio container. O tráfego que chega pelo mapeamento de portas vem de fora, então é recusado. O arquivo de configuração padrão da imagem tem todas as opções comentadas, então é exatamente isso que você recebe.

Service containers tornam a correção trabalhosa: eles iniciam antes de qualquer step rodar, então seu repositório (e o mosquitto.conf dele) ainda não foi baixado. A abordagem mais simples e previsível é iniciar o broker você mesmo em um step. O mesmo problema em ambientes locais é tratado em Mosquitto no Docker e configuração do Mosquitto não carrega.

Job 1: Mosquitto dentro do workflow

Faça commit de uma configuração mínima de testes no repositório:

test/mosquitto.conf
listener 1883 0.0.0.0
allow_anonymous true
persistence false
log_dest stdout

Você pode gerar variantes mais rígidas — com arquivo de senhas ou ACLs para testar negações — com o gerador de configuração do Mosquitto. Depois inicie o broker após o checkout e espere até ele responder de verdade em MQTT, e não só em TCP:

.github/workflows/mqtt-tests.yml
name: mqtt-tests

on:
  push:
  pull_request:

jobs:
  local-broker:
    runs-on: ubuntu-latest
    env:
      MQTT_HOST: localhost
      MQTT_PORT: "1883"
    steps:
      - uses: actions/checkout@v4

      - name: Start Mosquitto
        run: |
          docker run -d --name mosquitto -p 1883:1883 \
            -v "$PWD/test/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro" \
            eclipse-mosquitto:2
          for i in $(seq 1 30); do
            if docker exec mosquitto mosquitto_pub -h localhost -t testmqtt/ci/ready -m ok; then
              echo "broker ready"; exit 0
            fi
            sleep 1
          done
          echo "broker did not start"; docker logs mosquitto; exit 1

      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - run: pip install -r requirements-dev.txt

      - run: pytest -v tests/

      - name: Broker logs
        if: failure()
        run: docker logs mosquitto

Por que o loop roda mosquitto_pub em vez de checar a porta: o proxy de portas do Docker aceita conexões TCP no host assim que o container existe, mesmo antes de o broker estar escutando lá dentro. Uma publicação bem-sucedida prova que o broker está no ar. A imagem eclipse-mosquitto já traz os clientes de linha de comando, então não é preciso instalar nada a mais.

Em um projeto Node.js, troque os steps de Python por actions/setup-node@v4, npm ci e npm test; os steps do broker continuam iguais.

Job 2: testes contra um broker hospedado

Um container no CI é anônimo, sem criptografia e configurado do zero a cada execução — o oposto da produção. Um segundo job roda a mesma suíte sobre TLS com credenciais reais. Guarde-as em Settings → Secrets and variables → Actions como MQTT_USER e MQTT_PASS:

.github/workflows/mqtt-tests.yml (continuação)
  hosted-broker:
    runs-on: ubuntu-latest
    needs: local-broker
    # secrets are not available to pull requests from forks
    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
    concurrency:
      group: mqtt-hosted-${{ github.ref }}
      cancel-in-progress: true
    env:
      MQTT_HOST: broker.connectmq.com
      MQTT_PORT: "8883"
      MQTT_USER: ${{ secrets.MQTT_USER }}
      MQTT_PASS: ${{ secrets.MQTT_PASS }}
      MQTT_TOPIC_PREFIX: testmqtt/ci/${{ github.run_id }}-${{ github.run_attempt }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements-dev.txt
      - run: pytest -v tests/

O que cada parte faz:

  • needs: local-broker roda o job mais lento, que usa a rede, apenas quando o job rápido passou.
  • A condição if: pula o job em pull requests de forks, que não recebem secrets. Sem ela, o job falha com um erro de autenticação que parece um bug real.
  • MQTT_TOPIC_PREFIX inclui o ID da execução e o número da tentativa, então re-execuções e workflows paralelos nunca compartilham topics. Seus testes precisam montar todo topic a partir dele — veja testes de integração MQTT para o padrão de fixture.
  • O GitHub mascara automaticamente os valores dos secrets nos logs, mas mesmo assim evite imprimir as opções de conexão na saída de debug.

Mantendo o job hospedado rápido e organizado

Alguns hábitos mantêm o job com rede confiável à medida que a suíte cresce:

  • Rode um subconjunto. Marque os testes que precisam de TLS, credenciais ou ACLs (por exemplo @pytest.mark.hosted ou um projeto Jest separado) e rode só esses no job hospedado. A lógica pura já está coberta pelo job local.
  • Limpe as retained messages. Toda retained message criada por um teste sobrevive à execução. Limpe-a no teardown com uma publicação retained vazia, ou evite a flag retain fora dos testes que verificam esse comportamento.
  • Use sessões limpas. Sessões persistentes com client IDs aleatórios se acumulam no broker até expirarem. Use sessões limpas (ou uma expiração de sessão curta no MQTT 5), a menos que o teste seja sobre sessões.
  • Ajuste os timeouts para a rede. Uma mensagem que chega em 20 ms localmente pode precisar de um segundo a partir de um runner compartilhado pela internet. Use timeouts de mensagem de 5 a 10 segundos neste job; os testes continuam terminando assim que a mensagem chega.

Testando contra vários brokers com uma matriz

Se a sua biblioteca ou gateway precisa funcionar com mais de um broker, uma matriz roda a suíte contra cada um sem duplicar YAML:

Trecho da matriz
  compat:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        image: ["eclipse-mosquitto:2", "emqx/emqx:latest", "hivemq/hivemq-ce:latest"]
    steps:
      - uses: actions/checkout@v4
      - name: Start ${{ matrix.image }}
        run: docker run -d --name broker -p 1883:1883 ${{ matrix.image }} ${{ startsWith(matrix.image, 'eclipse-mosquitto') && 'mosquitto -c /mosquitto-no-auth.conf' || '' }}
      - name: Wait for MQTT
        run: |
          pip install paho-mqtt
          python scripts/wait_for_broker.py localhost 1883 --timeout 60
      - run: pip install -r requirements-dev.txt && pytest -v tests/

EMQX e HiveMQ CE demoram mais para iniciar que o Mosquitto e suas imagens não incluem mosquitto_pub, então a verificação de prontidão roda a partir do runner. Fixe tags exatas de imagem em pipelines reais para que uma nova versão do broker não quebre seu build sem aviso. Um pequeno wait_for_broker.py só precisa repetir client.connect() até dar certo ou o timeout expirar.

Depurando execuções de CI com falha

Erro no log do jobCausaCorreção
Connection refused / ECONNREFUSED logo após iniciarBroker ainda não pronto, ou local only modeUse o loop de prontidão MQTT; declare um listener
ECONNRESET em toda conexãoPorta mapeada, mas Mosquitto vinculado ao loopbacklistener 1883 0.0.0.0 na configuração
Connection Refused: not authorised no job hospedadoSecret ausente (PR de fork) ou usuário erradoVerifique a condição if: e os nomes dos secrets
[SSL: CERTIFICATE_VERIFY_FAILED]Porta TLS usada sem CA, ou hostname erradoUse o bundle de CA do sistema e o hostname real do broker
Testes passam sozinhos e falham na matrizTopics compartilhados entre jobsPrefixo de topic com ID da execução e valor da matriz

Vale a pena manter para sempre o step if: failure() que imprime docker logs: o log do broker mostra conexões rejeitadas, negações de ACL e client IDs. Para reason codes em erros do cliente, use a consulta de reason codes MQTT.

Verificando o pipeline

Faça o pipeline provar que consegue falhar. Envie uma branch que publica no topic errado e confirme que o job fica vermelho. Depois, enquanto o job hospedado roda, assine testmqtt/ci/# no cliente MQTT online com as mesmas credenciais: você verá o prefixo específico da execução aparecer e o tráfego de testes fluir, o que confirma que secrets, TLS e ACLs funcionam como esperado.

Veja o tráfego dos testes de CI ao vivo

Um pipeline MQTT confiável precisa de duas coisas: um broker no job configurado para aceitar conexões e verificado com uma publicação real, e uma segunda etapa que exercite o TLS, as credenciais e as ACLs que você usa em produção. Com prefixos de topic únicos e secrets fora dos builds de forks, ambos rodam a cada push sem atrapalhar um ao outro. Se preferir gerenciar os containers a partir do próprio código de teste, veja testes MQTT com Testcontainers.

Perguntas frequentes

Por que meu job do GitHub Actions não consegue conectar ao container do Mosquitto?

O Mosquitto 2.x inicia em local only mode quando nenhum listener está configurado, então só aceita conexões de dentro do próprio container. Inicie-o com uma configuração que declare listener 1883 e allow_anonymous true (ou credenciais reais), por exemplo o arquivo incluído /mosquitto-no-auth.conf.

Os secrets do GitHub ficam disponíveis em pull requests de forks?

Não. Workflows disparados por pull requests de repositórios fork não recebem os secrets do repositório. Rode os testes no broker hospedado apenas em pushes e em pull requests do mesmo repositório, e deixe o job com broker local para todo o resto.

O CI deve usar um Mosquitto local ou um broker hospedado?

Use os dois. Um container local dá feedback rápido e isolado a cada commit. Um job contra um broker hospedado verifica TLS, autenticação e ACLs como estão configurados em produção, o que um container descartável não consegue.