Integrate Combine Into an App

Aug 5 2021 · Swift 5.4, macOS 11.3, Xcode 12.5

Part 2: Integrate with Core Data & Unit Test

09. Test with Given-When-Then

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: 08. Use @FetchRequest to Get Core Data Entries Next episode: Part 2 Quiz: Integrate Combine into an App

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: 09. Test with Given-When-Then

Testing is an important part of development for all apps. You can even write tests against your Combine code. 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

In our app, there are two types of publishers we can test:

the @Published properties in the view model,

and the custom publishers we made for fetching jokes and translations. Let’s take a look at how to test those in a demo.

If you recall from the earlier episodes, we decorated several properties in the view model with @Published property wrappers. This means they are publishers to which we can subscribe, so we should include some tests to make sure those work correctly.

One of the viewModel properties was the backgroundColor of the joke card, which changes as the card is moved on screen (because it is either liked or not). In the test_backgroundColorFor50TranslationPercentIsGreen() method, define our “given” state

func test_backgroundColorFor50TranslationPercentIsGreen() {
	// Given
	let viewModel = self.viewModel()
	let translationPercent = 0.5
	let expected = Color("Green")
	var result: Color = .clear
	
After that, subscribe to the `$backgroundColor` publisher, using a `sink` to capture the value, and store the subscription.
	viewModel.$backgroundColor
	  .sink(receiveValue: {
	    result = $0
	  })
	  .store(in: &subscriptions)

For the “when” of our test, we want to check the value when the updateBackgroundColorForTranslation method is called with the given translationPercent.

	// When
	viewModel.updateBackgroundColorForTranslation(translationPercent)

Finally for the “then” of our test, compare the result to the expected value, and include an error message in case they don’t match.

	// Then
	XCTAssert(result == expected, "Color expected to be \(expected) but was \(result)")
}

Run the test and you’ll see that it succeeds.

Next let’s look at the success and error case for fetching a Joke, which will use expectations. The test code for translating the jokes looks similar.

In the test_fetchJokeSucceeds() method, define the “given” part of the test, which in this case will include an Expectation. This is because we’re fetching this data asynchronously, so we may not get the result immediately.

func test_fetchJokeSucceeds() {
  // Given
  let viewModel = self.viewModel()
  let expectation = self.expectation(description: #function)
  let expected = self.testJoke.value
  var result: Joke!

Then setup the Combine pipeline. After getting the $joke publisher, we need to drop the first emitted value - because we want to compare our expected value to the fetched joke, which will actually be the second item emitted (since the publisher already has a value ready to emit from the setup done in the viewModel function near the top of the file). Then use a sink to set the result and more importantly, fulfill the expectation.

  viewModel.$joke
    .dropFirst()
    .sink(receiveValue: {
      result = $0
      expectation.fulfill()
    })
    .store(in: &subscriptions)

This should be tested when viewModel.fetchJoke() is called, so that is the “when” part of the test.

  // When
  viewModel.fetchJoke()

Finally for the “Then” part of the test, wait 1 second for the expectation to return, and then use XCTAssert to compare the values.

  // Then
  waitForExpectations(timeout: 1, handler: nil)
  XCTAssert(result == expected, "Joke expected to be \(expected) but was \(String(describing: result))")
}

The test for the error case can be performed in a similar fashion, except that the expected value is now Joke.error instead of self.testJoke.value.

Again, run the test, and you’ll see that it succeeds.