All the tricks and techniques I’ve learned to improve the speed and quality of writing reactive unit tests with RxSwift.

Starting work on a next-gen smart home app for one of the biggest players in home automation systems, we knew that our code would eventually have thousands of use case and presenter functions that will need to be tested. Since every aspect of our app was implemented using a reactive paradigm with the help of RxSwift, we had to take a deep dive into RxSwift testing. Through trial and error, we’ve come to find a clean way of testing everything without sacrificing too much of our time.

If you are using or starting to use RxSwift, knowing how to test it properly will help you:

  • Learn how to write clean Rx chains
  • Deepen your understanding of RxSwift
  • Increase software quality and sleep better

This post is constructed from the ground up and separated into three parts. To begin constructing our tests, we first have to lay some foundation, then build the load-bearing walls that will hold everything together, and finally, fill the interior with the design of our choice. Our foundation will cover testing with RxBlocking and RxTest. Lenses and Sourcery will act as walls that hold everything together and interior design will be taken care of by Quick/Nimble. Each part will be available on GitHub as a separate branch where you can look at the finished implementations as you follow along.

  1. Introduction to reactive LightSwitch app [GitHub]
    Writing state-dependent unit tests for business logic
     • XCTest and RxBlocking
  2. Writing event-dependent unit tests for business logic [Part 2]
     • XCTest and RxTest
  3. Automating writing mocks and lenses [Part 3]
    Sourcery
    Making unit tests more readable
    Quick/Nimble

Introduction to reactive LightSwitch app

LightSwitch is a super simplified light control app built using the reactive framework RxSwift. It’s built using a clean architecture pattern. For those new to clean architecture here is a short overview:

  • The app is built in layers.
  • The presentation layer is your MVC, MVVM, MVP, etc. This is where you set up how you present the data that you get from the Business layer.
  • The business layer is where you put your business logic, like use cases.
  • The data layer contains your repositories.
  • The arrow represents the dependency flow. Nothing from inner layers should know anything about outer layers.
  • Dependency flow is implemented using dependency injection and dependency inversion.
class LightsUseCase: LightsUseCaseProtocol {

	private let lightsRepository: LightsRepositoryProtocol

	init(lightsRepository: LightsRepositoryProtocol) {
    	    self.lightsRepository = lightsRepository
}
...
}
  • In simple terms, your classes only have access to the protocols of lower layers (dependency inversion), and real implementation is injected through the initializer, property, or method (dependency injection).
    Why is this important? As you will see in a moment, this makes it very easy to mock your protocols and insert them into your classes which gives you full control over your test environment.

In our LightRepository, we abstracted the real implementation of IoT data source with our mocked one. 

It’s not important to understand how we did it, but you can look at the code here if you are curious.

What are we testing?

If you wish to turn on a light in your home, would you manually hardwire stuff until the light turns on? Of course not, a professional electrician gave you an abstraction to do this – a switch. A switch that you press not caring what happens next as long as it does its job. The same reasoning applies in code, but we want to verify that business logic is doing the right thing.

We’re placing our business logic in use cases. Each use case is a class that has functions and properties to implement a specific business rule, in this case, it is the API for controlling lights from the app.

Before diving into the rules, let’s quickly glance at the data structures in play here. To model a single light device in a system, we’ll define an entity model for it that contains an identifier and a name. In a real-world scenario, you can imagine dozens of other properties, but we’re keeping it simple.

struct LightModel {
   let id: Int
   let name: String
}

It’s common to use structs in Swift to hold data. Immutability works in our favor by preventing unwanted modifications to our data. This is something we generally want, but it forces us to design our approach for mutating the app state. We’ll come back to that later.

The next thing we’re modeling is the state of the light device. For that purpose, we’re defining the struct entity model with an id of the light, and it’s current state (on or off) represented as an enum. Again, in a real-world scenario, there are a dozen more properties.

struct LightStateModel {
    let id: Int
    let state: LightStateType // enum with cases .on and .off
}

The final structure we’re dealing with on the UI layer is LightWithState where each light entity is bound to its corresponding state object.

struct LightWithState {
   let light: LightModel
   let lightState: LightStateModel
}

You’re probably wondering why we’ve separated LightWithState into LightModel and LightStateModel.

The reason is simple. When you send a command to turn on a light, you only want to receive the properties that changed – keeping the UI state in sync with the data source. There’s no need to get all of the light device information if it didn’t change. Alternatively, one could design a protocol with a single entity, where only modified properties are pushed to the client, but that’s a different discussion.

By separating the entity from its state we’re minimizing data redundancy in data transfers.

Let’s look at our LightsUseCaseProtocol, and see that we can:

  • get a list of lights (queryLightsWithState), 
  • check if all lights are off (areAllLightsOff), 
  • turn light off/on (toggleLight).
protocol LightsUseCaseProtocol {
   func queryLightsWithState() -> Observable<[LightWithState]>
   func areAllLightsOff() -> Observable
   func toggleLight(withId id: Int) -> Completable
}

Now that structures and behaviors are set, it’s time to dive into the testing part.

Testing 101

When testing something you shouldn’t know its implementation details, it leads to testing bias where you are writing tests that conform to your already written code. Or better yet, tests should be written before the implementation, but we are only human. A word that you will hear often is “expectation”. You expect that specific input will produce a specific output.

That is why I won’t tell you how LightsUseCase is implemented, but if you want to know you can take a peek in the project files on GitHub.

On the topic of implementation:

You should always mock one level lower than what you are testing

  • Testing ViewController → mock the Presenter
  • Testing Presenter → mock the UseCase
  • Testing UseCase → mock the Repository (our plan)

The lights repository that will be mocked consists of three functions.

protocol LightsRepositoryProtocol {
   func queryAllLights() -> Observable<[LightModel]>
   func queryAllLightStates() -> Observable<[LightStateModel]>
   func toggleLight(withId id: Int) -> Completable
}

You can immediately notice the difference from the use case API, where our entity/state separation comes in hand.

  • queryAllLights — we are getting light entities in the system,
  • queryAllLightStates — we are getting light state entities in the system,
  • toggleLight — we can use this function to switch the light on/off.

Each LightModel has to be connected with id to its LightStateModel, and vice versa. 

Writing state-dependent unit tests for business logic using XCTest and RxBlocking

Before writing tests we need to have our mocks ready. Let’s start by creating a mock of our dependency – LightRepositoryProtocolMock. We do this by mocking the return value of each function with a public var. A mocked function will then return whatever we set it to return in our tests.

class LightsRepositoryProtocolMock: LightsRepositoryProtocol {

   // MARK: - queryAllLights

   var queryAllLightsReturnValue: Observable<[LightModel]>!
   func queryAllLights() -> Observable<[LightModel]> {
       return queryAllLightsReturnValue
   }

   // MARK: - queryAllLightStates

   var queryAllLightStatesReturnValue: Observable<[LightStateModel]>!
   func queryAllLightStates() -> Observable<[LightStateModel]> {
       return queryAllLightStatesReturnValue
   }

   // MARK: - toggleLight

   var toggleLightCallsCount = 0
   var toggleLightReceivedInvocations: [Int] = []
   var toggleLightReturnValue: Completable!
   func toggleLight(withId id: Int) -> Completable {
       toggleLightCallsCount += 1
       toggleLightReceivedInvocations.append(id)
       return toggleLightReturnValue
   }

}

Note: This may seem like a chore to do for protocols with a lot of functions. We will fix that later in Part 3 with Sourcery.

You probably noticed that we have a couple more vars for toggleLight. We will use them in our test to check if our function was called correctly.

After we mocked our repository, we can start setting up our first test.

In LightsUseCaseStatesTests we set up our SUT (system under test) environment. Here we see the power of using dependency inversion, replacing the real implementation with our mocked one is super easy – we’re just going to inject a mocked version as our repository.

class LightsUseCaseStatesTests: XCTestCase {

   private var lightsUseCase: LightsUseCaseProtocol!
   private var lightsRepository: LightsRepositoryProtocolMock!
   private var disposeBag: DisposeBag!

   override func setUpWithError() throws {
       try super.setUpWithError()
       lightsRepository = LightsRepositoryProtocolMock()
       lightsUseCase = LightsUseCase(
                           lightsRepository: lightsRepository)
       disposeBag = DisposeBag()
   }
...
}

Testing Observable sequences

Our first test will be testing queryLightsWithState.

func queryLightsWithState() -> Observable<[LightWithState]>

We expect that queryLightsWithState returns an array with three LightWithState items when our data source contains three LightModel and three LightStateModel with matching identifiers.

func testQueryLightsWithState_With_Three_Lights() throws {
   // Arrange
   let lightModel1 = LightModel(id: 1, name: "Light 1")
   let lightModel2 = LightModel(id: 2, name: "Light 2")
   let lightModel3 = LightModel(id: 3, name: "Light 3")

   let lightStateModel1 = LightStateModel(id: 1, state: .on)
   let lightStateModel2 = LightStateModel(id: 2, state: .off)
   let lightStateModel3 = LightStateModel(id: 3, state: .off)

   lightsRepository
      .queryAllLightsReturnValue = .just([
          lightModel1,
          lightModel2,
          lightModel3])
   lightsRepository
      .queryAllLightStatesReturnValue = .just([
          lightStateModel1,
          lightStateModel2,
          lightStateModel3])

   let lightsWithStateCount = lightsUseCase
      .queryLightsWithState()
      .map { $0.count }

   // Act + Assert
   XCTAssertEqual(try lightsWithStateCount.toBlocking().first(), 3)
}

.toBlocking() transforms an Observable into BlockingObservable. Blocking observables can use blocking operators, and one of them is .first() that we just used. .first() will block the current thread until the sequence produces the first element. When it does, we expect it to equal 3.

This looks manageable, but imagine if our test required 10 or more mocked entities to be created. It would quickly become very cumbersome and repetitive.

Concise stubbing of entities

We’ll create a handy protocol to fix this  —  StubProtocol

protocol StubProtocol {
   static func stub(withId id: Int) -> Self
}

It will help us define a generic stub for each of our structs.

When you are choosing values to put into your generic stub, think of the most basic implementation.

extension LightModel: StubProtocol {

   static func stub(withId id: Int = 1) -> Self {
       return LightModel(
           id: id,
           name: "Light \(id)")
   }

}

What you define here will be a starting point of each of your tests, so don’t burden it with every functionality possible. At the beginning of each test, you will be able to modify properties depending on what you are testing.

Then we’ll add an extension to an array, to enable stubbing multiple entities:

extension Array where Element: StubProtocol {

   static func stub(withCount count: Int) -> Array {
       return (1...count).map {
           .stub(withId: $0)
       }
   }

}

This will allow us to create an N amount of stubs with only one line of code.

Now we can replace the creation of our test models with these two lines:

...
let lightModels = [LightModel].stub(withCount: 3)
let lightStateModels = [LightStateModel].stub(withCount: 3)
...

How to test areAllLightsOff:

func areAllLightsOff() -> Observable<Bool> 

It starts similarly, we create some generic stubs that we know have a default state .on and test if areAllLightsOff returns false.

func testAreAllLightsOff_If_All_Are_On() throws {
   // Arrange   
   let lightModels = [LightModel].stub(withCount: 3)
   let lightStateModels = [LightStateModel].stub(withCount: 3)
   lightsRepository
       .queryAllLightsReturnValue = .just(lightModels)
   lightsRepository
       .queryAllLightStatesReturnValue = .just(lightStateModels)
   let areAllOff = lightsUseCase.areAllLightsOff()

   // Act + Assert
   XCTAssertFalse(try areAllOff.toBlocking().first()!)
}

Okay, that’s simple. But what if we want to test the opposite — when the state is .off, and we absolutely want that. We already defined our stub to have a default .on state and since structs are immutable, we have no way of changing it afterward. So let’s just add another handy extension :).

Modify extension:

extension LightStateModel {

   func modify(
       id: Int? = nil,
       state: LightStateType? = nil
   ) -> Self {
       return LightStateModel(
           id: id ?? self.id,
           state: state ?? self.state)
   }

}

What this does is allows us to easily change properties of a struct by recreating it with our new properties.

Now we can test the opposite:

func testAreAllLightsOff_If_All_Are_Off() throws {
   // Arrange
   let lightModels = [LightModel].stub(withCount: 3)
   let lightStateModels = [LightStateModel]
       .stub(withCount: 3)
       .map { $0.modify(state: .off) }

   lightsRepository
       .queryAllLightsReturnValue = .just(lightModels)
   lightsRepository
       .queryAllLightStatesReturnValue = .just(lightStateModels)
  
   let areAllOff = lightsUseCase.areAllLightsOff()
  
   // Act + Assert
   XCTAssertTrue(try areAllOff.toBlocking().first()!)
}

Note: Later in Part 3 we will see how we can evolve our modify function into something called a Lens. Great stuff, stay tuned.

Testing Completable sequences

The last function we need to test is toggleLight(withId id: Int).

func toggleLight(withId id: Int) -> Completable

Testing a trait like Completable is a little different. We can’t use .first() since there is no element to be returned.

Instead, we are going to introduce a new blocking operator — materialize().

Materialize blocks the current thread until sequence terminates and returns an enum with one of two cases:

case completed(elements: [T])
case failed(elements: [T], error: Error)

Materialize is included in RxBlocking as an extension to the blocking observable and should be used only in testing. There is also Materialize that is included in RxSwift that can be used outside of your tests.

Writing a test with materialize would look something like this:

func testToggleLight_Is_Called() throws {
   // Arrange
   lightsRepository.toggleLightReturnValue = .empty()

   // Act
   let toggleLight = lightsUseCase
       .toggleLight(withId: 2)
       .toBlocking()
       .materialize()

   // Assert
   let timesCalled = lightsRepository.toggleLightCallsCount
   let parameterPassed = lightsRepository
                             .toggleLightReceivedInvocations
                             .first!
   guard
       case .completed(_) = toggleLight
   else {
       XCTFail()
       return
   }
   XCTAssertEqual(timesCalled, 1)
   XCTAssertEqual(parameterPassed, 2)
}

We are testing three things:

  • Did our completable finish successfully?
  • Did our use case function call an appropriate function on the repository level?
  • If it did, did it pass correct parameters to that function?

Recap

Here you can find a finished implementation of what we have done so far.

We learned how to structure our app using clean architecture, how to create our mocks and stubs, and how to use the RxBlocking framework to test our Observables.

If you have any questions regarding writing unit tests with RxBlocking, feel free to leave them in the comments.

Part 2 will include writing event-dependent unit tests with XCTest and RxTest.

0 comments