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:
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883O container sobe, a porta é mapeada e todos os testes falham com Connection refused ou ECONNRESET. O log do broker explica o motivo:
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:
listener 1883 0.0.0.0
allow_anonymous true
persistence false
log_dest stdoutVocê 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:
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 mosquittoPor 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:
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-brokerroda 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_PREFIXinclui 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.hostedou 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:
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 job | Causa | Correção |
|---|---|---|
Connection refused / ECONNREFUSED logo após iniciar | Broker ainda não pronto, ou local only mode | Use o loop de prontidão MQTT; declare um listener |
ECONNRESET em toda conexão | Porta mapeada, mas Mosquitto vinculado ao loopback | listener 1883 0.0.0.0 na configuração |
Connection Refused: not authorised no job hospedado | Secret ausente (PR de fork) ou usuário errado | Verifique a condição if: e os nomes dos secrets |
[SSL: CERTIFICATE_VERIFY_FAILED] | Porta TLS usada sem CA, ou hostname errado | Use o bundle de CA do sistema e o hostname real do broker |
| Testes passam sozinhos e falham na matriz | Topics compartilhados entre jobs | Prefixo 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.



