Fork of a lightweight testing framework originally authored by a friend of mine, who may or may not want his identity revealed. It was originally hosted at https://notabug.org/VictorSCushman/EMU

@Kimberlee I. Model Kimberlee I. Model authored on 17 May 2020
dev Fork of EMUTest 3 years ago
examples Fork of EMUTest 3 years ago
.gitattributes Fork of EMUTest 3 years ago
.gitignore Fork of EMUTest 3 years ago
EMUtest.h Fork of EMUTest 3 years ago
LICENSE.txt Fork of EMUTest 3 years ago
README.md Fork of EMUTest 3 years ago
README.md

EMU - Extended MinUnit

Credit

EMU Test was developed by an old friend of mine, who probably nolonger wants his name dropped by me. Although the page is no longer available, EMU was originally hosted here.

Table of contents

  1. Introduction
  2. Download
  3. Writing Your First Test
  4. API
  5. Advanced Testing
  6. License

Introduction

Extended MinUnit (EMU) is a macro-based header-only unit testing framework designed to give users a simple yet powerful set of tools for testing C and C++ code. Originally designed as an extension to John Brewer's MinUnit, EMU has evolved into a full-fledged unit testing framework that still keeps the minimalist spirit of MinUnit at heart.

EMU provides a set of testing tools similar to what you would find in frameworks like CUnit, Boost Test, or Google Test without the need to compile and link against a weighty library. Just #include the EMUtest.h header in your test file(s) and you are ready to begin.

Download

  • Get the latest release of EMU with examples
  • (or) Direct download the latest release version of EMUtest.h without examples
  • (or) Clone the repository containing the latest development changes to EMU:
$ git clone https://notabug.org/VictorSCushman/EMU.git

Writing Your First Test

Start off by adding the EMUtest header to your include path and #include the header in your test file.

#include <EMUtest.h>

The boilerplate for all EMU tests is as follows:

EMU_TEST(/* put test name here */)
{
    /* put any number of statements here */
    EMU_END_TEST(); // last line should ALWAYS be EMU_END_TEST();
}

Let's create a test called MyTest.

/* examples/readme/basic/readme_basic.c */
#include <EMUtest.h>

EMU_TEST(MyTest)
{
    /* nothing here yet */
    EMU_END_TEST();
}

Notice that MyTest isn't a quoted string. Test names are very similar to variable names, and you should use the same rules for naming a test as you would when naming a global variable. Names like SomeFunc or _foo are fine while names like $uperCoolTest or My.Test.Case will cause compile time errors. Keep in mind that just like variable names, test names should be unique in order to prevent name conflicts. Be descriptive with your test names!


A test isn't really a test unless you actually test something, so let's break out some of EMU's testing tools. Say we have a function whose expected behavior is to always return true.

bool always_true(void);

We would expect this function to return true if we called it, so we can use the EMU_EXPECT_TRUE tool to check whether that expectation is fulfilled.

/* examples/readme/basic/readme_basic.c */
#include <stdbool.h>
#include <EMUtest.h>

bool always_true(void){return false; /* oh no */}

EMU_TEST(MyTest)
{
    EMU_EXPECT_TRUE(always_true());
    EMU_END_TEST();
}

If always_true() doesn't return true, EMU_EXPECT_TRUE will signal a failure and print that failure to stdout.


EMU tests don't just run automatically; they need to be specifically told to run. To run a test, write EMU_RUN(testname); inside a function. The corresponding test declared with EMU_TEST(testname) will be run when control reaches EMU_RUN.

/* examples/readme/basic/readme_basic.c */
#include <stdbool.h>
#include <EMUtest.h>

bool always_true(void){return false; /* oh no */}

EMU_TEST(MyTest)
{
    EMU_EXPECT_TRUE(always_true());
    EMU_END_TEST();
}

int main(void)
{
    EMU_RUN(MyTest);
    return 0;
}

Compiling and running the code above (from the directory containing EMUtest.h yields:

$ gcc -I. examples/readme/basic/readme_basic.c
$ ./a.out
[TEST      ] ---> MyTest
| EXPECT TRUE FAILURE:
|   FILE = "examples/readme/basic/readme_basic.c"
|   LINE = 9
[TEST  FAIL]

Oh no, the test failed! The EMU_EXPECT_TRUE tool signaled failure, printing the file and line number where the failing EMU_EXPECT_TRUE was written.


Let's fix the code.

/* examples/readme/basic/readme_basic_fixed.c */
#include <stdbool.h>
#include <EMUtest.h>

bool always_true(void){return true; /* much better */}

EMU_TEST(MyTest)
{
    EMU_EXPECT_TRUE(always_true());
    EMU_END_TEST();
}

int main(void)
{
    EMU_RUN(MyTest);
    return 0;
}
$ gcc -I. examples/readme/basic/readme_basic_fixed.c
$ ./a.out
[TEST      ] ---> MyTest
[TEST  PASS] ---> MyTest

Congratulations! You just wrote a passing EMU test!

EMU API

Test/Group Definition & Execution

Macro What it Does
EMU_TEST(testname) define a test with name testname (see Writing Your First Test)
EMU_GROUP(groupname) define a group with name groupname(see Organizing Tests Into Groups)
EMU_RUN(name) run the test or group called name (see Writing Your First Test)

Test Tools

A note about EXPECT vs. ASSERT

  • Tools of the form EMU_EXPECT_* will continue test execution on failure.
  • Tools of the form EMU_ASSERT_* will halt test execution on failure and immediately return from the test with failure status.
    • ASSERT tools are great for precondition checking necessary for the success of later tests and/or parts of the current test.
  • Both the EXPECT and ASSERT sets of tools accept variables or expressions for their arguments.
    • In general, you want to keep expressions passed to testing tools short so the purpose of a test statement can be easily understood.

List of testing tools

Expect Tool Assert Tool Passes If
EMU_EXPECT_TRUE(expr) EMU_ASSERT_TRUE(expr) (expr) != 0
EMU_EXPECT_FALSE(expr) EMU_ASSERT_FALSE(expr) (expr) == 0
EMU_EXPECT_EQ(a, b) EMU_ASSERT_EQ(a, b) (a) == (b)
EMU_EXPECT_NE(a, b) EMU_ASSERT_NE(a, b) (a) != (b)
EMU_EXPECT_GT(a, b) EMU_ASSERT_GT(a, b) (a) > (b)
EMU_EXPECT_GE(a, b) EMU_ASSERT_GE(a, b) (a) >= (b)
EMU_EXPECT_LT(a, b) EMU_ASSERT_LT(a, b) (a) < (b)
EMU_EXPECT_LE(a, b) EMU_ASSERT_LE(a, b) (a) <= (b)
EMU_EXPECT_NULL(expr) EMU_ASSERT_NULL(expr) (expr) == NULL
EMU_EXPECT_NOT_NULL(expr) EMU_ASSERT_NOT_NULL(expr) (expr) != NULL
EMU_EXPECT_FEQ(a, b, epsilon) EMU_ASSERT_FEQ(a, b, epsilon) floating point numbers a and b have less than epsilon difference between them. The value EMU_DEFAULT_EPSILON is defined as an acceptable epsilon for users unsure of what epsilon to use.
EMU_EXPECT_STREQ(a, b) EMU_ASSERT_STREQ(a, b) the contents of cstrings a and b are the same
EMU_EXPECT_STRNE(a, b) EMU_ASSERT_STRNE(a, b) the contents of cstrings a and b are NOT the same
EMU_EXPECT_THROW(expr) EMU_ASSERT_THROW(expr) expr throws an exception when executed (C++ only)
EMU_EXPECT_THROW_TYPE(expr, excep_t) EMU_ASSERT_THROW_TYPE(expr, excep_t) expr throws an exception of type excep_t when executed (C++ only)
EMU_EXPECT_NO_THROW(expr) EMU_ASSERT_NO_THROW(expr) expr does NOT throw an exception when executed (C++ only)

Typed testing tools

Testing tools of the form *_EQ, *_NE, *_GT, *_GE, *_LT, and *_LE all have special typed forms that will give verbose output describing the values of arguments a and b if the test statement fails.

Note that arguments a and b will both be cast to the specified type for printing. This may lead to misleading information if the types of your arguments don't match the tool you selected. EMU performs no type checking, so be sure to use the correct tool for your types.

Tool Use for Types
basetool_CHAR(a, b) signed char
unsigned char
basetool_INT(a, b) signed short
signed int
signed long
signed long long
basetool_UINT(a, b) unsigned short
unsigned int
unsigned long
unsigned long long
basetool_FLOAT(a, b) float
double

Example:

/* examples/readme/readme_typed.c */
#include <EMUtest.h>

EMU_TEST(untyped_ex)
{
    int ans = 7 * 7;
    EMU_EXPECT_EQ(ans, 50);
    EMU_END_TEST();
}

EMU_TEST(typed_ex)
{
    int ans = 7 * 7;
    EMU_EXPECT_EQ_INT(ans, 50);
    EMU_END_TEST();
}

int main(void)
{
    EMU_RUN(untyped_ex);
    EMU_RUN(typed_ex);
    return 0;
}
$ gcc -I. examples/readme/readme_typed.c
$ ./a.out
[TEST      ] ---> untyped_ex
| EXPECT EQ FAILURE:
|   FILE = "examples/readme/readme_typed.c"
|   LINE = 7
[TEST  FAIL]
[TEST      ] ---> typed_ex
| EXPECT EQ INT FAILURE: expected 49 to equal 50
|   FILE = "examples/readme/readme_typed.c"
|   LINE = 12
[TEST  FAIL]

Misc. Macros

Macro What it Does
EMU_FAIL(optional_msg) immediately exit the current test with failure status, printing the string literal optional_msg
NOTE: optional_msg is optional i.e. EMU_FAIL() and EMU_FAIL("some text") are both valid
EMU_PRINT_INDENT() print indent strings up to the current indent level (see Formatting Output)
EMU_PRINT_LINE_BREAK() print indent strings up to the current indent level followed by a newline (see Formatting Output)
EMU_DEFAULT_EPSILON predefined epsilon for use with EMU_tooltype_FEQ

Advanced Testing

Organizing Tests Into Groups

EMU groups are a way to organize sets of tests within a larger project. An EMU group automatically runs all tests added to the group when called with EMU_RUN. The boilerplate for an EMU group is similar to that of an EMU test:

EMU_GROUP(/* put group name here */)
{
    /* add tests/groups here */
    EMU_END_GROUP(); // last line should ALWAYS be EMU_END_GROUP();
}

Although tests and groups use similar syntax they are structured differently under the hood, so attempting to use test tools inside an EMU group will result in a compile time error.


To demo EMU groups we will use two relatively simple functions, both of which contain "bugs". For simplicity we will avoid using separate header, implementation, and test files for each function and instead just stick everything in a couple of .c files.

/* examples/readme/advanced/capitalize.c */
#include <ctype.h>
#include <EMUtest.h>

void capitalize(char* str)
{
    str[0] = toupper(str[0]);
}

EMU_TEST(capitalize)
{
    char test_str[64];

    // Non capitalized sentence.
    strcpy(test_str, "this is a sentence.");
    capitalize(test_str);
    EMU_EXPECT_STREQ(test_str, "This is a sentence.");

    // Already capitalized sentence.
    strcpy(test_str, "This is a sentence.");
    capitalize(test_str);
    EMU_EXPECT_STREQ(test_str, "This is a sentence.");

    // String where the first letter isn't alphabetical.
    strcpy(test_str, " this is a sentence.");
    capitalize(test_str);
    EMU_EXPECT_STREQ(test_str, "This is a sentence."); // OOPS!

    EMU_END_TEST();
}
/* examples/readme/advanced/square.c */
#include <EMUtest.h>

int square(int x)
{
    return x*x;
}

EMU_TEST(square)
{
    EMU_EXPECT_EQ(square(1), 1);
    EMU_EXPECT_EQ(square(-1), 1);

    EMU_EXPECT_EQ(square(2), 4);
    EMU_EXPECT_EQ(square(-2), 4);

    EMU_EXPECT_EQ(square(3), 9);
    EMU_EXPECT_EQ(square(-3), 9);

    EMU_EXPECT_EQ(square(4), 16);
    EMU_EXPECT_EQ(square(-4), 16);

    EMU_EXPECT_EQ(square(5), 25);
    EMU_EXPECT_EQ(square(-5), 25);

    EMU_EXPECT_EQ(square(3.5*3.5), 12.25); //OOPS!

    EMU_END_TEST();
}

We have two tests in separate files and we want to run them together with one statement using a group. We'll create a centralized main.test.c as a place to stick our group and main function.


To start off we #include <EMUtest.h> and add the EMU group boilerplate.

/* examples/readme/advanced/main.test.c */
#include <EMUtest.h>

EMU_GROUP(all_tests)
{
    /* nothing added yet */
    EMU_END_GROUP();
}

Tests are added to the current group using EMU_ADD(testname); within the group block. After adding the tests from capitalize.c and square.c, our group looks like this:

/* examples/readme/advanced/main.test.c */
#include <EMUtest.h>

EMU_GROUP(all_tests)
{
    EMU_ADD(capitalize);
    EMU_ADD(square);
    EMU_END_GROUP();
}

To run a group of tests use EMU_RUN(groupname); just like you would with a test.

/* examples/readme/advanced/main.test.c */
#include <EMUtest.h>

EMU_GROUP(all_tests)
{
    EMU_ADD(capitalize);
    EMU_ADD(square);
    EMU_END_GROUP();
}

int main(void)
{
    EMU_RUN(all_tests);
    return 0;
}

Notice that our tests and our group are in different .c files. During the compilation step the group in main.test.c has no way of knowing that the individual tests in our other .c files actually exist. To solve this issue use EMU_DECLARE(name); to bring in declarations for tests and groups found in other files similar to how forward function declarations are used in C/C++.

/* examples/readme/advanced/main.test.c */
#include <EMUtest.h>

EMU_DECLARE(capitalize);
EMU_DECLARE(square);

EMU_GROUP(all_tests)
{
    EMU_ADD(capitalize);
    EMU_ADD(square);
    EMU_END_GROUP();
}

int main(void)
{
    EMU_RUN(all_tests);
    return 0;
}

Compiling and running the code we get:

$ gcc -I. \
examples/readme/advanced/capitalize.c \
examples/readme/advanced/square.c \
examples/readme/advanced/main.test.c
$ ./a.out
[GROUP     ] ===> all_tests
| [TEST      ] ---> capitalize
| | EXPECT STREQ FAILURE:
| |   FILE = "examples/readme/advanced/capitalize.c"
| |   LINE = 27
| [TEST  FAIL]
| [TEST      ] ---> square
| | EXPECT EQ FAILURE:
| |   FILE = "examples/readme/advanced/square.c"
| |   LINE = 26
| [TEST  FAIL]
[GROUP FAIL]

Nesting groups

One final point worth mentioning about groups is that they are designed to be nested. This allows for a project to have a tree-like structure of tests that can all be run with one top-level EMU_RUN statement.

EMU tests return 1 on failure and 0 on success. EMU groups return the number of failing tests added to the group (including tests within nested groups). Thus nesting can be used to track the total number of failing tests within a project.

Here is an example of group nesting:

/* examples/readme/advanced/nested_groups.c */
#include <stdio.h>
#include <EMUtest.h>

EMU_TEST(test1) // will fail
{
    EMU_EXPECT_TRUE(0);
    EMU_END_TEST();
}

EMU_TEST(test2) // will fail
{
    EMU_EXPECT_STREQ("different", "strings");
    EMU_END_TEST();
}

EMU_TEST(test3) // will pass
{
    EMU_EXPECT_TRUE(1);
    EMU_END_TEST();
}

EMU_GROUP(group1) // 2 failures
{
    EMU_ADD(test1);
    EMU_ADD(test2);
    EMU_ADD(test3);
    EMU_END_GROUP();
}

//====================================//

EMU_TEST(test4) // will fail
{
    EMU_EXPECT_NULL(0xffffffffffffffff);
    EMU_END_TEST();
}

//====================================//

EMU_TEST(test5) // will fail
{
    EMU_EXPECT_EQ(0, 1);
    EMU_END_TEST();
}

EMU_GROUP(group2) // 1 failure
{
    EMU_ADD(test5);
    EMU_END_GROUP();
}

//====================================//

EMU_TEST(test6) // will pass
{
    EMU_END_TEST();
}


EMU_GROUP(group3) // 0 failures
{
    EMU_ADD(test6);
    EMU_END_GROUP();
}

//====================================//

EMU_GROUP(all_tests)
{
    EMU_ADD(group1); // 2 failures
    EMU_ADD(test4);  // 1 failure
    EMU_ADD(group2); // 1 failure
    EMU_ADD(group3); // 0 failures
    EMU_END_GROUP();
}

int main(void)
{
    int total_failures = EMU_RUN(all_tests);
    printf("Total failures = %d\n", total_failures);
    return 0;
}
$ gcc -I. examples/readme/advanced/nested_groups.c
$ ./a.out
[GROUP     ] ===> all_tests
| [GROUP     ] ===> group1
| | [TEST      ] ---> test1
| | | EXPECT TRUE FAILURE:
| | |   FILE = "examples/readme/advanced/nested_groups.c"
| | |   LINE = 7
| | [TEST  FAIL]
| | [TEST      ] ---> test2
| | | EXPECT STREQ FAILURE:
| | |   FILE = "examples/readme/advanced/nested_groups.c"
| | |   LINE = 11
| | [TEST  FAIL]
| | [TEST      ] ---> test3
| | [TEST  PASS]
| [GROUP FAIL]
| [TEST      ] ---> test4
| | EXPECT NULL FAILURE:
| |   FILE = "examples/readme/advanced/nested_groups.c"
| |   LINE = 27
| [TEST  FAIL]
| [GROUP     ] ===> group2
| | [TEST      ] ---> test5
| | | EXPECT EQ FAILURE:
| | |   FILE = "examples/readme/advanced/nested_groups.c"
| | |   LINE = 33
| | [TEST  FAIL]
| [GROUP FAIL]
| [GROUP     ] ===> group3
| | [TEST      ] ---> test6
| | [TEST  PASS]
| [GROUP PASS]
[GROUP FAIL]
Total failures = 4

Testing Without Logging

EMU uses the standard header stdio.h to log test results to stdout. To prevent logging to stdout add #define _EMU_DISABLE_LOGGING_ before #include <EMUtest.h> in test files that call tests/groups with EMU_RUN.

Settings for disabling logging and enabling color (discussed later) are set based on the #define statements within the file EMU_RUN is called from. Global settings for a project can be set by adding #define statements to the file from which EMU_RUN calls the top-level group for that project.

Using the return values of from tests/groups, it is fairly easy to return the number of failing tests in a project. Here is the main.test.c file from before with logging disabled.

/* examples/readme/advanced/main.test.nolog.c */
#define _EMU_DISABLE_LOGGING_
#include <EMUtest.h>

EMU_DECLARE(capitalize);
EMU_DECLARE(square);

EMU_GROUP(all_tests)
{
    EMU_ADD(capitalize);
    EMU_ADD(square);
    EMU_END_GROUP();
}

int main(void)
{
    return EMU_RUN(all_tests);
}
$ gcc -I. \
examples/readme/advanced/capitalize.c \
examples/readme/advanced/square.c \
examples/readme/advanced/main.test.nolog.c
$ ./a.out
$ echo "Total failed tests: $?"
Total failed tests: 2

Formatting Output

When working on larger projects the output from EMU can get a bit dense. While not required, separating tests and groups with newlines and other character blurbs can go a long way towards producing more readable output. EMU provides a macro EMU_PRINT_INDENT() that will print indent strings ("| ") such that further printing will occur on the same indent level as the current test/group. The macro EMU_PRINT_LINE_BREAK() will print a line with indent strings followed by a newline. The user is encouraged to experiment with EMU and develop their own preferred style.

As an example:

/* examples/readme/advanced/main.test.pretty.c */
#include <stdio.h>
#include <EMUtest.h>

EMU_DECLARE(capitalize);
EMU_DECLARE(square);

EMU_GROUP(all_tests)
{
    EMU_PRINT_INDENT(); putchar('\n'); // These two lines
    EMU_PRINT_LINE_BREAK();            // are eqivalent

    EMU_ADD(capitalize);
    EMU_PRINT_LINE_BREAK();

    EMU_PRINT_INDENT();
    puts("This is a message between tests!");
    EMU_PRINT_LINE_BREAK();

    EMU_ADD(square);
    EMU_PRINT_LINE_BREAK();

    EMU_END_GROUP();
}

int main(void)
{
    EMU_RUN(all_tests);
    return 0;
}
$ gcc -I. \
examples/readme/advanced/capitalize.c \
examples/readme/advanced/square.c \
examples/readme/advanced/main.test.pretty.c
$ ./a.out
[GROUP     ] ===> all_tests
| 
| 
| [TEST      ] ---> capitalize
| | EXPECT STREQ FAILURE:
| |   FILE = "examples/readme/advanced/capitalize.c"
| |   LINE = 27
| [TEST  FAIL]
| 
| This is a message between tests!
| 
| [TEST      ] ---> square
| | EXPECT EQ FAILURE:
| |   FILE = "examples/readme/advanced/square.c"
| |   LINE = 26
| [TEST  FAIL]
| 
[GROUP FAIL]

If you plan on running your tests from a terminal that supports ANSI escape codes then adding the line #define _EMU_ENABLE_COLOR_ before #include <EMUtest.h> will print test results in multicolor as opposed to monocolor. The rule for how to #define color enabling is the same as the rule for disabling logging: #define statements within the file from which EMU_RUN is called will determine settings for all nested tests/groups. An example program you can run to see if your terminal supports color printing with EMU can be found below:

/* examples/readme/advanced/enable_color.c */
#define _EMU_ENABLE_COLOR_
#include <EMUtest.h>

EMU_TEST(passing_test)
{
    EMU_EXPECT_TRUE(1);
    EMU_END_TEST();
}

EMU_GROUP(passing_group)
{
    EMU_ADD(passing_test);
    EMU_END_GROUP();
}

EMU_TEST(failing_test)
{
    EMU_EXPECT_TRUE(0);
    EMU_END_TEST();
}

EMU_GROUP(failing_group)
{
    EMU_ADD(failing_test);
    EMU_END_GROUP();
}

int main(void)
{
    EMU_RUN(passing_group);
    EMU_RUN(failing_group);
    return 0;
}

How exactly does EMU extend MinUnit?

It doesn't...mostly.

You may think it's odd that a unit testing framework called Extended MinUnit isn't actually an extension of MinUnit, and I would have to agree with you there. I started work on the initial version of EMU during an overnight bus ride from Philadelphia to Boston. The original goal of the project was to create a superset of MinUnit with a slightly broader set of tools (so users didn't have to rely solely on mu_assert). Over the course of the night EMU drifted further and further away from MinUnit's source. As a big fan and long-time user of Google Test I wanted to keep the syntax of EMU close to what I already knew, partially so other gtest users would be at least somewhat familiar with EMU's syntax, but mostly because it made my life easier. EMU changed from lower_snake_case to UPPER_SNAKE_CASE, stole the look of gtest (at least for the initial version), and replaced the "mu" prefix with "EMU" to fully separate the two frameworks.

By the time my bus arrived in Boston, EMU was far from a superset of MinUnit which begs the question, why didn't I just name it something else? Although EMU no longer shares syntax with MinUnit, and although EMU is now a lot longer than MinUnit, Extended MinUnit is still being developed with same goals as its predecessor. MinUnit's author (John Brewer) says it best in the conclusion of the MinUnit tech note.

People think that writing a unit testing framework has to be complex. In fact you can write one in just a few lines of code, as this tech note shows. Of course, if you have access to a full-featured testing framework like JUnit, by all means use it. But if you don't, you can still use a simple framework like MinUnit, or whip up your own in a few hours. There's no excuse for not unit testing.

EMU isn't going to replace Google Test, CUnit, or any of the other dozens of professionally developed unit testing frameworks out there, but what it can do is give anyone an easy-to-handle set of tools they can use to start unit testing even if they had zero experience with testing prior to finding EMU. Extended MinUnit doen't extend the source of MinUnit, but it does extend the idea behind MinUnit: any testing is better than no testing, and if users don't want to use something as complex and overwhelming as some of the frameworks above, maybe they can use EMU.

If you haven't already checked out MinUnit then you should do that now; I promise it's worth your time. MinUnit is beautifully simple and does a lot to show how a little innovation can go a long way.

License

MIT