Unit testing for design specs [iOS]

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!
Take off

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.

Source Code

Here's an iOS project in Swift that details the example in this post.
https://github.com/wingchi/td-grid-layout

Start with the design specs

Get your design specs in images and writing! Don't just assume you'll hammer out the details over a Slack conversation or in person.

How do I know when my design specs are solid?

This one is hard. As you turn more and more masterfully crafted designs into functioning apps you'll get a sense for the types of questions you need to ask to solidify your designs.

Design spec examples

Let's say you've been given design specs for a grid layout that changes based on the number of items in your grid. Something like so:

Basic Design Specs

Basic Wireframe

  • The items in your grid should either be full-width or half-width depending on the number of items.
  • All the grid items will have the same height.
  • The goal is for all the grid items to fit on the screen.

Great! You've got everything to start writing some code. Wrong!

Some points to think about in this scenario:

  • What is the minimum and maximum number of items?
  • Which items should be full or half-width in each scenario from min to max?
  • What if I have more than the maximum number of items?

Extra Design Specs

Better Wireframe

  • The grid will have 1-10 items on it.
  • All 1-10 items should fit on the screen at the same time.
  • The items should be full-width or half-width according to the ten screens in the wireframe.
  • All the grid items should have the same height.

There we go! Much better. Feels good to have solid design specs doesn't it?

Tej Parker nodding

Boilerplate

Now that we've solidified our design specs let's write some tests! Well, actually, let's write our boilerplate code and method signatures so our compiler doesn't throw build errors at us.

In my example, I'm going to setup my view controller with a collection view and make it conform to the UICollectionViewDataSource and UICollectionViewDelegateFlowLayout protocols.

I'm going to put my method signatures and actual sizing logic in a view model.

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 can view all the unit tests in my example repository.

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

Crossing the 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.

Thanks for reading! See you next time in 2 Test 2 Driven.

Comments

comments powered by Disqus