Testing RxSwift Code — How I Learned to Stop Worrying and Love Writing Unit Tests — Part 3

Part 3 of a three-part blog post on writing RxSwift unit tests. Boost your unit testing productivity with Sourcery, Lenses, Prisms, and Quick/Nimble.

  1. Introduction to reactive LightSwitch app [Part 1]
    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 [Github]
    Sourcery
    Making unit tests more readable
    Quick/Nimble

Sourcery

Sourcery is a code generation tool built on top of SourceKit. It’s extremely powerful when used properly and can save you countless hours of writing the same code you’ve written a thousand times before.

Want to automatically create protocols for classes/structs? Done.
Implement hashable/equatable? No problem.
Create mocks for unit testing? I thought you would never ask.

It does all of that by utilizing a template language called Stencil. There are many templates already available online, but if you need something more personal for your projects, you can always create your own.

We’ll do a brief introduction on how to set it up for you to get an idea, and for more details on configuration please visit https://github.com/krzysztofzablocki/Sourcery. The main area we want to focus on is how it can improve our unit tests once it’s implemented. For that, we’ll use the already defined AutoMockable.stencil template.

Sourcery setup:

  1. Create a configuration file “.sourcery.yml” and place it in your project. This tells Sourcery where to look for templates, where to save generated code, what source code to parse when generating code and much more.
  2. Copy (or write) the stencil template file to a folder defined in the configuration file.
  3. Let Sourcery know what it needs to generate.

In the template file, you can define whether you’ll do it with comments

// sourcery: AutoMockable 
protocol SomeProtocol {...}

or by implementing a custom empty protocol.

protocol SomeProtocol: AutoMockable {...}

Or even both, the choice is yours. You can even annotate extensions which makes it easy to set all Sourcery annotations in one file to keep them all in one place. Just annotate an empty protocol extension and Sourcery will generate a mock for that protocol.

// sourcery: AutoMockable 
extension SomeProtocol {}
// sourcery: AutoMockable
extension OtherProtocol {}
...

4. Run Sourcery manually through the terminal:
Run Pods/Sourcery/bin/sourcery file, append “ — watch" if you want an automatic update of generated files.
Or you can set it up that Sourcery is run on each project build:
Add a new build phase with the line "${PODS_ROOT}/Sourcery/bin/sourcery".

5. Enjoy your generated code.

Let’s see how a Sourcery mocked repository looks like.

You might notice that it’s very similar to what we did manually in Part 1. But this time Sourcery did all the work for us. It even added some extra features we didn’t have before like an option to set the return value as a closure. Now we can delete our old lame hand-made mock and replace it with this computer-generated mock of the future.

Mocking protocols with Sourcery is one of the best productivity boosts you can give yourself.

There is one more thing Sourcery is really good at doing, and that is creating Lenses for our structs.

Lenses

If you dabbled in functional programming before, you might already be familiar with lenses.

Lenses are a broad subject and people have talked about them in detail.

But again, we are only interested in its application in unit testing.
Let’s first explain how and why we want to use them and then we’ll explain how they are created.

Imagine that we have nested structs.

struct LightStateModel { 
let mode: Mode
...
}
struct Mode {
let type: ModeType
...
}
struct ModeType {
let name: String
...
}

We then create an instance of the “root” struct which in our case is LightStateModel:

let modeType = ModeType(name: "Blinking", ...) 
let mode = Mode(type: modeType, ...)
let lightStateModel = LightStateModel(mode: mode, ...)

How would you change the modeType in your lightStateModel object while keeping all the other attributes the same?

If you used our old modify functions from Part 1, you could write:

lightStateModel.modify( 
mode: lightStateModel.mode.modify(
type: lightStateModel.mode.type.modify(
name: "Color change")))

It doesn’t look very readable and we have to manually create modify functions for each struct.

With lenses you could use lens composition, which would allow you to write:

// Create a Lens composed of different Lenses 
let lightStateModelModeTypeNameLens = LightStateModel.stateLens *
Mode.typeLens *
ModeType.nameLens
// Set mode of lightStateModel lightStateModelModeTypeNameLens.set("Color change", lightStateModel)

You can then reuse lightStateModelModeTypeNameLens whenever you want to change the modeType name of any lightStateModel.

This means that any property in a struct, no matter how deep in the hierarchy can be changed with one line of code.

How does a Lens look like?

struct Lens<Whole, Part> { 
let get: (Whole) -> Part
let set: (Part, Whole) -> Whole
static func * <Subpart>(
lhs: Lens<Whole, Part>,
rhs: Lens<Part, Subpart>
) -> Lens<Whole, Subpart> {
return Lens<Whole, Subpart>(
get: { whole in rhs.get(lhs.get(whole)) },
set: { subpart, whole in
lhs.set(rhs.set(subpart, lhs.get(whole)), whole)
})
}
}

Each lens is both a setter and a getter for a specific property in a struct. We can either get that property or change it and receive a new object with that property modified. We also created a static func * which acts as a composition operator to combine different lenses into just one lens. As we saw a moment ago, composition helps us isolate a specific property in the struct hierarchy by creating a "shortcut" lens to it.

Let’s see how lenses look like when implemented on our LightModel in our LightSwitch project.

extension LightModel { 

static let idLens = Lens<LightModel, Int>(
get: { $0.id },
set: { id, lightModel in
LightModel(
id: id,
name: lightModel.name
)
}
)
static let nameLens = Lens<LightModel, String>(
get: { $0.name },
set: { name, lightModel in
LightModel(
id: lightModel.id,
name: name
)
}
)
}
  • Each property gets its own lens.
  • Static property idLens can be used to get and set id property on each LightModel object.
  • Static property nameLens can be used to get and set nameLens property on each LightModel object.
  • …and so on with more properties.

As you can see that is a lot of work even for a struct with just two properties. Luckily we can just tell Sourcery to do it for us.
You can use the stencil template for lenses from the official Github repository, but I found that it didn’t have enough versatility in dealing with more complex compositions which we will explain in a moment. That’s why I created my own AutoLenses.stencil which is tried and tested in a real production environment and has helped generate thousands of lenses for varying struct hierarchies.

How to use them in tests?

LightStateModel.stateLens.set(.off, lightStateModel)

Or you can throw them in map and change any property in all the structs with just one line:

let lightStateModels = [LightStateModel] 
.stub(withCount: 3)
.map { LightStateModel.stateLens.set(.off, $0) }

We are creating an array of three stub models with stub() function we defined in Part 1, then mapping that array and using a stateLens on each element.

Lens composition with optional elements

Let’s for a second go back to our imaginary nested structs.

struct LightStateModel { 
let mode: Mode
...
}
struct Mode {
let type: ModeType
...
}
struct ModeType {
let name: String
...
}

If we change our Mode in LightStateModel to be an optional Mode?, the composition that we defined previously won't work.

struct LightStateModel { 
let mode: Mode?
...
}
struct Mode {
let type: ModeType
...
}
struct ModeType {
let name: String
...
}

Fixing this will introduce new concepts like affines and prisms. I will do a short explanation and leave you with a great link where you can read more about those two.

The problem is in the way we defined composition, both left and right Part need to match:

Lens<Whole, Part> * Lens<Part, Subpart> = Lens<Whole, Subpart>

So it’s not surprising that introducing an optional would break it:

Lens<Whole, Part?> * Lens<Part, Subpart> = /error/

The way we fix it is by introducing two new concepts — Prism and Affine.

Prism — A lens but for enums, notice the optional getter. Since an enum can only be in one case, trying to get a specific case can result in nil.

struct Prism<Whole, Part> {     let tryGet: (Whole) -> Part? 
let set: (Part) -> Whole
}

Affine — A failable lens that can be created from both normal Lenses and Prisms. Here we made tryGet optional because of Prisms, and trySet optional to help us fix our composition problem.

struct Affine<Whole, Part> {     let tryGet: (Whole) -> Part? 
let trySet: (Part, Whole) -> Whole?
}

The trick now is to insert a prism made for Optional type in the middle.
(Prism is a lens for enums, and an optional is just an enum):

Lens<Whole, Part?> * Prism<Part?, Part> * Lens<Part, Subpart> = /error/

Parts are starting to match, but now we can’t compose combinations of Lenses and Prisms. That’s why we will transform all of them into affines.

Affine<Whole, Part?> * Affine<Part?, Part> * Affine<Part, Subpart> = Affine<Whole, Subpart>

Everything will match and we have the final Affine that we can use in the same way we used Lenses. The difference is that instead of get and set, you now have tryGet and trySet. Don't worry if this is too much to figure out, the great thing is that you don't need to learn it to use it. All of this is done in the background during the generation of Lenses. If Sourcery figures out that one of your properties is optional, it will automatically convert everything for you.

In our stencil, the whole process is contained in one extension:

static func * <Subpart>(
lhs: Lens<Whole, Part?>,
rhs: Lens<Part, Subpart>
) -> Affine<Whole, Subpart> {
return lhs.affine * Part?.prism.affine * rhs.affine
}

Making unit tests more readable with Quick/Nimble + RxNimble

But there was still some room for improvement. So far we have used XCTest with all our tests, and we ended up with test function names like:

testQueryLightsWithState_With_Three_Lights() testAreAllLightsOff_If_All_Are_On() 
testToggleLight_Is_Called() testQueryLightsWithState_When_Model_Before_State()

For the purpose of keeping the names short, you are sacrificing clarity and inserting ambiguity. We wanted to change that by letting our developers write sentence-like explanations while keeping the tests looking as clean as possible.

If that is something you would like to have in your tests, the best tool for the job is a popular combination of libraries — Quick/Nimble. If you are using RxTest/RxBlocking, you will also like RxNimble.

Less talk, more doing.
Our LightSwitch unit tests need a little makeover, let's refactor one of our tests that uses RxBlocking from Part 1 to see what we improved.

describe("areAllLightsOff") { 
it("returns false if all lights are ON") {
let lightModels = [LightModel].stub(withCount: 3)
let lightStateModels = [LightStateModel].stub(withCount: 3)
self.lightsRepository
.queryAllLightsReturnValue = .just(lightModels)
self.lightsRepository
.queryAllLightStatesReturnValue = .just(lightStateModels)
let areAllOff = self.lightsUseCase.areAllLightsOff()

expect(areAllOff).first.to(beFalse())
}
}
  • Each function has its own block, eliminating the need for comments and lines separating different functions.
  • We can insert much more information about what we are testing in the block without making it unreadable.
  • Our assertions can be read from left to right as a sentence.

Let’s also refactor one of our tests that uses RxTest from Part 2.

describe("queryLightsWithState") {     it("will have expected result if models come before states") { 
self.lightsRepository
.queryAllLightsReturnValue = self.lightModelSubject
self.lightsRepository
.queryAllLightStatesReturnValue =
self.lightStateModelSubject
let lightModels = [LightModel].stub(withCount: 3)
let lightStateModels = [LightStateModel].stub(withCount: 3)
let numberOfLightsWithStates = self.lightsUseCase
.queryLightsWithState()
.map { $0.count }
self.scheduler
.createColdObservable([.next(10, lightModels)])
.bind(to: self.lightModelSubject)
.disposed(by: self.disposeBag)
self.scheduler
.createColdObservable([.next(20, lightStateModels)])
.bind(to: self.lightStateModelSubject)
.disposed(by: self.disposeBag)
expect(numberOfLightsWithStates)
.events(
scheduler: self.scheduler,
disposeBag: self.disposeBag)
.to(equal([.next(20, 3)]))

}
}

Here we replaced the last three lines of code with one expectation!

Recap

We learned how Sourcery can do a lot of the heavy lifting for us, what lenses are, and how to use them in your tests. And finally, making our tests much nicer to look at with Quick/Nimble.

I hope you enjoyed reading and found something valuable to use in your tests. As always, if you have any questions feel free to leave them in the comments.

Originally published at https://five.agency on July 24, 2020.

Five is a mobile design and development agency with offices in Croatia and NYC.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store