SkillAgentSearch skills...

Narwhal

A progressive test framework for C.

Install / Use

/learn @vberlier/Narwhal
About this skill

Quality Score

0/100

Supported Platforms

Zed

README

⚓️ narwhal

Build Status GitHub tag (latest SemVer)

<img align="right" width="56%" src="https://raw.githubusercontent.com/vberlier/narwhal/master/examples/demo.svg?sanitize=true">

A progressive test framework for C.

Narwhal is a framework that makes it easy to write readable and maintainable tests for C programs and libraries.

#include "narwhal.h"

TEST(example)
{
    ASSERT_EQ("actual", "expected");
}
$ gcc *.c -o run_tests
$ ./run_tests

Check out the basic example for more details about this code snippet.

Features

  • Automatic test discovery
  • Use the same generic assertions everywhere
  • Assertion failures reported as diffs
  • Test output captured and displayed on failure
  • Create, combine and reuse test parameters
  • Externalize and compose setup and teardown code with fixtures
  • Easy-to-use resource management and output capturing utilities
  • Amalgamated source file and header ready to drop in your project
  • Mocking support with Narmock
  • Works well with errors reported by sanitizers

Installation

Narwhal is distributed as an amalgamated source file and header:

Drop the two files in your project, make sure narwhal.c is compiled and linked just like the other source files of your test program and you should be good to go.

<details> <summary> Narwhal can also be installed as a shared library. <strong>Click to see more</strong> </summary>

Building and installing the shared library

First, you'll need to download the source code.

$ git clone git://github.com/vberlier/narwhal.git && cd narwhal

You can now install the project with make install. The default destination is /usr so you'll need to run the command with the appropriate permissions if you want to install Narwhal globally.

$ sudo make install

You can use the DESTDIR variable to specify where Narwhal should be installed.

$ make install DESTDIR=~/my_project

This command will copy the libnarwhal.so library to ~/my_project/lib/ and the necessary headers to ~/my_project/include.

Uninstalling the shared library

You can uninstall Narwhal with make uninstall. The DESTDIR variable lets you specify from where the Narwhal library and the related headers should be removed.

$ make uninstall DESTDIR=~/my_project

The default destination is /usr so you'll need to run the command with the appropriate permissions if you want to uninstall a global Narwhal installation.

$ sudo make uninstall
</details>

Framework overview

Narwhal supports automatic test discovery, meaning that by default the framework provides a main function that runs all the tests defined in the executable.

If you're using a compiler that doesn't support GNU extensions or simply need to write your own main function, check out the section on using Narwhal without auto-discovery.

Defining tests

Narwhal lets you define tests using the TEST macro. The first argument is the name of the test. Note that the test name must be a valid identifier. The macro invocation should be followed by the test body, defined between curly braces.

TEST(example)
{
    // Test body
}

The test body is simply a function body in which you can write your test.

Using assertions

Narwhal defines a few macros that are meant to be used inside of tests to report failures. The most basic one is FAIL. It simply notifies Narwhal that the test failed and stops the test execution. You can optionally include an error message and use formatting to provide more details.

TEST(example1)
{
    /* ... */

    if (result != 42)
    {
        FAIL();  // The test execution stops and Narwhal reports a failure
    }
}

TEST(example2)
{
    /* ... */

    if (result != 42)
    {
        FAIL("The result should be 42.");
    }
}

TEST(example3)
{
    /* ... */

    if (result != 42)
    {
        FAIL("The result should be 42 but got %d.", result);
    }
}

In practice, you might not want to use FAIL directly unless the way you check that the test has failed doesn't rely on a meaningful expression. Most of the time, you'll actually use an assertion macro. The ASSERT macro essentially replaces the if (!assertion) FAIL() construct. The first argument of the macro should be the assertion that needs to be true for the test to succeed. If the assertion evaluates to false, Narwhal will report a failure and stop the test execution. You can also include an error message with formatting after the assertion.

TEST(example1)
{
    /* ... */

    ASSERT(result == 42);
}

TEST(example2)
{
    /* ... */

    ASSERT(result == 42, "The result should be 42.");
}

TEST(example3)
{
    /* ... */

    ASSERT(result == 42, "The result should be 42 but got %d.", result);
}

If the assertion is a simple equality check, you can let Narwhal perform the comparison and format the error message for you by using the ASSERT_EQ macro. The macro is generic and works with most signed and unsigned integers of various sizes, floats and doubles. It can compare pointers and if the arguments are strings, it will check that they are identical using strcmp. Upon failure, Narwhal will display the values of both the actual and the expected result. If the values are strings, their differences will be highlighted in a formatted diff.

TEST(example)
{
    /* ... */

    ASSERT_EQ(result, 42);
}

The ASSERT_NE macro can be used to perform the opposite check.

TEST(example)
{
    /* ... */

    ASSERT_NE(result, -1);
}

In addition, four basic comparison assertion macros are also available: ASSERT_LT, ASSERT_LE, ASSERT_GT and ASSERT_GE standing respectively for less than, less than or equal to, greater than and greater than or equal to.

TEST(example)
{
    /* ... */

    ASSERT_LT(result, 43);
    ASSERT_LE(result, 42);
    ASSERT_GT(result, 41);
    ASSERT_GE(result, 42);
}

Narwhal also provides the ASSERT_SUBSTRING and ASSERT_NOT_SUBSTRING macros. They both take two strings as parameters and check that the first one contains or doesn't contain the second respectively.

TEST(example)
{
    /* ... */

    ASSERT_SUBSTRING(long_string, "Hello, world!");
    ASSERT_NOT_SUBSTRING(long_string, "An error occurred");
}

You can check that two chunks of memory are equal with the ASSERT_MEMORY macro. The first and second arguments must be pointers and the third one should specify the number of bytes that are going to be compared. Upon failure, Narwhal will highlight the differences between the two chunks of memory in a formatted hexdump diff.

TEST(example)
{
    /* ... */

    uint8_t expected[] = { 'a', 'b', 'c', 42 };
    ASSERT_MEMORY(result, expected, sizeof(expected));
}

Adding parameters to tests

Running a test with various different inputs can be quite useful. Instead of duplicating the test and only changing some hard-coded values, you can let Narwhal run your test several times with different inputs by using a test parameter. You can create a test parameter with the TEST_PARAM macro. The first argument of the macro is the name of the test parameter. It must be a valid identifer. The second argument is the type of the parameter. The last argument must be an array literal that contains all the values that you want the parameter to take.

TEST_PARAM(arbitrary_number, int, { 0, -1, 8, 42 });

In order to apply a parameter to a test, you must include it in the list of test modifiers. The test modifiers are specified right after the test name in a test definition.

TEST(example, arbitrary_number)
{
    // The test will run for each possible value of the parameter
}

If you specify multiple test parameters in the list of test modifiers, the test will run for every possible combination.

TEST_PARAM(param1, char *, { "one", "two" });
TEST_PARAM(param2, int, { 1, 2, 3, 4 });

TEST(example, param1, param2)
{
    // The test will run with 8 different parameter combinations
}

In order to have access to the current value of a parameter inside of the test body, you'll need to use the GET_PARAM macro. The only argument of the macro is the name of the parameter that you want to bring in scope.

TEST(example, param1, param2)
{
    GET_PARAM(param1);
    GET_PARAM(param2);

    // The current value of each parameter can now be used in the test
}

If you want to declare a test parameter inside of a header file, you'll need to use the DECLARE_PARAM macro. The first argument is the name of the parameter and the second one is the type.

// arbitrary_number.h

#ifndef ARBITRARY_NUMBER_H
#define ARBITRARY_NUMBER_H

#include "narwhal.h"

DECLARE_PARAM(arbitrary_number, int);

#endif
// arbitrary_number.c

#include "narwhal.h"

TEST_PARAM(arbitrary_number, int, { 0, -1, 8, 42 });

Using test fixtures

Test fixtures let you provide data to a test from the outside. This allows you to keep the body of the test focused on making assertions instead of executing setup and teardown code. In addition, by extracting cleanup instructions outside of the test, you can be sure that they will be executed even if the test fails. Narwhal fixtures are inspired by pytest.

In order to define a test fixture, you'll need to use the TEST_FIXTURE macro. The first argument of the macro i

Related Skills

View on GitHub
GitHub Stars117
CategoryDevelopment
Updated1y ago
Forks4

Languages

C

Security Score

85/100

Audited on Sep 14, 2024

No findings