Search code examples
objective-cunit-testinguser-inputxctestnsfilehandle

Is there any way to emulate user's input in XCTest function?


I have method which expects user's input

@implementation TeamFormation
- (void)run {
    NSFileHandle *kbd = [NSFileHandle fileHandleWithStandardInput];
    NSData *inputData = [kbd availableData];
    NSString *option = [[[NSString alloc] initWithData:inputData
                     encoding:NSUTF8StringEncoding] substringToIndex:1];
    NSLog(@"%@",option);
}
@end

Then I would like to cover this method by a test case

@interface TeamFormationTests : XCTestCase

@end

@implementation TeamFormationTests

- (void)testTeamFormation {
    TeamFormation *teamFormation = [TeamFormation new];
    [teamFormation run];

    // emulate user's input here
}

@end

So, how to emulate user's input in test case function?


Solution

  • You have many options how to achieve this. Two obvious below.

    Change run to accept an argument

    • - (void)run to - (void)runWithFileHandle:(NSFileHandle *)handle
    • your app code can pass stdin filehandle
    • your test code can pass handle to a file with desired input

    Mock it with protocol

    Create DataProvider protocol:

    @protocol DataProvider
    
    @property(readonly, copy) NSData *availableData;
    
    @end
    

    Make NSFileHandle to conform to this protocol:

    @interface NSFileHandle (AvailableDataProvider) <DataProvider>
    @end
    

    Store an object implementing this protocol on TeamFormation:

    @interface TeamFormation : NSObject
    
    @property (nonatomic, nonnull, strong) id<DataProvider> dataProvider;
    
    - (NSString *)run;
    
    @end
    

    By default, use stdin file handle:

    @implementation TeamFormation
    
    - (instancetype)init {
        if ((self = [super init]) == nil) {
            return nil;
        }
        
        _dataProvider = [NSFileHandle fileHandleWithStandardInput];
        return self;
    }
    
    - (NSString *)run {
        NSData *inputData = [self.dataProvider availableData];
        return [[[NSString alloc] initWithData:inputData encoding:NSUTF8StringEncoding] substringToIndex:1];
    }
    
    @end
    

    Create TestDataProvider in your test:

    @interface TestDataProvider: NSObject<DataProvider>
    
    @property (nonatomic, strong, nonnull) NSData *dataToProvide;
    
    @end
    
    @implementation TestDataProvider
    
    - (instancetype)init {
        if ((self = [super init]) == nil) {
            return nil;
        }
        
        _dataToProvide = [NSData new];
        
        return self;
    }
    
    - (NSData *)availableData {
        return _dataToProvide;
    }
    
    @end
    

    And use it in TestFormationTests:

    @implementation TeamFormationTests
    
    - (void)testFormationRun {
        TestDataProvider *dataProvider = [TestDataProvider new];
        TeamFormation *formation = [TeamFormation new];
        formation.dataProvider = dataProvider;
        
        XCTAssertThrows([formation run]);
        
        dataProvider.dataToProvide = [@"foo" dataUsingEncoding:NSUTF8StringEncoding];
        XCTAssertEqualObjects([formation run], @"f");
        
        dataProvider.dataToProvide = [@"bar" dataUsingEncoding:NSUTF8StringEncoding];
        XCTAssertEqualObjects([formation run], @"b");
    }
    
    @end