diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7a86854841e3..b80d551c112f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -42,6 +42,11 @@ INSTALL(TARGETS ubox ubox-static
 ADD_SUBDIRECTORY(lua)
 ADD_SUBDIRECTORY(examples)
 
+IF(UNIT_TESTING)
+  ENABLE_TESTING()
+  ADD_SUBDIRECTORY(tests)
+ENDIF()
+
 find_library(json NAMES json-c)
 IF(EXISTS ${json})
 	ADD_LIBRARY(blobmsg_json SHARED blobmsg_json.c)
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
new file mode 100644
index 000000000000..6832b3f52c30
--- /dev/null
+++ b/tests/CMakeLists.txt
@@ -0,0 +1,10 @@
+ADD_SUBDIRECTORY(cram)
+
+MACRO(ADD_UNIT_TEST name)
+  ADD_EXECUTABLE(test-${name} test-${name}.c)
+  TARGET_LINK_LIBRARIES(test-${name} ubox)
+  TARGET_INCLUDE_DIRECTORIES(test-${name} PRIVATE ${PROJECT_SOURCE_DIR})
+ENDMACRO(ADD_UNIT_TEST)
+
+ADD_UNIT_TEST(avl)
+ADD_UNIT_TEST(b64)
diff --git a/tests/cram/CMakeLists.txt b/tests/cram/CMakeLists.txt
new file mode 100644
index 000000000000..ca00d11dd686
--- /dev/null
+++ b/tests/cram/CMakeLists.txt
@@ -0,0 +1,27 @@
+FIND_PACKAGE(PythonInterp 3 REQUIRED)
+FILE(GLOB test_cases "test_*.t")
+
+SET(PYTHON_VENV_DIR "${CMAKE_CURRENT_BINARY_DIR}/.venv")
+SET(PYTHON_VENV_PIP "${PYTHON_VENV_DIR}/bin/pip")
+SET(PYTHON_VENV_CRAM "${PYTHON_VENV_DIR}/bin/cram")
+
+ADD_CUSTOM_COMMAND(
+	OUTPUT ${PYTHON_VENV_CRAM}
+	COMMAND ${PYTHON_EXECUTABLE} -m venv ${PYTHON_VENV_DIR}
+	COMMAND ${PYTHON_VENV_PIP} install cram
+)
+ADD_CUSTOM_TARGET(prepare-cram-venv ALL DEPENDS ${PYTHON_VENV_CRAM})
+
+ADD_TEST(
+	NAME cram
+	COMMAND ${PYTHON_VENV_CRAM} ${test_cases}
+	WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
+MACRO(ADD_CRAM_TEST_ENV env file)
+	SET_PROPERTY(TEST cram APPEND PROPERTY ENVIRONMENT "${env}=$<TARGET_FILE:${file}>")
+ENDMACRO(ADD_CRAM_TEST_ENV)
+
+ADD_CRAM_TEST_ENV(JSHN jshn)
+ADD_CRAM_TEST_ENV(TEST_AVL test-avl)
+ADD_CRAM_TEST_ENV(TEST_B64 test-b64)
diff --git a/tests/cram/test_avl.t b/tests/cram/test_avl.t
new file mode 100644
index 000000000000..661779b9b190
--- /dev/null
+++ b/tests/cram/test_avl.t
@@ -0,0 +1,10 @@
+check that avl is producing expected results:
+
+  $ valgrind --quiet --leak-check=full $TEST_AVL
+  test_basics: insert: 0=zero 0=one 0=two 0=three 0=four 0=five 0=six 0=seven 0=eight 0=nine 0=ten 0=eleven 0=twelve 
+  test_basics: insert duplicate: -1=zero -1=one -1=two -1=three -1=four -1=five -1=six -1=seven -1=eight -1=nine -1=ten -1=eleven -1=twelve 
+  test_basics: first=eight last=zero
+  test_basics: for each element: eight eleven five four nine one seven six ten three twelve two zero 
+  test_basics: delete 'one' element
+  test_basics: for each element reverse: zero two twelve three ten six seven nine four five eleven eight 
+  test_basics: delete all elements
diff --git a/tests/cram/test_base64.t b/tests/cram/test_base64.t
new file mode 100644
index 000000000000..7e41442746d8
--- /dev/null
+++ b/tests/cram/test_base64.t
@@ -0,0 +1,17 @@
+check that base64 is producing expected results:
+
+  $ valgrind --quiet --leak-check=full $TEST_B64
+  0 
+  4 Zg==
+  4 Zm8=
+  4 Zm9v
+  8 Zm9vYg==
+  8 Zm9vYmE=
+  8 Zm9vYmFy
+  0 
+  1 f
+  2 fo
+  3 foo
+  4 foob
+  5 fooba
+  6 foobar
diff --git a/tests/cram/test_jshn.t b/tests/cram/test_jshn.t
new file mode 100644
index 000000000000..d228f0e648c6
--- /dev/null
+++ b/tests/cram/test_jshn.t
@@ -0,0 +1,25 @@
+set jshn for convenience:
+
+  $ [ -n "$JSHN" ] && export PATH="$(dirname "$JSHN"):$PATH"
+  $ alias jshn="valgrind --quiet --leak-check=full jshn"
+
+check usage:
+
+  $ jshn
+  Usage: jshn [-n] [-i] -r <message>|-R <file>|-w
+  [2]
+
+test bad json:
+
+  $ jshn -r '[]'
+  Failed to parse message data
+  [1]
+
+test good json:
+
+  $ jshn -r '{"foo": "bar", "baz": {"next": "meep"}}'
+  json_init;
+  json_add_string 'foo' 'bar';
+  json_add_object 'baz';
+  json_add_string 'next' 'meep';
+  json_close_object;
diff --git a/tests/test-avl.c b/tests/test-avl.c
new file mode 100644
index 000000000000..18ee9b7697c5
--- /dev/null
+++ b/tests/test-avl.c
@@ -0,0 +1,87 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "avl.h"
+#include "avl-cmp.h"
+#include "utils.h"
+
+#define OUT(fmt, ...) do { \
+	fprintf(stdout, "%s: " fmt, __func__, ## __VA_ARGS__); \
+} while (0);
+
+struct node {
+	struct avl_node avl;
+};
+
+static void test_basics()
+{
+	size_t i;
+	struct avl_tree t;
+	struct node *temp;
+	struct node *elem;
+	struct node *last;
+	struct node *first;
+	const char *vals[] = {
+		"zero", "one", "two", "three", "four", "five", "six",
+		"seven", "eight", "nine", "ten", "eleven", "twelve"
+	};
+
+	avl_init(&t, avl_strcmp, false, NULL);
+
+	OUT("insert: ");
+	for (i=0; i<ARRAY_SIZE(vals); i++) {
+		struct node *n = malloc(sizeof(struct node));
+		n->avl.key = vals[i];
+
+		int r = avl_insert(&t, &n->avl);
+		fprintf(stdout, "%d=%s ", r, (char *)n->avl.key);
+	}
+	fprintf(stdout, "\n");
+
+	OUT("insert duplicate: ");
+	for (i=0; i<ARRAY_SIZE(vals); i++) {
+		struct node *n = malloc(sizeof(struct node));
+		n->avl.key = vals[i];
+
+		int r = avl_insert(&t, &n->avl);
+		fprintf(stdout, "%d=%s ", r, (char *)n->avl.key);
+
+		if (r)
+			free(n);
+	}
+	fprintf(stdout, "\n");
+
+	first = avl_first_element(&t, first, avl);
+	last = avl_last_element(&t, last, avl);
+	OUT("first=%s last=%s\n", (char*)first->avl.key, (char*)last->avl.key);
+
+	OUT("for each element: ");
+	avl_for_each_element(&t, elem, avl) {
+		fprintf(stdout, "%s ", (char*)elem->avl.key);
+	}
+	fprintf(stdout, "\n");
+
+	OUT("delete 'one' element\n");
+	elem = avl_find_element(&t, "one", elem, avl);
+	avl_delete(&t, &elem->avl);
+	free(elem);
+
+	OUT("for each element reverse: ");
+	avl_for_each_element_reverse(&t, elem, avl) {
+		fprintf(stdout, "%s ", (char*)elem->avl.key);
+	}
+	fprintf(stdout, "\n");
+
+	OUT("delete all elements\n");
+	avl_for_each_element_safe(&t, elem, avl, temp) {
+		avl_delete(&t, &elem->avl);
+		free(elem);
+	}
+}
+
+int main()
+{
+	test_basics();
+	return 0;
+}
diff --git a/tests/test-b64.c b/tests/test-b64.c
new file mode 100644
index 000000000000..c29b4e2c73a3
--- /dev/null
+++ b/tests/test-b64.c
@@ -0,0 +1,39 @@
+#include <stdio.h>
+#include <string.h>
+
+#include "utils.h"
+
+static void test_b64_encode(const char *src)
+{
+	char dst[255] = {0};
+	int r = b64_encode(src, strlen(src), dst, sizeof(dst));
+	fprintf(stdout, "%d %s\n", r, dst);
+}
+
+static void test_b64_decode(const char *src)
+{
+	char dst[255] = {0};
+	int r = b64_decode(src, dst, sizeof(dst));
+	fprintf(stdout, "%d %s\n", r, dst);
+}
+
+int main()
+{
+	test_b64_encode("");
+	test_b64_encode("f");
+	test_b64_encode("fo");
+	test_b64_encode("foo");
+	test_b64_encode("foob");
+	test_b64_encode("fooba");
+	test_b64_encode("foobar");
+
+	test_b64_decode("");
+	test_b64_decode("Zg==");
+	test_b64_decode("Zm8=");
+	test_b64_decode("Zm9v");
+	test_b64_decode("Zm9vYg==");
+	test_b64_decode("Zm9vYmE=");
+	test_b64_decode("Zm9vYmFy");
+
+	return 0;
+}
