7

I'm writing a framework that also includes a simple logger that can be enabled/disabled at runtime. Since I want have unit tests for as much of the framework as possible, I also want to test whether the logger works correctly.

The logger simply logs via NSLog. Now I need to test whether the output really matches the expectation (i.e. does it really log and is the output in the correct format). I couldn't find a way to do that using Xcode's XCTest framework.

I could modify the logger so that it doesn't use NSLog while testing but that strikes me as error-prone. So, is there a better way to check the NSLog output?

DarkDust
  • 90,870
  • 19
  • 190
  • 224
  • I'm new to TDD so can you explain what the reason of testing system API in your project instead of mocking NSLog and test code around it – sage444 Feb 09 '16 at 11:41
  • and as answer you can check this question http://stackoverflow.com/q/9619708/1403732 where asker told that he redirected `NSLog` stream to file. I think it's good starting point for you – sage444 Feb 09 '16 at 11:45
  • I do _not_ want to test whether `NSLog` works, I want to test whether the logging is correctly done, until the very end. That involves checking the output of `NSLog`, if possible. Redirecting might indeed be a way to investigate. It would probably hide most/all output from the testing environment itself so I must redirect just for the test and then restore redirection… it's something I should play around with. Had thought about it before but missed the point that I can probably restore redirection after my test. – DarkDust Feb 09 '16 at 11:49
  • @sage444: So I did solve it by redirecting the file descriptor. Thanks for making me think about that again :-) – DarkDust Feb 11 '16 at 09:40

2 Answers2

1

Have the logger output strings. Have another object print these. This way you can unit test the first object separately from the second.

GabLeRoux
  • 16,715
  • 16
  • 63
  • 81
Joride
  • 3,722
  • 18
  • 22
  • Interesting idea. Although this would allow me to test whether the output has the desired content, I would still not be able to test whether the output is actually happening. – DarkDust Feb 05 '16 at 12:00
  • The question then is: do you not trust NSLog? You want to write a unites for a function in a system framework? – Joride Feb 05 '16 at 20:05
  • It's not about NSLog specifically, actually. It's about testing whether the complete action (log a message) is executed, until the very end. All ideas I've come up with so far (also with your suggestion) only allow me to test a part of the code: there is always going to be some code that I currently cannot test and that thus can be broken accidentally without anyone noticing. I've come up with a design that will reduce the non-testable code to just one trivial line, though. So that's sufficient for me now but I'd prefer a 100% solution. – DarkDust Feb 07 '16 at 09:09
  • I see. The only way to write a unit test for making sure NSLog actually wrote to the console would be to check the console. Not sure how/if you can do that. – Joride Feb 07 '16 at 13:21
0

I solved this by writing a utility class that redirects a file descriptor to a file and is able to reset this redirection:

@interface IORedirector : NSObject

- (instancetype)initWithFileDescriptor:(int)fileDescriptor targetPath:(NSURL *)fileURL flags:(int)oflags mode:(mode_t)mode;

- (void)reset;

@end

@implementation IORedirector
{
    int _fileDescriptor;
    int _savedFileDescriptor;
}

- (instancetype)initWithFileDescriptor:(int)fileDescriptor targetPath:(NSURL *)fileURL flags:(int)oflags mode:(mode_t)mode
{
    _savedFileDescriptor = dup(fileDescriptor);
    if (_savedFileDescriptor == -1) {
        NSLog(@"Could not save file descriptor %d: %s", fileDescriptor, strerror(errno));
        return nil;
    }

    int tempFD = open(fileURL.path.fileSystemRepresentation, oflags, mode);
    if (tempFD == -1) {
        NSLog(@"Could not open %@: %s", fileURL.path, strerror(errno));
        close(_savedFileDescriptor);
        return nil;
    }

    if (close(fileDescriptor) == -1) {
        NSLog(@"Closing file descriptor %d failed: %s", fileDescriptor, strerror(errno));
        close(_savedFileDescriptor);
        close(tempFD);
        return nil;
    }

    if (dup2(tempFD, fileDescriptor) == -1) {
        NSLog(@"Could not replace file descriptor %d with new one for %@: %s", fileDescriptor, fileURL.path, strerror(errno));
        close(_savedFileDescriptor);
        close(tempFD);
        return nil;
    }

    _fileDescriptor = fileDescriptor;
    close(tempFD);

    return self;
}

- (void)dealloc
{
    [self reset];
}

- (void)reset
{
    if (_savedFileDescriptor == -1) return;

    if (close(_fileDescriptor) == -1) {
        NSLog(@"Closing file descriptor %d failed: %s", _fileDescriptor, strerror(errno));
        return;
    }

    if (dup2(_savedFileDescriptor, _fileDescriptor) == -1) {
        NSLog(@"Could not restore file descriptor %d: %s", _fileDescriptor, strerror(errno));
        return;
    }

    close(_savedFileDescriptor);
    _savedFileDescriptor = -1;
}

@end

The initializer either returns a valid instance (redirection established) or nil. Call reset to restore the redirection immediately.

Both the initializer and reset can fail and "lose" the original file descriptor, but the window is small and unlikely. This is probably only relevant when using this class in a multithreaded app (which I don't), so if you intend to use this in a multithreaded environment you want to make sure that only one initializer or reset can run at a time using a global mutex or something.

Using this class (which I've got a separate test case for, of course), I can redirect STDERR_FILENO to a file and thus catch the output of NSLog. This way I can test the unmodified logging class.

Creating a suitable path to a temporary file is left as an exercise to the reader.

DarkDust
  • 90,870
  • 19
  • 190
  • 224