diff --git a/t/helper/test-example-tap.c b/t/helper/test-example-tap.c
index d072ad559f..7b02177a9f 100644
--- a/t/helper/test-example-tap.c
+++ b/t/helper/test-example-tap.c
@@ -92,5 +92,38 @@ int cmd__example_tap(int argc, const char **argv)
 	test_res = TEST(t_empty(), "test with no checks");
 	TEST(check_int(test_res, ==, 0), "test with no checks returns 0");

+	if (TEST_RUN("TEST_RUN passing test"))
+		check_int(1, ==, 1);
+	if (TEST_RUN("TEST_RUN failing test"))
+		check_int(1, ==, 2);
+	if (TEST_RUN("TEST_RUN passing TEST_TODO()"))
+		TEST_TODO(check(0));
+	if (TEST_RUN("TEST_RUN failing TEST_TODO()"))
+		TEST_TODO(check(1));
+	if (TEST_RUN("TEST_RUN test_skip()")) {
+		check(0);
+		test_skip("missing prerequisite");
+		check(1);
+	}
+	if (TEST_RUN("TEST_RUN test_skip() inside TEST_TODO()"))
+		TEST_TODO((test_skip("missing prerequisite"), 1));
+	if (TEST_RUN("TEST_RUN TEST_TODO() after failing check")) {
+		check(0);
+		TEST_TODO(check(0));
+	}
+	if (TEST_RUN("TEST_RUN failing check after TEST_TODO()")) {
+		check(1);
+		TEST_TODO(check(0));
+		check(0);
+	}
+	if (TEST_RUN("TEST_RUN messages from failing string and char comparison")) {
+		check_str("\thello\\", "there\"\n");
+		check_str("NULL", NULL);
+		check_char('a', ==, '\n');
+		check_char('\\', ==, '\'');
+	}
+	if (TEST_RUN("TEST_RUN test with no checks"))
+		; /* nothing */
+
 	return test_done();
 }
diff --git a/t/t0080/expect b/t/t0080/expect
index 0cfa0dc6d8..92526e1b06 100644
--- a/t/t0080/expect
+++ b/t/t0080/expect
@@ -40,4 +40,37 @@ not ok 17 - messages from failing string and char comparison
 # BUG: test has no checks at t/helper/test-example-tap.c:92
 not ok 18 - test with no checks
 ok 19 - test with no checks returns 0
-1..19
+ok 20 - TEST_RUN passing test
+# check "1 == 2" failed at t/helper/test-example-tap.c:98
+#    left: 1
+#   right: 2
+not ok 21 - TEST_RUN failing test
+not ok 22 - TEST_RUN passing TEST_TODO() # TODO
+# todo check 'check(1)' succeeded at t/helper/test-example-tap.c:102
+not ok 23 - TEST_RUN failing TEST_TODO()
+# check "0" failed at t/helper/test-example-tap.c:104
+# skipping test - missing prerequisite
+# skipping check '1' at t/helper/test-example-tap.c:106
+ok 24 - TEST_RUN test_skip() # SKIP
+# skipping test - missing prerequisite
+ok 25 - TEST_RUN test_skip() inside TEST_TODO() # SKIP
+# check "0" failed at t/helper/test-example-tap.c:111
+not ok 26 - TEST_RUN TEST_TODO() after failing check
+# check "0" failed at t/helper/test-example-tap.c:117
+not ok 27 - TEST_RUN failing check after TEST_TODO()
+# check "!strcmp("\thello\\", "there\"\n")" failed at t/helper/test-example-tap.c:120
+#    left: "\011hello\\"
+#   right: "there\"\012"
+# check "!strcmp("NULL", NULL)" failed at t/helper/test-example-tap.c:121
+#    left: "NULL"
+#   right: NULL
+# check "'a' == '\n'" failed at t/helper/test-example-tap.c:122
+#    left: 'a'
+#   right: '\012'
+# check "'\\' == '\''" failed at t/helper/test-example-tap.c:123
+#    left: '\\'
+#   right: '\''
+not ok 28 - TEST_RUN messages from failing string and char comparison
+# BUG: test has no checks at t/helper/test-example-tap.c:125
+not ok 29 - TEST_RUN test with no checks
+1..29
diff --git a/t/unit-tests/test-lib.c b/t/unit-tests/test-lib.c
index 3c513ce59a..fc50755fae 100644
--- a/t/unit-tests/test-lib.c
+++ b/t/unit-tests/test-lib.c
@@ -16,6 +16,8 @@ static struct {
 	unsigned running :1;
 	unsigned skip_all :1;
 	unsigned todo :1;
+	char *desc;
+	char *location;
 } ctx = {
 	.lazy_plan = 1,
 	.result = RESULT_NONE,
@@ -123,9 +125,45 @@ void test_plan(int count)
 	ctx.lazy_plan = 0;
 }

-int test_done(void)
+static void test__run_maybe_end(void)
 {
+	if (ctx.running) {
+		assert(ctx.location);
+		assert(ctx.desc);
+		test__run_end(0, ctx.location, "%s", ctx.desc);
+		FREE_AND_NULL(ctx.location);
+		FREE_AND_NULL(ctx.desc);
+	}
 	assert(!ctx.running);
+	assert(!ctx.location);
+	assert(!ctx.desc);
+}
+
+int test__run(const char *location, const char *format, ...)
+{
+	va_list ap;
+	char *desc;
+
+	test__run_maybe_end();
+
+	va_start(ap, format);
+	desc = xstrvfmt(format, ap);
+	va_end(ap);
+
+	if (test__run_begin()) {
+		test__run_end(1, location, "%s", desc);
+		free(desc);
+		return 0;
+	} else {
+		ctx.location = xstrdup(location);
+		ctx.desc = desc;
+		return 1;
+	}
+}
+
+int test_done(void)
+{
+	test__run_maybe_end();

 	if (ctx.lazy_plan)
 		test_plan(ctx.count);
@@ -169,7 +207,7 @@ void test_skip_all(const char *format, ...)

 int test__run_begin(void)
 {
-	assert(!ctx.running);
+	test__run_maybe_end();

 	ctx.count++;
 	ctx.result = RESULT_NONE;
diff --git a/t/unit-tests/test-lib.h b/t/unit-tests/test-lib.h
index 2de6d715d5..6df40e3b12 100644
--- a/t/unit-tests/test-lib.h
+++ b/t/unit-tests/test-lib.h
@@ -21,6 +21,13 @@
  */
 void test_plan(int count);

+/*
+ * Start a test, returns 1 if the test was actually started or 0 if it
+ * was skipped.  The test ends when the next test starts or test_done()
+ * is called.
+ */
+#define TEST_RUN(...) test__run(TEST_LOCATION(), __VA_ARGS__)
+
 /*
  * test_done() must be called at the end of main(). It will print the
  * plan if plan() was not called at the beginning of the test program
@@ -156,6 +163,7 @@ extern union test__tmp test__tmp[2];
 int test__run_begin(void);
 __attribute__((format (printf, 3, 4)))
 int test__run_end(int, const char *, const char *, ...);
+int test__run(const char *location, const char *format, ...);
 void test__todo_begin(void);
 int test__todo_end(const char *, const char *, int);

