Java'da bir MQTT entegrasyon testi, gerçek bir client'ı gerçek bir broker'a bağlar, abone olur, publish eder ve gelen mesajı doğrular. JUnit 5 ile zor olan kısım API değil, asenkron teslimattır: mesajlar başka bir thread'de, biraz sonra ya da hiç gelmez. Bu rehber, bunu Eclipse Paho, HiveMQ MQTT Client, Awaitility ve Testcontainers ile doğru şekilde ele alan bir test kurulumunu gösteriyor.
Bağımlılıklar
Örnekler Maven kullanıyor; aynı koordinatlar Gradle'da da çalışır. Sürümler yazım sırasında günceldi — daha yeni patch sürümleri de sorun olmaz.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>com.hivemq</groupId>
<artifactId>hivemq-mqtt-client</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.20.3</version>
<scope>test</scope>
</dependency>
</dependencies>Yalnızca uygulamanızın gerçekten kullandığı MQTT client'ına ihtiyacınız var. Bu kütüphanelerde yeniyseniz Paho ile Java'da MQTT veya HiveMQ MQTT Client rehberi ile başlayın.
Rastgele kırılmayan MQTT testleri için kurallar
| Kural | Neden |
|---|---|
| Her teste benzersiz client ID | Aynı ID'ye sahip iki client birbirini düşürür ("session taken over") |
| Her teste benzersiz topic öneki | Paralel testler ve eski retained mesajlar doğrulamalarınıza karışmaz |
| Publish etmeden önce abone olun ve SUBACK'i bekleyin | Abonelik aktif olmadan publish edilen mesaj basitçe teslim edilmez |
Timeout ile bekleyin, asla Thread.sleep ile değil | Çalıştığında hızlı, çalışmadığında net bir hata |
@AfterEach içinde bağlantıyı kapatın | Unutulan bağlantılar broker limitlerini tüketir ve oturumları canlı tutar |
| Broker adresi yapılandırmadan gelsin | Aynı testler lokalde Testcontainers'a, CI'da barındırılan bir broker'a karşı çalışır |
Client ID oluşturucu tüm broker'larda güvenli formatları gösterir; testlerde bir önek ve rastgele bir sonek yeterlidir.
Paho ve Awaitility ile bir test
Paho'nun senkron MqttClient'ı, broker onaylayana kadar connect() ve subscribe() çağrılarını bloklar; bu da test hazırlığını kolaylaştırır. Teslimat yine de Paho'nun callback thread'inde gerçekleşir; bu yüzden mesajları thread-safe bir kuyrukta toplayın ve Awaitility'nin onu sorgulamasına izin verin.
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.jupiter.api.*;
class PahoRoundTripTest {
static final String BROKER =
System.getenv().getOrDefault("MQTT_URL", "tcp://broker.connectmq.com:1883");
MqttClient client;
String topic;
@BeforeEach
void connect() throws MqttException {
String id = UUID.randomUUID().toString().substring(0, 8);
topic = "testmqtt/junit/" + id + "/temperature";
client = new MqttClient(BROKER, "junit-" + id, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setCleanSession(true);
options.setConnectionTimeout(10);
String user = System.getenv("MQTT_USER");
if (user != null) {
options.setUserName(user);
options.setPassword(System.getenv("MQTT_PASS").toCharArray());
}
client.connect(options);
}
@AfterEach
void disconnect() throws MqttException {
if (client.isConnected()) client.disconnect();
client.close();
}
@Test
void publishedMessageIsReceived() throws MqttException {
var received = new ConcurrentLinkedQueue<String>();
client.subscribe(topic, 1, (t, msg) -> received.add(new String(msg.getPayload(), UTF_8)));
client.publish(topic, "21.5".getBytes(UTF_8), 1, false);
await().atMost(Duration.ofSeconds(5))
.untilAsserted(() -> assertEquals("21.5", received.peek()));
}
}MemoryPersistence önemlidir: varsayılan dosya kalıcılığı, çalışma dizinine her client ID için bir klasör yazar; bu da build dizininizi kirletir ve paralel çalıştırmalarda çakışabilir.
Aynı test HiveMQ MQTT Client ile
HiveMQ client MQTT 5'i destekler. Blocking API'sinde testler için ideal bir publishes() kuyruğu vardır: abone olmadan önce kaydedin, ardından timeout ile receive() çağırın.
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.*;
import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
import com.hivemq.client.mqtt.datatypes.MqttQos;
import com.hivemq.client.mqtt.mqtt5.Mqtt5BlockingClient;
import com.hivemq.client.mqtt.mqtt5.Mqtt5Client;
import com.hivemq.client.mqtt.mqtt5.message.publish.Mqtt5Publish;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.*;
class HiveMqRoundTripTest {
Mqtt5BlockingClient client;
String topic;
@BeforeEach
void connect() {
String id = UUID.randomUUID().toString().substring(0, 8);
topic = "testmqtt/junit/" + id + "/status";
client = Mqtt5Client.builder()
.identifier("junit-" + id)
.serverHost("broker.connectmq.com")
.serverPort(1883)
.buildBlocking();
client.connect();
}
@AfterEach
void disconnect() {
client.disconnect();
}
@Test
void statusMessageIsDelivered() throws InterruptedException {
try (var publishes = client.publishes(MqttGlobalPublishFilter.ALL)) {
client.subscribeWith().topicFilter(topic).qos(MqttQos.AT_LEAST_ONCE).send();
client.publishWith().topic(topic).qos(MqttQos.AT_LEAST_ONCE)
.payload("online".getBytes(UTF_8)).send();
Optional<Mqtt5Publish> msg = publishes.receive(5, TimeUnit.SECONDS);
assertTrue(msg.isPresent(), "no message within 5 s on " + topic);
assertEquals("online", new String(msg.get().getPayloadAsBytes(), UTF_8));
}
}
}TLS için .sslWithDefaultConfig() ve 8883 portunu ekleyin; kimlik bilgileri için connect çağrısına .simpleAuth().username(...).password(...).applySimpleAuth() ekleyin.
Testcontainers ile kullan-at broker
Çevrimdışı çalışması gereken veya belirli bir broker yapılandırması isteyen testler için her test sınıfında bir broker container'ı başlatın. Mosquitto 2.x yapılandırma almadığında yalnızca localhost'u dinler; bu yüzden imajla gelen kimlik doğrulamasız yapılandırmayı kullanın:
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
@Testcontainers
class LocalBrokerTest {
@Container
static GenericContainer<?> mosquitto =
new GenericContainer<>(DockerImageName.parse("eclipse-mosquitto:2.0"))
.withExposedPorts(1883)
.withCommand("mosquitto", "-c", "/mosquitto-no-auth.conf")
.waitingFor(Wait.forLogMessage(".*mosquitto version.*running.*", 1));
static String brokerUrl() {
return "tcp://" + mosquitto.getHost() + ":" + mosquitto.getMappedPort(1883);
}
@Test
void brokerAcceptsConnections() throws Exception {
var client = new org.eclipse.paho.client.mqttv3.MqttClient(
brokerUrl(), "junit-local",
new org.eclipse.paho.client.mqttv3.persist.MemoryPersistence());
client.connect();
client.disconnect();
client.close();
}
}static bir container sınıf başına bir kez başlar; instance alanı ise her test metodu için bir tane başlatır ve bu yavaştır. HiveMQ CE'ye karşı test etmek isterseniz Testcontainers bir HiveMQ modülü de (org.testcontainers:hivemq) sunar. Artıları ve eksileri Testcontainers ile MQTT testleri yazısında anlatılıyor.
Lokal container mı, barındırılan broker mı?
Bir container, kodunuzun bir broker'a karşı çalıştığını kanıtlar. Kodunuzun sizin broker'ınıza — TLS, kimlik doğrulama, ACL'ler ve cihazlarınızın kullandığı ağ yolu olan broker'a — karşı çalıştığını kanıtlamaz. İyi bir ayrım:
- Unit ve bileşen testleri: Testcontainers, ağ bağımlılığı yok, hızlı geri bildirim.
- CI'da entegrasyon testleri: production ile aynı TLS ve kimlik bilgisi yapılandırmasına sahip,
MQTT_URL,MQTT_USERveMQTT_PASSile seçilen barındırılan bir test broker'ı. - Deploy sonrası smoke testler: production broker'ı, ayrılmış bir test topic'i, salt okunur doğrulamalar.
Negatif durumları test etmek
Entegrasyon testleri, olmaması gerekeni de kanıtlamalıdır. İki kalıp:
- Beklenen bağlantı hatası. Paho ile
assertThrows(MqttSecurityException.class, () -> client.connect(badOptions))yanlış kimlik bilgilerinin reddedildiğini doğrular;getReasonCode()MQTT 3.1.1'de "bad username or password" ve "not authorized" için 4 veya 5 döndürür. Diğer kodlar için reason code aracına bakın. - Beklenen sessizlik. Bir ACL'nin bir topic'i engellediğini doğrulamak için abone olun, sınırlı bir süre bekleyin ve kuyruğun boş kaldığını doğrulayın:
await().during(Duration.ofSeconds(2)).atMost(Duration.ofSeconds(3)).until(received::isEmpty).during, Awaitility'nin koşulun sürekli doğru kaldığını kontrol etmesini sağlar.
Suite'i çalıştırmak
Ağa bağımlı testleri @Tag("integration") ile etiketleyin ve Maven Failsafe (*IT.java) veya ayrı bir Gradle görevi ile çalıştırın; böylece mvn test hızlı ve çevrimdışı kalır. CI'da broker değişkenlerini export edip mvn verify çalıştırın; GitHub Actions'ta MQTT testleri eksiksiz bir workflow gösteriyor.
Bir test çalışırken online MQTT client'ı açın ve suite'inizin ürettiği trafiği görmek için testmqtt/junit/# topic'ine abone olun — başarısız bir testin hiç publish etmediğini mi yoksa yanlış topic'e mi publish ettiğini anlamanın en hızlı yolu budur.
JUnit test trafiğinizi canlı izleyin
JUnit 5 ile iyi MQTT testleri izolasyon ve sabra dayanır: her test için benzersiz bir client ID ve topic, publish etmeden önce onaylanmış bir abonelik ve sleep yerine sınırlı beklemeler. Broker adresini yapılandırılabilir tutun; aynı suite dizüstü bilgisayarınızdaki bir container'ı, bir CI broker'ını ve deploy sonrası bir smoke testi kapsasın. Daha geniş bir strateji için MQTT entegrasyon testleri yazısına bakın.
Sıkça sorulan sorular
MQTT testlerinde Thread.sleep kullanmalı mıyım?
Hayır. Sabit bir sleep yavaş bir CI runner'da kısa kalır, hızlı birinde ise zaman kaybettirir. Awaitility veya timeout'lu bir CompletableFuture kullanın: test, mesaj gelir gelmez devam eder ve gelmezse anlaşılır bir hata mesajıyla başarısız olur.
JUnit testleri için Paho mu, HiveMQ client mı?
Uygulamanızın kullandığı client ile test edin. Eclipse Paho (mqttv3) MQTT 3.1.1 için en yaygın olanıdır; HiveMQ MQTT Client MQTT 5'i destekler ve blocking, async ve reactive API'ler sunar. Bu rehberdeki kalıplar ikisiyle de çalışır.
Paralel MQTT testlerinin birbirini etkilemesini nasıl önlerim?
Her teste benzersiz bir client ID ve benzersiz bir topic öneki verin, örneğin testmqtt/junit/<rastgele-id>/. Böylece testler aynı broker'a karşı paralel çalışabilir; birbirlerinin mesajlarını almaz ve yinelenen client ID'ler yüzünden birbirlerini düşürmezler.


