diff --git a/tests/unix/extra_coverage.py b/tests/unix/extra_coverage.py
index f2b40282b3bc776a5c263bb2018cfc1cfe41bc96..ef1b50fda066d3e0e17a021962db76ea1051d785 100644
--- a/tests/unix/extra_coverage.py
+++ b/tests/unix/extra_coverage.py
@@ -5,15 +5,46 @@ except NameError:
     import sys
     sys.exit()
 
+import uerrno
+
 data = extra_coverage()
 
 # test hashing of str/bytes that have an invalid hash
-print(data)
+print(data[0], data[1])
 print(hash(data[0]))
 print(hash(data[1]))
 print(hash(bytes(data[0], 'utf8')))
 print(hash(str(data[1], 'utf8')))
 
+# test streams
+stream = data[2] # has set_error and set_buf. Write always returns error
+stream.set_error(uerrno.EAGAIN) # non-blocking error
+print(stream.read()) # read all encounters non-blocking error
+print(stream.read(1)) # read 1 byte encounters non-blocking error
+print(stream.readline()) # readline encounters non-blocking error
+print(stream.readinto(bytearray(10))) # readinto encounters non-blocking error
+print(stream.write(b'1')) # write encounters non-blocking error
+print(stream.write1(b'1')) # write1 encounters non-blocking error
+stream.set_buf(b'123')
+print(stream.read(4)) # read encounters non-blocking error after successful reads
+stream.set_buf(b'123')
+print(stream.read1(4)) # read1 encounters non-blocking error after successful reads
+stream.set_buf(b'123')
+print(stream.readline(4)) # readline encounters non-blocking error after successful reads
+try:
+    print(stream.ioctl(0, 0)) # ioctl encounters non-blocking error; raises OSError
+except OSError:
+    print('OSError')
+stream.set_error(0)
+print(stream.ioctl(0, bytearray(10))) # successful ioctl call
+
+stream2 = data[3] # is textio and sets .write = NULL
+try:
+    print(stream2.write(b'1')) # attempt to call NULL implementation
+except OSError:
+    print('OSError')
+print(stream2.read(1)) # read 1 byte encounters non-blocking error with textio stream
+
 # test basic import of frozen scripts
 import frzstr1
 import frzmpy1
diff --git a/tests/unix/extra_coverage.py.exp b/tests/unix/extra_coverage.py.exp
index d3d7252115f5fdb56b3674547f91c9deb6588d4a..8f62ccf51f558002ffcebe54b434e4d373c7a7c5 100644
--- a/tests/unix/extra_coverage.py.exp
+++ b/tests/unix/extra_coverage.py.exp
@@ -43,11 +43,24 @@ Warning: test
 ?
 +1e+00
 +1e+00
-('0123456789', b'0123456789')
+0123456789 b'0123456789'
 7300
 7300
 7300
 7300
+None
+None
+None
+None
+None
+None
+b'123'
+b'123'
+b'123'
+OSError
+0
+OSError
+None
 frzstr1
 frzmpy1
 frzstr_pkg1.__init__
diff --git a/unix/coverage.c b/unix/coverage.c
index 7140be41b9d7eab7d3f6b7a25c84dfb85221c780..8b46fd0ff34873bc959677bef589a4d9ff63bff4 100644
--- a/unix/coverage.c
+++ b/unix/coverage.c
@@ -1,4 +1,5 @@
 #include <stdio.h>
+#include <string.h>
 
 #include "py/obj.h"
 #include "py/objstr.h"
@@ -8,6 +9,127 @@
 #include "py/builtin.h"
 #include "py/emit.h"
 #include "py/formatfloat.h"
+#include "py/stream.h"
+
+// stream testing object
+typedef struct _mp_obj_streamtest_t {
+    mp_obj_base_t base;
+    uint8_t *buf;
+    size_t len;
+    size_t pos;
+    int error_code;
+} mp_obj_streamtest_t;
+
+STATIC mp_obj_t stest_set_buf(mp_obj_t o_in, mp_obj_t buf_in) {
+    mp_obj_streamtest_t *o = MP_OBJ_TO_PTR(o_in);
+    mp_buffer_info_t bufinfo;
+    mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ);
+    o->buf = m_new(uint8_t, bufinfo.len);
+    memcpy(o->buf, bufinfo.buf, bufinfo.len);
+    o->len = bufinfo.len;
+    o->pos = 0;
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(stest_set_buf_obj, stest_set_buf);
+
+STATIC mp_obj_t stest_set_error(mp_obj_t o_in, mp_obj_t err_in) {
+    mp_obj_streamtest_t *o = MP_OBJ_TO_PTR(o_in);
+    o->error_code = mp_obj_get_int(err_in);
+    return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_2(stest_set_error_obj, stest_set_error);
+
+STATIC mp_uint_t stest_read(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
+    mp_obj_streamtest_t *o = MP_OBJ_TO_PTR(o_in);
+    if (o->pos < o->len) {
+        if (size > o->len - o->pos) {
+            size = o->len - o->pos;
+        }
+        memcpy(buf, o->buf + o->pos, size);
+        o->pos += size;
+        return size;
+    } else if (o->error_code == 0) {
+        return 0;
+    } else {
+        *errcode = o->error_code;
+        return MP_STREAM_ERROR;
+    }
+}
+
+STATIC mp_uint_t stest_write(mp_obj_t o_in, const void *buf, mp_uint_t size, int *errcode) {
+    mp_obj_streamtest_t *o = MP_OBJ_TO_PTR(o_in);
+    (void)buf;
+    (void)size;
+    *errcode = o->error_code;
+    return MP_STREAM_ERROR;
+}
+
+STATIC mp_uint_t stest_ioctl(mp_obj_t o_in, mp_uint_t request, uintptr_t arg, int *errcode) {
+    mp_obj_streamtest_t *o = MP_OBJ_TO_PTR(o_in);
+    (void)arg;
+    (void)request;
+    (void)errcode;
+    if (o->error_code != 0) {
+        *errcode = o->error_code;
+        return MP_STREAM_ERROR;
+    }
+    return 0;
+}
+
+STATIC const mp_rom_map_elem_t rawfile_locals_dict_table[] = {
+    { MP_ROM_QSTR(MP_QSTR_set_buf), MP_ROM_PTR(&stest_set_buf_obj) },
+    { MP_ROM_QSTR(MP_QSTR_set_error), MP_ROM_PTR(&stest_set_error_obj) },
+    { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) },
+    { MP_ROM_QSTR(MP_QSTR_read1), MP_ROM_PTR(&mp_stream_read1_obj) },
+    { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
+    { MP_ROM_QSTR(MP_QSTR_write1), MP_ROM_PTR(&mp_stream_write1_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
+    { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) },
+    { MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&mp_stream_ioctl_obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(rawfile_locals_dict, rawfile_locals_dict_table);
+
+STATIC const mp_stream_p_t fileio_stream_p = {
+    .read = stest_read,
+    .write = stest_write,
+    .ioctl = stest_ioctl,
+};
+
+STATIC const mp_obj_type_t mp_type_stest_fileio = {
+    { &mp_type_type },
+    .protocol = &fileio_stream_p,
+    .locals_dict = (mp_obj_dict_t*)&rawfile_locals_dict,
+};
+
+// stream read returns non-blocking error
+STATIC mp_uint_t stest_read2(mp_obj_t o_in, void *buf, mp_uint_t size, int *errcode) {
+    (void)o_in;
+    (void)buf;
+    (void)size;
+    *errcode = MP_EAGAIN;
+    return MP_STREAM_ERROR;
+}
+
+STATIC const mp_rom_map_elem_t rawfile_locals_dict_table2[] = {
+    { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) },
+    { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
+};
+
+STATIC MP_DEFINE_CONST_DICT(rawfile_locals_dict2, rawfile_locals_dict_table2);
+
+STATIC const mp_stream_p_t textio_stream_p2 = {
+    .read = stest_read2,
+    .write = NULL,
+    .is_text = true,
+};
+
+STATIC const mp_obj_type_t mp_type_stest_textio2 = {
+    { &mp_type_type },
+    .protocol = &textio_stream_p2,
+    .locals_dict = (mp_obj_dict_t*)&rawfile_locals_dict2,
+};
+
 
 #if defined(MICROPY_UNIX_COVERAGE)
 
@@ -157,8 +279,17 @@ STATIC mp_obj_t extra_coverage(void) {
         mp_printf(&mp_plat_print, "%s\n", buf2);
     }
 
+    mp_obj_streamtest_t *s = m_new_obj(mp_obj_streamtest_t);
+    s->base.type = &mp_type_stest_fileio;
+    s->buf = NULL;
+    s->len = 0;
+    s->pos = 0;
+    s->error_code = 0;
+    mp_obj_streamtest_t *s2 = m_new_obj(mp_obj_streamtest_t);
+    s2->base.type = &mp_type_stest_textio2;
+
     // return a tuple of data for testing on the Python side
-    mp_obj_t items[] = {(mp_obj_t)&str_no_hash_obj, (mp_obj_t)&bytes_no_hash_obj};
+    mp_obj_t items[] = {(mp_obj_t)&str_no_hash_obj, (mp_obj_t)&bytes_no_hash_obj, MP_OBJ_FROM_PTR(s), MP_OBJ_FROM_PTR(s2)};
     return mp_obj_new_tuple(MP_ARRAY_SIZE(items), items);
 }
 MP_DEFINE_CONST_FUN_OBJ_0(extra_coverage_obj, extra_coverage);