diff --git a/tests/Makefile b/tests/Makefile
index c8ecf8b..67595f1 100644
--- a/tests/Makefile
+++ b/tests/Makefile
@@ -1,5 +1,7 @@
 PROGRAM=tests
 
+EXTRA_COMPONENTS=extras/dhcpserver
+
 PROGRAM_SRC_DIR = . ./cases
 
 # Add unity test framework headers & core source file
diff --git a/tests/cases/03_byte_load_flash.c b/tests/cases/03_byte_load_flash.c
index 4554e4c..fee464e 100644
--- a/tests/cases/03_byte_load_flash.c
+++ b/tests/cases/03_byte_load_flash.c
@@ -1,8 +1,9 @@
-/* Unit tests to verify the "unaligned load handler" in core/exception_vectors.S that allows us to
-   complete byte loads from unaligned memory, etc.
-
-   Adapted from a test program in 'experiments' that did this.
-*/
+/**
+ * Unit tests to verify the "unaligned load handler" in core/exception_vectors.S 
+ * that allows us to complete byte loads from unaligned memory, etc.
+ *
+ * Adapted from a test program in 'experiments' that did this.
+ */
 #include "testcase.h"
 #include "esp/rom.h"
 #include "esp/timer.h"
diff --git a/tests/cases/04_wifi_basic.c b/tests/cases/04_wifi_basic.c
new file mode 100644
index 0000000..382869d
--- /dev/null
+++ b/tests/cases/04_wifi_basic.c
@@ -0,0 +1,181 @@
+/**
+ * This test verifies basic WiFi communication.
+ *
+ * Device A creates a WiFi access point and listens on port 23 for incomming
+ * connection. When incomming connection occurs it sends a string and waits
+ * for the response.
+ *
+ * Device B connects to a WiFi access point and opens TCP connection to
+ * device A on port 23. Then it receives a string and sends it back.
+ */
+#include <string.h>
+
+#include <FreeRTOS.h>
+#include <task.h>
+
+#include <espressif/esp_common.h>
+#include "sdk_internal.h"
+
+#include <lwip/api.h>
+#include <lwip/err.h>
+#include <lwip/sockets.h>
+#include <lwip/sys.h>
+#include <lwip/netdb.h>
+#include <lwip/dns.h>
+#include <dhcpserver.h>
+
+#include "testcase.h"
+
+#define AP_SSID         "esp-open-rtos-ap"
+#define AP_PSK          "esp-open-rtos"
+#define SERVER          "172.16.0.1"
+#define PORT            23
+#define BUF_SIZE        128
+
+DEFINE_TESTCASE(04_wifi_basic, DUAL)
+
+/*********************************************************
+ *   WiFi AP part
+ *********************************************************/
+
+static void server_task(void *pvParameters)
+{
+    char buf[BUF_SIZE];
+    struct netconn *nc = netconn_new(NETCONN_TCP);
+    TEST_ASSERT_TRUE_MESSAGE(nc != 0, "Failed to allocate socket");
+
+    netconn_bind(nc, IP_ADDR_ANY, PORT);
+    netconn_listen(nc);
+
+    struct netconn *client = NULL;
+    err_t err = netconn_accept(nc, &client);
+    TEST_ASSERT_TRUE_MESSAGE(err == ERR_OK, "Error accepting connection");
+
+    ip_addr_t client_addr;
+    uint16_t port_ignore;
+    netconn_peer(client, &client_addr, &port_ignore);
+
+    snprintf(buf, sizeof(buf), "test ping\r\n");
+    printf("Device A: send data: %s\n", buf);
+    netconn_write(client, buf, strlen(buf), NETCONN_COPY);
+
+    struct pbuf *pb;
+    for (int i = 0; i < 10; i++) {
+        TEST_ASSERT_EQUAL_INT_MESSAGE(ERR_OK, netconn_recv_tcp_pbuf(client, &pb),
+                "Failed to receive data");
+        if (pb->len > 0) {
+            memcpy(buf, pb->payload, pb->len);
+            buf[pb->len] = 0;
+            break;
+        }
+        vTaskDelay(100 / portTICK_RATE_MS);
+    }
+    TEST_ASSERT_TRUE_MESSAGE(pb->len > 0, "No data received");
+    printf("Device A: received data: %s\n", buf);
+    TEST_ASSERT_FALSE_MESSAGE(strcmp((const char*)buf, "test pong\r\n"),
+            "Received wrong data");
+
+    netconn_delete(client);
+
+    TEST_PASS();
+}
+
+static void a_04_wifi_basic(void)
+{
+    printf("Device A started\n");
+    sdk_wifi_set_opmode(SOFTAP_MODE);
+
+    struct ip_info ap_ip;
+    IP4_ADDR(&ap_ip.ip, 172, 16, 0, 1);
+    IP4_ADDR(&ap_ip.gw, 0, 0, 0, 0);
+    IP4_ADDR(&ap_ip.netmask, 255, 255, 0, 0);
+    sdk_wifi_set_ip_info(1, &ap_ip);
+
+    struct sdk_softap_config ap_config = {
+        .ssid = AP_SSID,
+        .ssid_hidden = 0,
+        .channel = 3,
+        .ssid_len = strlen(AP_SSID),
+        .authmode = AUTH_WPA_WPA2_PSK,
+        .password = AP_PSK,
+        .max_connection = 3,
+        .beacon_interval = 100,
+    };
+    sdk_wifi_softap_set_config(&ap_config);
+
+    ip_addr_t first_client_ip;
+    IP4_ADDR(&first_client_ip, 172, 16, 0, 2);
+    dhcpserver_start(&first_client_ip, 4);
+
+    xTaskCreate(server_task, (signed char *)"setver_task", 1024, NULL, 2, NULL);
+}
+
+
+/*********************************************************
+ *   WiFi client part
+ *********************************************************/
+
+static void connect_task(void *pvParameters)
+{
+    struct sockaddr_in serv_addr;
+    char buf[BUF_SIZE];
+
+    // wait for wifi connection
+    while (sdk_wifi_station_get_connect_status() != STATION_GOT_IP) {
+        vTaskDelay(1000 / portTICK_RATE_MS);
+        printf("Waiting for connection to AP\n");
+    }
+
+    int s = socket(AF_INET, SOCK_STREAM, 0);
+    TEST_ASSERT_TRUE_MESSAGE(s >= 0, "Failed to allocate a socket");
+
+    bzero(&serv_addr, sizeof(serv_addr));
+    serv_addr.sin_port = htons(PORT);
+    serv_addr.sin_family = AF_INET;
+
+    TEST_ASSERT_TRUE_MESSAGE(inet_aton(SERVER, &serv_addr.sin_addr.s_addr),
+            "Failed to set IP address");
+
+    TEST_ASSERT_TRUE_MESSAGE(
+            connect(s, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == 0,
+            "Socket connection failed");
+
+    bzero(buf, BUF_SIZE);
+
+    int r = 0;
+    for (int i = 0; i < 10; i++) {
+        r = read(s, buf, BUF_SIZE);
+        if (r > 0) {
+            break;
+        }
+        vTaskDelay(100 / portTICK_RATE_MS);
+    }
+    TEST_ASSERT_TRUE_MESSAGE(r > 0, "No data received");
+
+    printf("Device B: received data: %s\n", buf);
+    TEST_ASSERT_FALSE_MESSAGE(strcmp((const char*)buf, "test ping\r\n"),
+            "Received wrong data");
+
+    snprintf(buf, sizeof(buf), "test pong\r\n");
+    printf("Device B: send data: %s\n", buf);
+    TEST_ASSERT_EQUAL_INT_MESSAGE(strlen(buf), write(s, buf, strlen(buf)),
+            "Error socket writing");
+
+    close(s);
+
+    TEST_PASS();
+}
+
+static void b_04_wifi_basic(void)
+{
+    printf("Device B started\n");
+    struct sdk_station_config config = {
+        .ssid = AP_SSID,
+        .password = AP_PSK,
+    };
+
+    sdk_wifi_set_opmode(STATION_MODE);
+    sdk_wifi_station_set_config(&config);
+
+    xTaskCreate(&connect_task, (signed char *)"connect_task", 1024, NULL, 2, NULL);
+}
diff --git a/tests/include/testcase.h b/tests/include/testcase.h
index 63e91de..11f59a4 100644
--- a/tests/include/testcase.h
+++ b/tests/include/testcase.h
@@ -41,7 +41,7 @@ void testcase_register(const testcase_t *testcase);
 #define DEFINE_TESTCASE(NAME, TYPE)                                     \
     static testcase_fn_t a_##NAME;                                      \
     static testcase_fn_t b_##NAME;                                      \
-    _DEFINE_TESTCASE_COMMON(NAME, TYPE, A_##NAME, B_##NAME)
+    _DEFINE_TESTCASE_COMMON(NAME, TYPE, a_##NAME, b_##NAME)
 
 
 #define _DEFINE_TESTCASE_COMMON(NAME, TYPE, A_FN, B_FN)                 \
diff --git a/tests/test_main.c b/tests/test_main.c
index a93d6e1..aada715 100644
--- a/tests/test_main.c
+++ b/tests/test_main.c
@@ -2,6 +2,9 @@
 #include <stdlib.h>
 #include <esp/uart.h>
 #include <string.h>
+#include <FreeRTOS.h>
+#include <task.h>
+#include <espressif/esp_common.h>
 
 /* Convert requirement enum to a string we can print */
 static const char *get_requirements_name(const testcase_type_t arg) {
@@ -30,7 +33,8 @@ void testcase_register(const testcase_t *testcase)
         testcases_alloc += 1;
         testcases = realloc(testcases, testcases_alloc * sizeof(testcase_t));
         if(!testcases) {
-            printf("Failed to reallocate test case register length %d\n", testcases_alloc);
+            printf("Failed to reallocate test case register length %d\n", 
+                    testcases_alloc);
             testcases_count = 0;
             testcases_alloc = 0;
         }
@@ -38,13 +42,14 @@ void testcase_register(const testcase_t *testcase)
     memcpy(&testcases[testcases_count++], testcase, sizeof(testcase_t));
 }
 
-void user_init(void)
+static void test_task(void *pvParameters)
 {
     uart_set_baud(0, 115200);
     printf("esp-open-rtos test runner.\n");
     printf("%d test cases are defined:\n\n", testcases_count);
     for(int i = 0; i < testcases_count; i++) {
-        printf("CASE %d = %s %s\n", i, testcases[i].name, get_requirements_name(testcases[i].type));
+        printf("CASE %d = %s %s\n", i, testcases[i].name, 
+                get_requirements_name(testcases[i].type));
     }
 
     printf("Enter A or B then number of test case to run, ie A0.\n");
@@ -100,9 +105,11 @@ void user_init(void)
     testcases_alloc = 0;
     testcases_count = 0;
 
-    printf("\nRunning test case %d (%s %s) as instance %c \nDefinition at %s:%d\n***\n", case_idx,
+    printf("\nRunning test case %d (%s %s) as instance %c "
+                "\nDefinition at %s:%d\n***\n", case_idx,
            testcase.name, get_requirements_name(testcase.type), type,
            testcase.file, testcase.line);
+
     Unity.CurrentTestName = testcase.name;
     Unity.TestFile = testcase.file;
     Unity.CurrentTestLineNumber = testcase.line;
@@ -111,5 +118,13 @@ void user_init(void)
         testcase.a_fn();
     else
         testcase.b_fn();
-    TEST_FAIL_MESSAGE("\n\nTest initialisation routine returned without calling TEST_PASS. Buggy test?");
+    /* TEST_FAIL_MESSAGE("\n\nTest initialisation routine returned" */
+    /*         " without calling TEST_PASS. Buggy test?"); */
+}
+
+void user_init(void)
+{
+    sdk_wifi_set_opmode(NULL_MODE);
+    test_task(0);
+    /* xTaskCreate(test_task, (signed char *)"test_task", 512, NULL, 2, NULL); */
 }
diff --git a/tests/test_runner.py b/tests/test_runner.py
index 4acd017..f31d4f3 100755
--- a/tests/test_runner.py
+++ b/tests/test_runner.py
@@ -7,11 +7,12 @@ import serial
 import threading
 import re
 import time
-import traceback
 
-SHORT_OUTPUT_TIMEOUT=0.25 # timeout for resetting and/or waiting for more lines of output
-TESTCASE_TIMEOUT=10
-TESTRUNNER_BANNER="esp-open-rtos test runner."
+
+SHORT_OUTPUT_TIMEOUT = 0.25  # timeout for resetting and/or waiting for more lines of output
+TESTCASE_TIMEOUT = 30
+TESTRUNNER_BANNER = "esp-open-rtos test runner."
+
 
 def main():
     global verbose
@@ -24,6 +25,7 @@ def main():
             flash_image(args.bport)
 
     env = TestEnvironment(args.aport, TestEnvironment.A)
+    env_b = None
     cases = env.get_testlist()
     if args.type != 'solo':
         env_b = TestEnvironment(args.bport, TestEnvironment.B)
@@ -31,10 +33,16 @@ def main():
         if cases != cases_b:
             raise TestRunnerError("Test cases on units A & B don't match")
 
-    counts = dict((status,0) for status in TestResult.STATUS_NAMES.keys())
+    counts = dict((status, 0) for status in TestResult.STATUS_NAMES.keys())
     failures = False
     for test in cases:
-        res = test.run(env)
+        if test.case_type == 'dual':
+            if env_b is None:
+                res = TestResult(TestResult.SKIPPED, 'Dual test case skipped')
+            else:
+                res = test.run(env, env_b)
+        else:
+            res = test.run(env)
         counts[res.status] += 1
         failures = failures or res.is_failure()
 
@@ -46,6 +54,7 @@ def main():
 
     sys.exit(1 if failures else 0)
 
+
 class TestCase(object):
     def __init__(self, index, name, case_type):
         self.name = name
@@ -56,11 +65,11 @@ class TestCase(object):
         return "#%d: %s (%s)" % (self.index, self.name, self.case_type)
 
     def __eq__(self, other):
-        return (self.index == other.index
-                and self.name == other.name
-                and self.case_type == other.case_type)
+        return (self.index == other.index and
+                self.name == other.name and
+                self.case_type == other.case_type)
 
-    def run(self, env_a, env_b = None):
+    def run(self, env_a, env_b=None):
         """
         Run the test represented by this instance, against the environment(s) passed in.
 
@@ -71,7 +80,7 @@ class TestCase(object):
         mon_b = env_b.start_testcase(self) if env_b else None
         while True:
             if mon_a.get_result() and (mon_b is None or mon_b.get_result()):
-                break # all running test environments have finished
+                break  # all running test environments have finished
 
             # or, in the case both are running, stop as soon as either environemnt shows a failure
             try:
@@ -93,15 +102,16 @@ class TestCase(object):
             res = max(mon_a.get_result(), mon_b.get_result())
         else:
             res = mon_a.get_result()
-        if not verbose: # finish the line after the ...
+        if not verbose:  # finish the line after the ...
             print(TestResult.STATUS_NAMES[res.status])
             if res.is_failure():
                 message = res.message
-                if "/" in res.message: # cut anything before the file name in the failure
+                if "/" in res.message:  # cut anything before the file name in the failure
                     message = message[message.index("/"):]
                 print("FAILURE MESSAGE:\n%s\n" % message)
         return res
 
+
 class TestResult(object):
     """ Class to wrap a test result code and a message """
     # Test status flags, higher = more severe
@@ -112,11 +122,11 @@ class TestResult(object):
     ERROR = 4
 
     STATUS_NAMES = {
-        CANCELLED : "Cancelled",
-        SKIPPED : "Skipped",
-        PASSED : "Passed",
-        FAILED : "Failed",
-        ERROR : "Error"
+        CANCELLED: "Cancelled",
+        SKIPPED: "Skipped",
+        PASSED: "Passed",
+        FAILED: "Failed",
+        ERROR: "Error"
         }
 
     def __init__(self, status, message):
@@ -126,10 +136,18 @@ class TestResult(object):
     def is_failure(self):
         return self.status >= TestResult.FAILED
 
-    def __cmp__(self, other):
+    def __qe__(self, other):
         if other is None:
-            return 1
-        return self.status - other.status
+            return False
+        else:
+            return self.status == other.status
+
+    def __lt__(self, other):
+        if other is None:
+            return False
+        else:
+            return self.status < other.status
+
 
 class TestMonitor(object):
     """ Class to monitor a running test case in a separate thread, defer reporting of the result until it's done.
@@ -163,7 +181,7 @@ class TestMonitor(object):
             while not self._cancelled and time.time() < start_time + TESTCASE_TIMEOUT:
                 line = self._port.readline().decode("utf-8", "ignore")
                 if line == "":
-                    continue # timed out
+                    continue  # timed out
                 self.output += "%s+%4.2fs %s" % (self._instance, time.time()-start_time, line)
                 verbose_print(line.strip())
                 if line.endswith(":PASS\r\n"):
@@ -183,6 +201,7 @@ class TestMonitor(object):
         finally:
             self._port.timeout = None
 
+
 class TestEnvironment(object):
     A = "A"
     B = "B"
@@ -211,7 +230,7 @@ class TestEnvironment(object):
 
         def collect_testcases(line):
                 if line.startswith(">"):
-                    return True # prompt means list of test cases is done, success
+                    return True  # prompt means list of test cases is done, success
                 m = re.match(r"CASE (\d+) = (.+?) ([A-Z]+)", line)
                 if m is not None:
                     t = TestCase(int(m.group(1)), m.group(2), m.group(3).lower())
@@ -223,7 +242,9 @@ class TestEnvironment(object):
         return tests
 
     def start_testcase(self, case):
-        """ Starts the specified test instance and returns an TestMonitor reader thread instance to monitor the output """
+        """ Starts the specified test instance and returns a TestMonitor reader thread instance
+        to monitor the output
+        """
         # synchronously start the test case
         self.reset()
         if not self._port.wait_line(lambda line: line.startswith(">")):
@@ -243,16 +264,18 @@ def get_testdir():
 
 
 def flash_image(serial_port):
-    # Bit hacky: rather than calling esptool directly, just use the Makefile flash target
-    # with the correct ESPPORT argument
+    # Bit hacky: rather than calling esptool directly,
+    # just use the Makefile flash target with the correct ESPPORT argument
     env = dict(os.environ)
     env["ESPPORT"] = serial_port
     verbose_print("Building and flashing test image to %s..." % serial_port)
     try:
         stdout = sys.stdout if verbose else None
-        output = subprocess.run(["make","flash"], check=True, cwd=get_testdir(), stdout=stdout, stderr=subprocess.STDOUT, env=env)
+        subprocess.run(["make", "flash"], check=True, cwd=get_testdir(),
+                       stdout=stdout, stderr=subprocess.STDOUT, env=env)
     except subprocess.CalledProcessError as e:
-        raise TestRunnerError("'make flash EPPORT=%s' failed with exit code %d" % (serial_port, e.returncode))
+        raise TestRunnerError("'make flash EPPORT=%s' failed with exit code %d" %
+                              (serial_port, e.returncode))
     verbose_print("Flashing successful.")
 
 
@@ -262,7 +285,7 @@ def parse_args():
     parser.add_argument(
         '--type', '-t',
         help='Type of test hardware attached to serial ports A & (optionally) B',
-        choices=['solo','dual','eyore_test'], default='solo')
+        choices=['solo', 'dual', 'eyore_test'], default='solo')
 
     parser.add_argument(
         '--aport', '-a',
@@ -296,11 +319,12 @@ class TestRunnerError(RuntimeError):
     def __init__(self, message):
         RuntimeError.__init__(self, message)
 
+
 class TestSerialPort(serial.Serial):
     def __init__(self, *args, **kwargs):
         super(TestSerialPort, self).__init__(*args, **kwargs)
 
-    def wait_line(self, callback, timeout = SHORT_OUTPUT_TIMEOUT):
+    def wait_line(self, callback, timeout=SHORT_OUTPUT_TIMEOUT):
         """ Wait for the port to output a particular piece of line content, as judged by callback
 
             Callback called as 'callback(line)' and returns not-True if non-match otherwise can return any value.
@@ -316,7 +340,7 @@ class TestSerialPort(serial.Serial):
             while not res:
                 line = self.readline()
                 if line == b"":
-                    break # timed out
+                    break  # timed out
                 line = line.decode("utf-8", "ignore").rstrip()
                 res = callback(line)
             return res
@@ -336,4 +360,3 @@ if __name__ == '__main__':
     except TestRunnerError as e:
         print(e)
         sys.exit(2)
-