Unit-testing a simple usage of RACSignal with RACS

2019-04-02 17:27发布

问题:

(I may be using this in a totally incorrect manner, so feel free to challenge the premise of this post.)

I have a small RACTest app (sound familiar?) that I'm trying to unit test. I'd like to test MPSTicker, one of the most ReactiveCocoa-based components. It has a signal that sends a value once per second that accumulates, iff an accumulation flag is set to YES. I added an initializer to take a custom signal for its incrementing signal, rather than being only timer-based.

I wanted to unit test a couple of behaviours of MPSTicker:

  • Verify that its accumulation signal increments properly (i.e. monotonically increases) when accumulation is enabled and the input incrementing signal sends a new value.
  • Verify that it sends the same value (and not an incremented value) when the input signal sends a value.

I've added a test that uses the built-in timer to test the first increment, and it works as I expected (though I'm seeking advice on improving the goofy RACSequence initialization I did to get a signal with the @(1) value I wanted.)

I've had a very difficult time figuring out what input signal I can provide to MPSTicker that I can manually send values to. I'm envisioning a test like:

<set up ticker>
<send a tick value>
<verify accumulated value is 1>
<send another value>
<verify accumulated value is 2>

I tried using a RACSubject so I can use sendNext: to push in values as I see fit, but it's not working like I expect. Here's two broken tests:

- (void)testManualTimerTheFirst
{
    // Create a custom tick with one value to send.
    RACSubject *controlledSignal = [RACSubject subject];
    MPSTicker *ticker = [[MPSTicker alloc] initWithTickSource:controlledSignal];
    [ticker.accumulateSignal subscribeNext:^(id x) {
        NSLog(@"%s value is %@", __func__, x);
    }];

    [controlledSignal sendNext:@(2)];
}

- (void)testManualTimerTheSecond
{
    // Create a custom tick with one value to send.
    RACSubject *controlledSignal = [RACSubject subject];
    MPSTicker *ticker = [[MPSTicker alloc] initWithTickSource:controlledSignal];

    BOOL success = NO;
    NSError *error = nil;
    id value = [ticker.accumulateSignal asynchronousFirstOrDefault:nil success:&success error:&error];

    if (!success) {
        XCTAssertTrue(success, @"Signal failed to return a value. Error: %@", error);
    } else {
        XCTAssertNotNil(value, @"Signal returned a nil value.");
        XCTAssertEqualObjects(@(1), value, @"Signal returned an unexpected value.");
    }

    // Send a value.
    [controlledSignal sendNext:@(1)];
}

In testManualTimerTheFirst, I never see any value from controlledSignal's sendNext: come through to my subscribeNext: block.

In testManualTimerTheSecond, I tried using the asynchronousFirstOrDefault: call to get the first value from the signal, then manually sent a value on my subject, but the value didn't come through, and the test failed when asynchronousFirstOrDefault: timed out.

What am I missing here?

回答1:

This may not answer your question exactly, but it may give you insights on how to effectively test your signals. I've used 2 approaches myself so far:

XCTestCase and TRVSMonitor

TRVSMonitor is a small utility which will pause the current thread for you while you run your assertions. For example:

TRVSMonitor *monitor = [TRVSMonitor monitor];

[[[self.service searchPodcastsWithTerm:@"security now"] collect] subscribeNext:^(NSArray *results) {
    XCTAssertTrue([results count] > 0, @"Results count should be > 0";
    [monitor signal];

} error:^(NSError *error) {
    XCTFail(@"%@", error);
    [monitor signal];
}];

[monitor wait];

As you can see, I'm telling the monitor to wait right after I subscribe and signal it to stop waiting at the end of subscribeNext and error blocks to make it continue executing (so other tests can run too). This approach has the benefit of not relying on a static timeout, so your code can run as long as it needs to.

Using CocoaPods, you can easily add TRVSMonitor to your project:

pod "TRVSMonitor", "~> 0.0.3"

Specta & Expecta

Specta is a BDD/TDD (behavior driven/test driven) test framework. Expecta is a framework which provides more convenient assertion matchers. It has built-in support for async tests. It enables you to write more descriptive tests with ReactiveCocoa, like so:

it(@"should return a valid image, with cache state 'new'", ^AsyncBlock {
    [[cache imageForURL:[NSURL URLWithString:SECURITY_NOW_ARTWORK_URL]] subscribeNext:^(UIImage *image) {
        expect(image).notTo.beNil();
        expect(image.cacheState).to.equal(JPImageCacheStateNew);

    } error:^(NSError *error) {
        XCTFail(@"%@", error);

    } completed:^{
        done();
    }];
});

Note the use of ^AsyncBlock {. Using simply ^ { would imply a synchronous test.

Here you call the done() function to signal the end of an asynchronous test. I believe Specta uses a 10 second timeout internally.

Using CocoaPods, you can easily add Expecta & Specta:

pod "Expecta", "~> 0.2.3"
pod "Specta", "~> 0.2.1"


回答2:

See this question: https://stackoverflow.com/a/19127547/420594

The XCAsyncTestCase has some extra functionality to allow for asynchronous test cases.

Also, I haven't looked at it in depth yet, but could ReactiveCocoaTests be of some interest to you? On a glance, they appear to be using Expecta.