79728109

Date: 2025-08-07 05:41:28
Score: 1
Natty:
Report link

There are a quite a few issues here:

  1. Your code can exit before it hits any of the #expect tests.

    You set up the sink and then emit the fakeMessages and then immediately exit. You have no assurances that it will even reach your #expect tests within the sink at all. You need to do something to make sure the test doesn’t finish before it has consumed the published values.

    Fortunately, async-await offers a simple solution. E.g., you might take the sut.$messages publisher, and then await its values. So either:

    • Use a for await-in loop:

      for await value in values in sut.$messages.values {
          …
      }
      
    • Or use an iterator:

      var iterator = sut.$messages.values.makeAsyncIterator()
      
      let value1 = await iterator.next()
      …
      let value2 = await iterator.next()
      …
      
      // etc
      

    Either way, this is how you can await a value emitted from the values asynchronous sequence associated with the sut.$messages publisher, thereby assuring that the test will not finish before you process the values.

  2. Having modified this to make sure your test does not finish prematurely, the next question is how do you have it timeout if your stubbed service fails to emit the values. You can do this a number of ways, but I tend to use a task group, with one task for the tests and another for a timeout operation. E.g.:

    try await withThrowingTaskGroup(of: Void.self) { group in
        group.addTask {
            let value1 = await iterator.next()
            #expect(value1 == [])
            …
        }
        group.addTask {
            try await Task.sleep(for: .seconds(1))
            throw ChatScreenViewModelTestsError.timedOut
        }
        try await group.next()
        group.cancelAll()
    }
    

    Or a more complete example:

    @Test
        func onAppearShouldReturnInitialMessagesAndStartPolling() async throws {
            let mockMessageProvider = MockMessageProvider()
    
            let sut = createSUT(messageProvider: mockMessageProvider)
            sut.onAppear()
    
            var iterator = sut.$messages
                .buffer(size: 10, prefetch: .keepFull, whenFull: .dropOldest)
                .values
                .makeAsyncIterator()
    
            Task {
                await mockMessageProvider.emit(.success(fakeMessages))     // Emit initial messages
                await mockMessageProvider.emit(.success(moreFakeMessages)) // Emit more messages
            }
    
            try await withThrowingTaskGroup(of: Void.self) { group in
                group.addTask {
                    let value1 = await iterator.next()
                    #expect(value1 == [])
                    let value2 = await iterator.next()
                    #expect(value2 == fakeMessages)
                    let value3 = await iterator.next()
                    #expect(value3 == moreFakeMessages)
                }
                group.addTask {
                    try await Task.sleep(for: .seconds(1))
                    throw ChatScreenViewModelTestsError.timedOut
                }
                try await group.next()
                group.cancelAll()
            }
        }
    }
    
  3. Your code assumes that you will see two published values:

    #expect(sut.messages[0].count == 0)
    #expect(sut.messages[1].count > 0)
    

    This is not a valid assumption. A Published.Publisher does not handle back pressure. If the async sequence published values faster than they could be consumed, your property will drop values (unless you buffer your publisher, like I have in my example in point 2). This might not be a problem in an app that polls infrequently, but especially in tests where you mock the publishing of values without delay, you can easily end up dropping values.

  4. Your sut.onAppear starts an asynchronous Task {…}. But you don’t wait for this and immediately emit on the mocked service, MockMessageProvider. This is a race. You have no assurances that poll has been called before you emit values. If not, because emit uses nil-chaining of continuation?.yield(value), that means that emit might end up doing nothing, as there might not be any continuation to which values can be yielded yet.

    Personally, I would I would decouple the asynchronous sequence from the polling logic. E.g., I would retire AsyncStream and reach for an AsyncChannel from the Swift Async Algorithms, which can be instantiated when the message provider is instantiated. And then poll would not be an asynchronous sequence itself, but rather a routine that starts polling your remote service:

    protocol MessageProviderUseCase: Sendable {
        var channel: AsyncChannel<MessagePollResult> { get }
        func startPolling(interval: TimeInterval)
    }
    
    private final class MockMessageProvider: MessageProviderUseCase {
        let channel = AsyncChannel<MessagePollResult>()
    
        func startPolling(interval: TimeInterval) {
            // This is intentionally blank … 
            //
            // In the actual message provider, the `startPolling` would periodically
            // fetch data and then `emit` its results.
        }
    
        func emit(_ value: MessagePollResult) async {
            await channel.send(value)
        }
    }
    

    Because the channel is created when the message provider is created, it doesn't matter the order that startPolling and emit are called in our mock implementation.


Some other observations:

Reasons:
  • Blacklisted phrase (1): how do you
  • RegEx Blacklisted phrase (2.5): do you have it
  • Contains signature (1):
  • Long answer (-1):
  • Has code block (-0.5):
  • High reputation (-2):
Posted by: Rob