Unit testing for design specs [iOS]

22 May 2018

You don't need to write UI tests for your designs to ensure your layouts are working as expected. I'll show you an example of how I'd do this.

Ready? Let's go!

TL;DR

Get solid design specs. Distill those design specs into adaptable equations and conditionals. Write tests for that logic. Write code for that logic. Use that logic in your views. Build better products faster.

The idea is that we'll figure out the size for our collection view cell in our view model so that when our view controller calls the UICollectionViewDelegateFlowLayout method collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) we can simply call cellSize(for frame: CGRect, at indexPath: IndexPath) from our view model and pass the index path and collection view frame through to do our calculations.

One test at a time

I live my life one test at time in 10 lines of code or less.

Really though, this blog is going to make a lot of Fast & Furious references, so hold on to your piston rings.

Now we write our tests!

My philosophy here is that each unit test should test a very specific scenario. I'm going to write one unit test for each layout possibility from our design wireframes. To get started I'll write my tests for screens 1 through 5 where all the cells are full-width.

You'll notice that I have some expected height and width calculations in my tests. Those are there so that we can mimic them in our code later on. In the future if that calculation logic changes in our code then our tests will fail, and we will either catch a layout bug or determine that our layout spec has changed.

Making our tests pass

Obviously my tests will fail, so I need to flesh out my cellSize(for frame: CGRect, at indexPath: IndexPath) logic so that these five tests pass. The logic here is pretty straight forward since all these tests expect a full-width cell size.

Same tests, more logic

Now comes the fun part. I'll add my tests for screens 6 through 10.

This is where the layout logic gets a bit more complicated. For example, if we have six items to display then items three through six need to be half-width. Fortunately, we can distill this layout logic into a single function that takes index path as input and outputs a boolean for full-width. I'll name it cellShouldBeFullWidth(at indexPath: IndexPath) -> Bool. All the cells 6 through 10 should be half-width, and we can put in our half-width logic for cells 1 through 5 here.

I'll making this function private in my view model because it will be tested through the tests for my cellSize method.

Here's the final form of my code for GridViewModel.

This should be all you need to make your tests pass, and all your design spec logic should be covered.

Finish line

At this point getting across the finish line is easy! Simply, replace the filler CGSize in our collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) in our view controller delegate method with a call to gridViewModel.cellSize(for: collectionView.bounds, at: indexPath).

The Moral of the Story

Obviously, this is a very specific example of layout logic, but my point is that even with design specs that aren't "business logic" we can implement unit tests. A lot of design specs can be distilled into a handful of equations and conditionals. By writing unit tests for these equations and conditionals we can ensure a more robust product from start to finish.