Most of my C library code can be compiled with -DTEST to expose the main() (and often some auxilliary functions too) in the source file with the implementation.  So if I have a set of functions declared in source.h and defined in source.c, then source.c might look like:
#include "source.h"
#include …other headers…
…code defining functions declared in source.h
#ifdef TEST
#include <stdio.h>
int main(void)
{
    …test code…
}
#endif /* TEST */
This works when the test suite is small enough to fit in the source file.  If the tests get bigger than the code, then I create one or more separate source files containing test code.  Each of those files can have its own main(), or they can be designed to be linked together — whichever seems more convenient.
What's appropriate depends on the size and complexity of the tests.  Some functions end up with fixed — hard-wired — tests; some spend time reading data from standard input; others process argument lists if supplied and fall back on some minimal test if there are no arguments.  The test code might use a unit test infrastructure or might be more or less ad hoc, again depending on complexity (and antiquity) of the code.