Intermediate Combine

Apr 13 2021 · Swift 5.3, macOS 11.1, Xcode 12.2

Part 1: Intermediate Combine

06. Testing Combine Operators

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 05. Retrying and Catching Errors Next episode: Part 1 Quiz: Intermediate Combine

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 06. Testing Combine Operators

Testing is an important part of development for all apps. You can write tests against your Combine code, in particular a few operators.

You’ll use a pattern to organize your test logic - the Given-When-Then pattern. Given a condition; when an action is performed; then an expected result occurs. Let’s dive straight into a demo, and see how we can test collect and flatMap

Add a subscriptions property to store subscriptions in, and set it to an empty array in teardown()

var subscriptions = Set<AnyCancellable>()
  
  override func tearDown() {
    subscriptions = []
  }

Now walk through the pattern - given a condition is first. Given the values 0, 1, and 2 in an array and a publisher from that array:


  func test_collect() {
      // Given
      let values = [0, 1, 2]
      let publisher = values.publisher

When an action is performed - when collect operates on the publisher, and sink subscribes to the collect operator

      // When
      publisher
        .collect()
        .sink(receiveValue: {
          // Then
          XCTAssert(
            $0 == values,
            "Result was expected to be \(values) but was \($0)"
          )
        })
        .store(in: &subscriptions)
    }

Within that sink, the “expected result occurs” - XCTAssert ensures that the collected output equals the passed in values.

To run this test, run the playground, and check the console for output. If you were running this test in an Xcode project, you could

Run the test by clicking on the diamond next to test_collect(). The project will build and run the tests, and the diamond will turn to a green checkmark when it passes. To make sure that the test is correct, you can change the assert condition to something you know will fail:

XCTAssert(
  $0 == values + [1],
  "Result was expected to be \(values + [1]) but was \($0)"
)

Run the test again, and you’ll get a failure, along with a message stating what was expected versus what was actually received from the test. We’ll focus on passing tests for the rest of the episode.

As you may know from other Combine courses, the flatMap operator can be used to flatten multiple upstream publishers into a single publisher, and you can optionally specify the max number of publishers to receive and flatten.

To test this, add a test_flatMapWithMax2Publishers method to the test class, and define a typealias for IntPublisher to represent a PassthroughSubject that emits Integer values and Never emits an error. This will represent the start of the “Given” portion of our test cycle.

  func test_flatMapWithMax2Publishers() {
    // Given
    typealias IntPublisher = PassthroughSubject<Int, Never>

Then define 3 IntPublisher subjects.

    let intSubject1 = IntPublisher()
    let intSubject2 = IntPublisher()
    let intSubject3 = IntPublisher()

Make a publisher that is a CurrentValueSubject that emits values of type IntPublisher, and never emits an error, passing in the first subject

    let publisher = CurrentValueSubject<IntPublisher, Never>(intSubject1)

Now define the expected values, and initialize an array for the results.

    let expected = [1, 2, 4]
    var results = [Int]()

Now initialize the combine pipeline. Call flatMap on the publisher, specifying you want at max 2 publishers. Then attach a sink subscriber that appends the value received to the array.

    publisher
      .flatMap(maxPublishers: .max(2)) { $0 }
      .sink(receiveValue: {
        results.append($0)
      })
      .store(in: &subscriptions)

Now for the “When” part of the testing pattern. Send values to the IntPublisher publishers, and interweave sending IntPublishers to the main publisher. Send a completion event when you are done.

    // When
    intSubject1.send(1)
    publisher.send(intSubject2)
    intSubject2.send(2)
    publisher.send(intSubject3)
    intSubject3.send(3)
    intSubject2.send(4)
    publisher.send(completion: .finished)

Since the publisher is a CurrentValueSubject it will replay the current value to any new subscribers.

Note that after the third subject is passed in, a value is sent to the third subject, then to the second subject. Since flatMap only takes in a max of 2 publishers, that third publisher will get ignored.

Now for the “Then” part of the testing pattern. Assert that the results match the expected values, or print an error message if they don’t match.

    // Then
    XCTAssert(
      results == expected,
      "Results expected to be \(expected) but were \(results)"
    )
  }

Run the tests. You’ll see the test pass, since the third publisher is ignored, and the values [1,2,4] are received by the sink.