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.