RxSwift simplifies working with asynchronous streams of data in iOS development. While its reactive nature offers immense power, it also introduces complexity in testing. This article dives into unit testing in RxSwift, beginning with an example SimpleViewController
that demonstrates combining streams and binding to the UI.
We’ll dissect the SimpleViewController
, explain its functionality, and explore how to write robust unit tests for the reactive components.
Understanding the SimpleViewController
The SimpleViewController
is a basic example of how RxSwift can simplify UI updates by reacting to changes in user input.
Key Features
- Dynamic Binding of TextFields to a Label:
- The
SimpleViewController
combines the input from two text fields and displays their concatenated values in a label usingRxSwift
‘sObservable.combineLatest
.
- The
- Reactive Programming with RxSwift:
- The
RxCocoa
extensions forUITextField
andUILabel
allow seamless binding between UI elements.
- The
- Memory Management with
DisposeBag
:- RxSwift uses
DisposeBag
to ensure subscriptions are disposed of when the view controller is deallocated, preventing memory leaks.
- RxSwift uses
import UIKit
import RxSwift
import RxCocoa
class SimpleViewController: UIViewController {
private let disposeBag = DisposeBag()
let firstTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter first value"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let secondTextField: UITextField = {
let textField = UITextField()
textField.placeholder = "Enter second value"
textField.borderStyle = .roundedRect
textField.translatesAutoresizingMaskIntoConstraints = false
return textField
}()
let resultLabel: UILabel = {
let label = UILabel()
label.text = ""
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .fill
stackView.distribution = .equalSpacing
stackView.spacing = 16
return stackView
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupBindings()
}
private func setupUI() {
stackView.addArrangedSubview(firstTextField)
stackView.addArrangedSubview(secondTextField)
stackView.addArrangedSubview(resultLabel)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])
}
private func setupBindings() {
Observable
.combineLatest(firstTextField.rx.text.orEmpty,
secondTextField.rx.text.orEmpty) { firstText, secondText in
return "First: \(firstText)\nSecond: \(secondText)"
}
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)
}
}
Code Walkthrough
UI Setup
private func setupUI() {
stackView.addArrangedSubview(firstTextField)
stackView.addArrangedSubview(secondTextField)
stackView.addArrangedSubview(resultLabel)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16)
])
}
The setupUI
function organizes the UI elements using a vertical UIStackView
. Each element—two UITextField
s and a UILabel
—is added to the stack view. Constraints center the stack view and ensure it adjusts to the screen size.
Reactive Binding with RxSwift
The setupBindings
function showcases the core reactive programming approach:
private func setupBindings() {
Observable
.combineLatest(firstTextField.rx.text.orEmpty,
secondTextField.rx.text.orEmpty) { firstText, secondText in
return "First: \(firstText)\nSecond: \(secondText)"
}
.bind(to: resultLabel.rx.text)
.disposed(by: disposeBag)
}
Explanation:
combineLatest
:- Merges two observables (
firstTextField.rx.text.orEmpty
andsecondTextField.rx.text.orEmpty
). - Emits a combined value every time either text field’s text changes.
- Merges two observables (
bind(to:)
:- Directly binds the merged output string to the
text
property of theresultLabel
.
- Directly binds the merged output string to the
disposed(by:)
:- Adds the subscription to the
disposeBag
, ensuring cleanup when theSimpleViewController
is deallocated.
- Adds the subscription to the
Understanding the Unit Tests for SimpleViewController
Unit testing is crucial for ensuring that the components of a view controller behave as expected. In the provided test class, several test cases validate the behavior of SimpleViewController
, focusing on its UI setup and the reactive bindings. Let’s break down each test and its purpose.
import XCTest
import RxSwift
import RxCocoa
import RxTest
import RxBlocking
@testable import RXSwift_Exercise
class SimpleViewControllerTests: XCTestCase {
var viewController: SimpleViewController!
override func setUp() {
super.setUp()
// Initialize the ViewController
viewController = SimpleViewController()
// Loading the View in the ViewController
_ = viewController.view
}
override func tearDown() {
viewController = nil
super.tearDown()
}
func testUISetup() {
// Checking if the elements are in the interface
XCTAssertNotNil(viewController.firstTextField)
XCTAssertNotNil(viewController.secondTextField)
XCTAssertNotNil(viewController.resultLabel)
XCTAssertNotNil(viewController.stackView)
// Check ifthe fields and the label were added to the stackView
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.firstTextField))
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.secondTextField))
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.resultLabel))
}
func testResultLabelUpdatesWithTextFields() {
// Simulate an entry to the the first field
viewController.firstTextField.text = "Hello"
viewController.firstTextField.sendActions(for: .editingChanged)
// Simulate an entry to the the second field
viewController.secondTextField.text = "World"
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Hello\nSecond: World")
// Simulate a new entry in the first field
viewController.firstTextField.text = "Rx"
viewController.firstTextField.sendActions(for: .editingChanged)
// Simulate a new entry in the second field
viewController.secondTextField.text = "Swift"
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Rx\nSecond: Swift")
}
func testEmptyTextFieldsResultLabel() {
// Simulating empty fields
viewController.firstTextField.text = ""
viewController.firstTextField.sendActions(for: .editingChanged)
viewController.secondTextField.text = ""
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: \nSecond: ")
}
func testPartialInputInTextFields() {
// Simulates partial input in text fields
viewController.firstTextField.text = "Partial"
viewController.firstTextField.sendActions(for: .editingChanged)
viewController.secondTextField.text = ""
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Partial\nSecond: ")
}
}
Code Walkthrough
testUISetup
func testUISetup() {
// Checking if the elements are in the interface
XCTAssertNotNil(viewController.firstTextField)
XCTAssertNotNil(viewController.secondTextField)
XCTAssertNotNil(viewController.resultLabel)
XCTAssertNotNil(viewController.stackView)
// Check if the fields and the label were added to the stackView
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.firstTextField))
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.secondTextField))
XCTAssertTrue(viewController.stackView.arrangedSubviews.contains(viewController.resultLabel))
}
Purpose:
- Validates the UI setup to ensure that all the key elements (
firstTextField
,secondTextField
,resultLabel
, andstackView
) are properly instantiated and notnil
. - Confirms that the UI components (
firstTextField
,secondTextField
,resultLabel
) are added to thestackView
as arranged subviews.
Why it matters: This ensures the setupUI
method has correctly constructed the interface and that the required UI elements are accessible during runtime.
testResultLabelUpdatesWithTextFields
func testResultLabelUpdatesWithTextFields() {
// Simulate an entry to the the first field
viewController.firstTextField.text = "Hello"
viewController.firstTextField.sendActions(for: .editingChanged)
// Simulate an entry to the the second field
viewController.secondTextField.text = "World"
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Hello\nSecond: World")
// Simulate a new entry in the first field
viewController.firstTextField.text = "Rx"
viewController.firstTextField.sendActions(for: .editingChanged)
// Simulate a new entry in the second field
viewController.secondTextField.text = "Swift"
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Rx\nSecond: Swift")
}
Purpose:
- Ensures that the
resultLabel
text is updated dynamically when text changes occur in the two text fields (firstTextField
andsecondTextField
).
How it works:
- Mock user input by assigning text to the text fields and simulating the
.editingChanged
event withsendActions(for:)
. - Verify that the
resultLabel
reflects the combined values of the text fields, as expected by the reactive binding.
Why it matters: This test guarantees that the setupBindings
method properly connects the UITextField
inputs to the UILabel
output.
3. testEmptyTextFieldsResultLabel
func testEmptyTextFieldsResultLabel() {
// Simulating empty fields
viewController.firstTextField.text = ""
viewController.firstTextField.sendActions(for: .editingChanged)
viewController.secondTextField.text = ""
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: \nSecond: ")
}
Purpose:
- Validates that the
resultLabel
displays a proper result when both text fields are empty.
How it works:
- Simulate clearing the text in both text fields.
- Verify that the
resultLabel
text matches the expected empty state ("First: \nSecond: "
).
Why it matters: Ensures the application handles edge cases gracefully, such as when no input is provided.
4. testPartialInputInTextFields
func testPartialInputInTextFields() {
// Simulates partial input in text fields
viewController.firstTextField.text = "Partial"
viewController.firstTextField.sendActions(for: .editingChanged)
viewController.secondTextField.text = ""
viewController.secondTextField.sendActions(for: .editingChanged)
// Checking the result in the label
XCTAssertEqual(viewController.resultLabel.text, "First: Partial\nSecond: ")
}
Purpose:
- Tests the behavior when only one text field contains input, and the other is empty.
How it works:
- Assign text to one field while leaving the other empty.
- Verify the
resultLabel
displays the partial input correctly.
Why it matters: This ensures the app doesn’t break or behave unexpectedly when one text field is left blank.
General Notes on the Test Class
- Lifecycle Methods:
setUp
initializes theSimpleViewController
and ensures its view is loaded before each test.tearDown
cleans up the instance after each test, avoiding memory leaks or shared state between tests.
- Reactive Testing with UI Components:
- The tests simulate user interaction by modifying
UITextField
values and triggering their actions (.editingChanged
). - This mimics real user behavior, ensuring the
SimpleViewController
reacts correctly in practice.
- The tests simulate user interaction by modifying
- Validation Strategy:
- The tests systematically validate different scenarios:
- Full input in both fields (
testResultLabelUpdatesWithTextFields
). - Empty input (
testEmptyTextFieldsResultLabel
). - Partial input (
testPartialInputInTextFields
).
- Full input in both fields (
- This ensures the
SimpleViewController
behaves predictably regardless of user input.
- The tests systematically validate different scenarios:
Conclusion
These unit tests validate that SimpleViewController
:
- Sets up its UI elements correctly.
- Properly binds text fields to the result label using reactive programming.
- Handles edge cases like empty or partial input gracefully.
By using techniques like simulating user interaction and verifying output, these tests provide confidence that the reactive logic works as expected in real-world scenarios. Combined with tools like RxTest
or RxBlocking
, these approaches can be extended to test more complex reactive applications.
Check out the repository source code
https://github.com/san0suke/rxswift-tests-example