Mocking MQTT with Playwright: Simplify Your System Tests

Leverage WebSocket interception to eliminate the need for a MQTT broker in your Playwright test environment.

Published on January 18, 2025
Updated on January 19, 2025

Testing real-time web applications often involves complex setups, for example, when your app relies on a protocol like MQTT for publish/subscribe messaging. Managing a MQTT broker “just for tests” adds unnecessary overhead to your development and continuous integration environments. With Playwright, it’s possible to intercept WebSocket traffic when your test suite runs. This allows us to mock the MQTT interactions over WebSockets, thus giving us a way to run system tests without the need of spinning up an actual MQTT broker.

In this post, I explain how to mock MQTT traffic during Playwright system tests for a tiny demo application built for this purpose. The demo application uses Vue and MQTT.js.

Project source code available at...
visini/mqtt-playwright-example

Setting Up a Local Mosquitto Broker

To build the demo application where we will later demonstrate how to mock MQTT-over-WebSockets, we need a real MQTT broker. Mosquitto is easy to set up via Docker. Here’s a minimal Docker Compose configuration to get started. Add the following to your docker-compose.yml file:

services:
  mosquitto:
    image: eclipse-mosquitto:latest
    container_name: mosquitto
    ports:
      - "1883:1883"
      - "8080:8080"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf

Next, create a mosquitto.conf file to enable MQTT-over-WebSockets (served on port 8080):

listener 8080
protocol websockets
allow_anonymous true

Start it up with:

docker compose up

This will serve as a real MQTT broker for our demo application. The demo application will connect to this MQTT broker to send and receive messages. Eventually, we will mock these MQTT communication in our Playwright system tests, but before that, we have to build a tiny app that we can build tests for.

A Minimal Vue App with MQTT

Here’s a very simple Vue component which subscribes to an MQTT topic via MQTT.js. It’s a message broadcaster, meaning whatever you type in the input field will be broadcasted to all subscribers (including itself). The component subscribes to a topic, in this case demo-broadcaster/messages, via MQTT-over-WebSockets, and displays all messages received in a list. Good enough for our purposes – that is, demonstrating how to mock MQTT in system tests run via Playwright!

App.vue
<script setup lang="ts">
import { ref } from "vue"
import mqtt from "mqtt"
 
const brokerUrl = "ws://localhost:8080"
const topic = "demo-broadcaster/messages"
const messages = ref<string[]>([])
const inputMessage = ref("")
 
const client = mqtt.connect(brokerUrl)
 
client.on("connect", () => {
  client.subscribe(topic, (err) => {
    if (err) console.error("Subscription failed:", err)
  })
})
 
client.on("message", (_topic, message) => {
  messages.value.push(message.toString())
})
 
const sendMessage = () => {
  if (!inputMessage.value.trim()) return
  client.publish(topic, inputMessage.value)
  inputMessage.value = ""
}
</script>
 
<template>
  <h1>MQTT Message Broadcaster</h1>
  <input v-model="inputMessage" placeholder="Type your message" />
  <button @click="sendMessage">Send</button>
  <ul>
    <li v-for="(message, index) in messages" :key="index">
      {{ message }}
    </li>
  </ul>
</template>

This component (together with the real MQTT broker Mosquitto we started via Docker) simulates a realistic application for which we would want to run system tests with mocked MQTT communication, thereby eliminating the need for a running broker during tests.

Understanding how MQTT-over-WebSockets works

To reliably mock MQTT interactions in Playwright, we need to replicate the exact packet flow the client (in our case, MQTT.js) expects during an MQTT session. This goes beyond simply opening a WebSocket connection; you must provide valid MQTT responses for every major step – CONNECT, SUBSCRIBE, and PUBLISH – just as a real broker would. Here’s the basic flow:

  • CONNECT and CONNACK → When an MQTT client first establishes a connection, it sends a CONNECT packet containing details like the client ID and protocol version. The broker must respond with a CONNACK packet to confirm the connection. Failing to send a CONNACK (or sending an invalid one) means the client library will assume the broker is offline or misconfigured, and the connection will fail.
  • SUBSCRIBE and SUBACK → After connecting, the client typically subscribes to one or more topics. That subscription is initiated with a SUBSCRIBE packet indicating which topics the client wants to receive messages for. A valid broker replies with a SUBACK packet to confirm that the subscription is acknowledged. Without this step, the client library won’t treat the subscription as active, and no messages will be delivered. We need to replicate this behavior in our mock broker.
  • PUBLISH → To emulate messages being published to topics, we should handle all PUBLISH packets that the client sends. The simplest approach is to echo these messages right back via a PUBLISH packet to simulate new inbound messages. If your test scenario involves different MQTT QoS levels or retained messages, you can extend this logic to return additional properties that replicate real-world behavior more closely.

The MQTT protocol has a very specific byte-level format for each packet type. Instead of crafting all these packets by hand, libraries like mqtt-packet parse incoming frames into JavaScript objects and generate MQTT-compliant buffers for responses. This means you don’t have to manually figure out each packet’s binary layout – mqtt-packet does the heavy lifting, ensuring you’re always sending and receiving well-formed MQTT data.

By recreating these MQTT flows within a WebSocket route, you’re effectively standing up a minimal broker implementation in your test environment. The result is a more reliable, self-contained system test suite that doesn’t rely on an external MQTT server – yet still offers the same publish/subscribe behavior your application code expects.

Mocking MQTT with Playwright

To bring all of this together in Playwright, we rely on page.routeWebSocket to intercept the WebSocket connection the MQTT client establishes. See more details in Playwright’s docs about WebSocket routes. Once intercepted, we feed any incoming data through mqtt-packet’s parser, which turns raw bytes into JavaScript objects. From there, we can match the MQTT command (connect, subscribe, publish, etc.) and generate the corresponding response packet with mqtt-packet.generate. This ensures the client sees exactly what it would expect from a real broker – CONNACK to confirm a connection, SUBACK to acknowledge subscriptions, and PUBLISH messages when the client sends or should receive them.

In the following code example, the mockMQTT function sets up this interception and MQTT packet handling. We then perform a basic scenario in our demo application by filling in an input field, sending the message, and asserting that our messages make it into the DOM. Under the hood, there’s no actual broker running – Playwright and mqtt-packet orchestrate the entire flow, everything is mocked. This approach keeps our tests completely self-contained, while still delivering a realistic MQTT-over-WebSockets experience for the client code.

mqtt.test.ts
import { test, expect } from "@playwright/test"
import mqttPacket from "mqtt-packet"
 
const brokerUrl = "ws://localhost:8080"
 
async function mockMQTT(page) {
  await page.routeWebSocket(brokerUrl, (ws) => {
    // Create MQTT parser and generator
    const parser = mqttPacket.parser({ protocolVersion: 5 })
    const generate = mqttPacket.generate
 
    parser.on("packet", (packet) => {
      if (packet.cmd === "connect") {
        // Confirm connection
        ws.send(
          generate({
            cmd: "connack",
            returnCode: 0,
            sessionPresent: false,
          }),
        )
      } else if (packet.cmd === "subscribe") {
        // Confirm subscription
        ws.send(
          generate({
            cmd: "suback",
            messageId: packet.messageId,
            granted: [0],
          }),
        )
      } else if (packet.cmd === "publish") {
        // Echo the message back
        ws.send(
          generate({
            cmd: "publish",
            topic: packet.topic,
            payload: packet.payload,
            qos: 0,
          }),
        )
      }
    })
 
    // Parse incoming messages
    ws.onMessage((raw) => {
      parser.parse(raw)
    })
  })
}
 
test("MQTT messaging works with a mocked broker", async ({ page }) => {
  await mockMQTT(page) // Intercept all MQTT traffic
  await page.goto("/")
 
  // Send first message
  await page.fill("input", "Hello from mocked broker")
  await page.click("button")
 
  // Send second message
  await page.fill("input", "Another test message")
  await page.click("button")
 
  // Check that two messages are listed
  const messages = page.locator("li")
  await expect(messages).toHaveCount(2)
  await expect(messages.nth(0)).toHaveText("Hello from mocked broker")
  await expect(messages.nth(1)).toHaveText("Another test message")
})

Beyond simple echo functionality, you can easily extend this code to reflect more sophisticated MQTT features. QoS levels, retained messages, wildcard subscriptions – these could all be added by extending the packet parsing and generation logic shown above, in order to match real-world broker expectations. The result is that your test setup remains both lightweight and realistic.

Conclusion

Mocking MQTT at the WebSocket level in Playwright allows you to maintain a fully self-contained test environment. By intercepting and parsing MQTT packets, we can replicate a minimal, yet realistic broker flow.

Project source code available at...
visini/mqtt-playwright-example

I hope this post is helpful for those looking to simplify their Playwright system tests that involve MQTT. If you have any questions or feedback, feel free to reach out. Happy testing!

© 2025 Camillo Visini
Imprint RSS