Skip to content

E2E Tests

This guide helps you to execute and author E2E tests on your local environment.


+----------+                            |
| Dev      |                            A
| Machine  |               +------------+
|          |               | IoT Edge   |
+----------+       )       | LBS/PktFwd |
  |              ) )       | NtwSrv     |
  | (usb)  |   ) ) )       +------------+
  |        A     ) )
 +---------+       )
 | Arduino |
  • LoRaWan solution up and running (IoT Edge Device, IoT Hub, LoRa Keys Azure Function, Redis, container registry etc.)
  • Seeeduino LoRaWan device (leaf test device) connected via USB to a computer where the LoRaWan.Tests.E2E will run.
  • Module LoRaWanNetworkSrvModule logging configured with following environment variables:
    • LOG_LEVEL: 1
    • LOG_TO_TCP: true
    • LOG_TO_TCP_ADDRESS: development machine IP address (ensure IoT Edge machine can ping it)
  • E2E test configuration (in file appsettings.local.json) has TCP logging enabled "TcpLog": "true"


LoRa Basics Station and Network Server setup

Configure LoRa Basics Station and Network Server to run on your concentrator:

  1. Copy LoRaEngine/example.env file and name it .env.

    * Update Container Registry settings and facade function app section with details of your created registry and function app. * Update LoRaWanNetworkSrvModule settings:


    * Update LBS_TC_URI:

  2. Build and push IoT Edge solution to your container registry using LoRaEngine/deployment.lbs.template.json file.

    In VS Code: Ctrl+Shift+P -> Azure IoT Edge: Bild and Push IoT Edge Solution -> select template file.

  3. Create deployment for a single device using generated deployment template.

    In VS Code, right-click on LoRaEngine/config/deployment.lbs.json and select Create Deployment for Single Device.

  4. Provision a device in your IoT Hub by following the Basics Station configuration docs.

End device setup

Connect and setup Seeduino Arduino with the serial pass sketch

void setup()

void loop()

E2E test configuration

Create/edit E2E settings in file appsettings.local.json

The value of LeafDeviceSerialPort in Windows will be the COM port where the Arduino board is connected to (Arduino IDE displays it). On macos you can discover through ls /dev/tty* and/or ls /dev/cu* bash commands. On Linux you can discover them with ls /dev/ttyACM*.

  "testConfiguration": {
    "IoTHubEventHubConnectionString": "Endpoint=sb://;SharedAccessKeyName=iothubowner;SharedAccessKey=xxx;EntityPath=xxxxx",
    "IoTHubEventHubConsumerGroup": "your-iothub-consumer-group",
    "IoTHubConnectionString": ";SharedAccessKeyName=iothubowner;SharedAccessKey=xxx",
    "EnsureHasEventDelayBetweenReadsInSeconds": "15",
    "EnsureHasEventMaximumTries": "5",
    "LeafDeviceSerialPort": "your-usb-port",
    "LeafDeviceGatewayID": "your-iot-edge-device-id",
    "CreateDevices": true,
    "NetworkServerModuleLogAssertLevel": "Error",
    "DevicePrefix": "your-two-letter-device-prefix",
    "TcpLog": "true",
    "FunctionAppBaseUrl": "",
    "FunctionAppCode": "your-function-code=",
    "TxPower": "a-value-for-setting-arduino-tx-power",
    // following options are used and to be configured for MultiConcentrator and CUPS Test
    // where the Concentrator startup/shutdown is programmatically invoked in the test itself
    "RunningInCI": "should-be-false-if-not-running-with-e2e-ci",
    "RemoteConcentratorConnection": "username@remote-ssh-host",
    "DefaultBasicStationEui": "a-fixed-basic-station-eui-to-be-used",
    "BasicStationExecutablePath": "path-to-station-prebuilt-binary-on-remote-ssh-host",
    "SshPrivateKeyPath": "path-on-local-host-to-ssh-private-key-used-for-remote-ssh-connection",
    "SharedLnsEndpoint": "wss://IP_or_DNS:5001",
    "SharedCupsEndpoint": "https://IP_OR_DNS:5002",
    "RadioDev": "the-device-path-for-concentrator (i.e. /dev/ttyUSB0)",
    "IsCorecellBasicStation": false // or true if a SX1302 based concentrator is used

If the value of CreateDevices setting is true, running the tests will create/update devices in IoT Hub prior to executing tests. Devices will be created starting with ID "0000000000000001". The deviceID prefix can be modified by setting a value in DevicePrefix setting ('FF' → 'FF00000000000001').

Creating a new test

Each test uses an unique device to ensure transmissions don't exceed LoRaWan regulations. Additionally, it makes it easier to track logs for each test.

To create a new device modify the IntegrationTestFixture class by:

  1. Creating a new property of type TestDeviceInfo as (increment the device ids by 1):

    // Device13_OTAA: used for wrong AppEUI OTAA join
    public TestDeviceInfo Device13_OTAA { get; private set; }
  2. Create the TestDeviceInfo instance in IntegrationTestFixture.SetupDevices() method

    // Device13_OTAA: used for Join with wrong AppEUI
    Device13_OTAA = new TestDeviceInfo()
        DeviceID = "0000000000000013",
        AppEUI = "BE7A00000000FEE3",
        AppKey = "8AFE71A145B253E49C3031AD068277A3",
        SensorDecoder = "DecoderValueSensor",
        // GatewayID property of the device
        GatewayID = gatewayID,
        // Indicates if the device exists in IoT Hub
        // Some tests don't require a device to actually exist
        IsIoTHubDevice = true,
  3. Create the test method / fact in a test class. If a new test class is needed (to group logically test) read the section 'Creating Test Class'). Code should be similar to this:

// Tests using a invalid Network Session key, resulting in mic failed
// Uses Device8_ABP
public async Task Test_ABP_Invalid_NwkSKey_Fails_With_Mic_Error()
    var device = TestFixture.Device8_ABP;

    Assert.NotEqual(nwkSKeyToUse, device.NwkSKey);
    await ArduinoDevice.setDeviceModeAsync(LoRaArduinoSerial._device_mode_t.LWABP);
    await ArduinoDevice.setIdAsync(device.DevAddr, device.DeviceID, null);
    await ArduinoDevice.setKeyAsync(nwkSKeyToUse, device.AppSKey, null);

    await ArduinoDevice.SetupLora(TestFixture.Configuration.LoraRegion);

    await ArduinoDevice.transferPacketAsync("100", 10);

    // Don't pollute radio transmission channel
    await Task.Delay(Constants.DELAY_FOR_SERIAL_AFTER_SENDING_PACKET);

    // Add here test logic

Creating Test Class

E2E tests cannot be parallelized because they all share dependency to Arduino device. Those classes also rely on TCP and IoT Event Hub listeners that should be created once per test execution, not per test.

Therefore, when creating a new test class follow the guidelines:

  • Use attribute [Collection(Constants.TestCollectionName)] to ensure executing in serial
  • inherit from IntegrationTestBase. Create a single constructor receiving a IntegrationTestFixture as parameter. Call the base class constructor passing the parameter.


 // Tests xxxx
[Collection(Constants.TestCollectionName)] // Set the same collection to ensure execution in serial
public sealed class MyTest : IntegrationTestBase
    // Constructor receives the IntegrationTestFixture that is a singleton
    public MyTest(IntegrationTestFixture testFixture) : base(testFixture)


Assertions and expectations are implemented in 3 levels.

Arduino Serial logs

Serial logs from Arduino allow test cases to ensure the leaf device is receiving the correct response from the antena (LoRaPktFwd module).

Checks can be done the following way:

// After transferPacketWithConfirmed: Expectation from serial "+CMSG: ACK Received"
// It has retries to account for i/o delays
await AssertUtils.ContainsWithRetriesAsync("+CMSG: ACK Received", ArduinoDevice.SerialLogs);

LoRaWan Network Server Module logs

The network server module logs important execution steps. Those messages can be used to ensure expected actions happened in the network server module. This validation creates a tight dependency between tests and logging.

We might need to reavaluate it if the friction between code changes and breaking tests gets too high. An option is to have the Network server publish events when an operation happens and have the test create assertion on them (i.e. { "type": "otaajoin", "status": "succeeded", "deviceid": "xxx", "time": "a-date" }).

Module logs can be listened from IoT Hub or TCP (experimental).

Validating against the module logs.

// Ensures that the message 0000000000000004: message '{"value": 51}' sent to hub is logged
// It contains retries to account for i/o delays
await TestFixture.AssertNetworkServerModuleLogStartsWithAsync($"{device.DeviceID}: message '{{\"value\":{msg}}}' sent to hub");

IoT Hub Device Message

For end to end validation we listen for device messages arriving in IoT Hub. Examples:

// Ensure device payload is available. It contains retries to account for i/o delays
// Data: {"value": 51}
var expectedPayload = $"{{\"value\":{msg}}}";
await TestFixture.AssertIoTHubDeviceMessageExistsAsync(device.DeviceID, expectedPayload);

Last update: 2022-02-24
Created: 2021-11-16