In this CMD+U Conference live coding demo, Michael May shows techniques for starting to test code which has no existing tests.
We’ve seen some amazing talks today about the different types of testing that you can do. I want to take a slightly different stance and talk about how you apply those techniques when you have a pre-existing code base which has no test.
For the last few years, I’ve worked on projects exactly like that every time I’ve joined a new company. There has been an existing code base and the last four projects had no tests when I have arrived.
This is a project that my friend Emily Toop wrote: AllergyTracker. You record different allergens that you might encounter during your day–pollen, dust, alcohol, cats, etc.–and then you record the allergies that you experience later on. The hope is that you can find patterns between them.
This project is about 10,000 lines of Objective-C code and it has no tests. We’re going to fix that.
I like to start with the compiler settings. Some of you might think: “this is a talk about testing!” I’m going to try and persuade you that the compiler warnings are a test, because they go red when they fail, and they assert certain things that you assume about your code. I would say that is a test.
Let’s compile the app for the first time (I’m not going to run the app. We’re going to look at how you get into the code itself). We have one warning, easily fixed. Now we’re 60 seconds in, and we’ve already made the code slightly better than it was before. But we can do better.
I’ve turned on a few compiler warnings, documentation comments, four character literals, and hidden variables. Let’s turn implicit signedness conversion on, and we have 25 errors.
The first one is fairly obvious: it’s telling us that we should have an unsigned int and we have an integer. If we look at the documentation for MakeRange, you see it should be an unsigned integer.
Let’s have a quick look in the code: we are passing in two everywhere at the moment. That’s good. We know we’re not passing any negative numbers at the moment, so we can confidently make that change.
Notice I’m trying not to change the code itself, because we are trying to get to the point where we are writing tests. We’re not changing the code at this point; we’re testing it. But we need to make that small change, and we are down to 24 errors.
I’ll publish a version where I’ve cleaned up this code even further, but for now we are going to turn that back off. Hopefully I’ve encouraged you to think that maybe the compiler is the first place that you might go in testing an app that has no tests.
What else could the compiler do for us? Let’s look at that same code again. What does it do? Let’s put some spaces in. It’s passing back an array… an array of what? We don’t know. We have some collections, we have a dictionary. What are the keys and values of that dictionary? We can probably work that out.
name as the key, and the top instance of
name is a string. The values are
counts, that’s a number (probably an unsigned integer), and we’re turning it into an
NSNumber. We’re adding more information to the code than we had before.
Get more development news like this
What else can we do? The keys are strings, and we’re sorting them. We’re expecting to return an array of strings. I’m trying not to change the code too much. I’m letting the compiler reassure me that I’m making good assumptions. Again, compile. And obviously we want to reflect these changes in the header file as well.
At this point, I would start decorating the places where these other warnings occur with the same decorator, so that we’re passing that knowledge around the app. The app is getting better documented as we go, and the compiler is doing some type checking for us. Hopefully I’ve shown you how you can use type checking to give you a bit of basic testing of the app before you get down to some real unit testing.
We heard earlier about how unit testing is all about models. I normally have a look at the models as one of the first places to think about unit testing, because they tend to be quite isolated. Once you understand the model you tend to understand more about the app. It could be a good insight to what the app is doing. We can see it has a core data instance, three entities with no relationship to each other, and some very basic entities.
Let’s go have a look at the
symptom is an NSmanaged object. It has the same properties as it had entities. We’re leaking information about how we’re persisting these objects to some storage, and all of our properties are mutable.
Mutability is not a desirable attribute; as a standard offering, we want to be able to control our mutability. If I had more time, I would probably think about wrapping the
symptom up into some sort of plain NSObject. We’d then be able to proxy the attribute of the access to them. Having looked at this code,
selected is the only thing we want to change, and that’s a boolean, but for the moment we’re exposing the fact that it’s actually a number.
I looked through this code earlier, and I noticed that it has magical records. Given that I know that the
symptom is an NSManagedObject, that I can mutate it, and that I have in it a magical record, in theory I could do all sorts of things at any time… which is not something you want in order to test an app. We can again get the compiler to give us a hand and tell us some more about what this app is doing.
As I said before, I don’t like mutability in my model objects unless I say so. I’m going to see if the compiler agrees with me, that nothing in this app is changing without my knowledge. I’m adding some decorators, doing the bare minimum possible in changing the code.
The compiler is telling me that there are four places in the app where we are attempting to mutate this
symptom object. The file
Symptom+Extras sounds like it’s probably an okay place to do that. It’s on
awakeFromInsert, so we’re adding one of these model objects into the database, and we’re setting a unique ID for it. That makes sense.
Next we have
DataManager, which sounds like a place where you want some management of data. We can see we’re setting up some data. We’re creating a list of symptoms here, and the interactions, the things you might encounter. Seems like another place where you might reasonably expect
symptom to mutate.
But this looks interesting. We’re inside of a settings table view controller, which is a UI table view controller. This is doing exactly what I was saying: I don’t like the idea it knows about magical record, it knows that we have
selected is a boolean, and is busy mutating them on the fly. I suspect the other one is exactly the same.
In fact, if I looked a bit harder at this, I might see some other things. We have analytics going on inside of the magical record save. We have UI view controller inside as well. We have quite a lot of stuff going on inside of what we would normally expect to be something that is hidden away inside, like a data manager.
If I had more time, this would be a perfect place to think about extracting the code and breaking it up into pieces that would more properly belong in different places. Obviously, the UI stuff probably still belongs in the view controller, but the magical record interactions probably do belong in the data manager and should be mediated such that I cannot set
selected to any value I like. It should be a boolean.
We used the compiler to exorcise the app, and we’ve learned about some of the “code smells” lurking around. We might want to write some real tests now, in the more traditional sense. (I’m going to undo those edits, because obviously this is autogenerated code and we’ll lose them anyway.)
QuickActions is the 3D touch on the app icon. Emily added a nice feature, which is you can log the things that you encounter during the day and the allergies that you get by doing a deep press on the icon. Things that you encounter frequently you are probably going to encounter again. This adds the top incidents you encounter to the shortcuts, and then this handles the shortcut. Let’s go have a look at this code.
We’re generating an array of these application shortcut items. And then we’re setting them onto UIApplication. Seems reasonable. If we wanted to write some tests for this, where would we start?
We’ve heard before that testing UI is possible but not always easy. In this case, I would consider looking a little closer: we have a title, which we’re generating on the fly for the shortcuts. Maybe we can test that given any incident, we have the right title for the shortcut.
(If I wanted to extract this code out, I would take a copy of it, and then hack away the bits I am not interested, see what I have left, and then think about what to do next.)
What are we going to return? A string called
shortcutTitleForIncidentName. I’ve tried not to change the code, but I have extracted from it a bit of code that I think is eminently testable, which is: given any incident name, what is the shortcut title for it? It will be very tempting at this point to take this new method that we created, and then to dump it straight in here. But that’s not testing, that’s moving code around. What we want to do at this point is do some testing.
In the great style of many children’s programs in the 1970s in the UK, here’s one I made earlier. We can say, given a particular incident name, what’s the shortcut name? Pretty straightforward test. Let’s see what happens.
It’s failing because it says it cannot find a particular method. Let’s check that I named it the same. I probably did. You probably suppose what the problem is because this is Objective-C, we cannot see that method. I normally like to do a category to expose those things that I have no choice but to expose in Objective-C, and borrow some of the conventions that we are seeing in Swift. If someone sees it later they know why it’s there and know it’s not really meant for you to be using, but for exposing for testing. Let’s see what happens now.
We have a small amount of code that we’ve extracted and can test. It’s very small, but it is a significant step forward from where we were. We can write many tests against this. What happens when we have a longer or empty name? The nice thing is that once we have a suite of tests, we can think about rewriting that code. Which is where we start to talk about Swift.
As I said, this is all Objective-C. There’s a lot of Objective-C code out there in the world. We’re probably going to encounter it for many years to come. We want to rewrite that code in many cases. I would suggest that the best way to rewrite it is to start by wrapping it in some tests. How are you going to know if you’ve introduced any new bugs, if you haven’t tested it thoroughly? It’s much easier to test this with a unit test than it is than to extract it, to test it manually.
Here we have a paging view controller. It has some viewy stuff on it, it has a delegate of some form, something called
resetDefaultPage, and a title text with it. It’s a chunky little view controller. I’ve scanned through this before, this method is the core of the thing. I’m going to do a quick search through there.
When we’re showing the different allergies that you might be able to select, you can page through them. Here’s a right arrow tap, and there we go change page, and a left arrow tap, change page. If we look at the swipe actions, they call the left arrow and right arrow tap. Most of the interactions, and resetting back to page zero, end up going through this method. As we might expect, it has some viewy stuff going on: it’s setting up the arrows, whether they are enabled or not, because we might hit the end of the options, doing some animation, but it does not seem a very testable thing.
Certainly, we could test it with UI tests, but we do not have UI tests yet; we’re still looking at the unit test level, if we think about the pyramid of testing that we talked about earlier. I did notice before that there is this code here, which is grabbing a
UILabel and putting that as our
UILabel has a title, which is a string; that could be a good thing to test. If we test the title of any page, given the page index was valid, we would not have to test every page manually anymore.
Let’s look at
titleForPageAtIndex. We have two implementations. Emily has left an exception: if we’re using the paging view controller version, you need to use your own implementation. It’s telling us what we expect the state of the play to be at run time. If this were Swift, you could use an assert here or precondition.
The code here feels awkward. We’re calling down to a subclass, which is then calling back up. When I looked at this code, I noticed that it’s very self-contained, which is nice. It seems to use these
rr_ add-date things, but there are no references to
self. That could potentially be something that we might have to extract, or isolate, or in some way test independently, essentially a unit. Where would we put this if we extracted it?
We have a lot of date handling code, but when I looked closer, I noticed
rr_addNumberOfDays is already in NSDate utilities. This
rr_dateFormatter is in NSDateFormatter+ utilities. If we put this particular method into either of those, we would create dependency between them. At the moment these two are independent of each other. This code, in fact, sits above them and makes use of them both. It doesn’t belong in either. We wouldn’t want to add that as a dependency.
I’m going to extract this code and see what happens when I put it somewhere else. That is the test. I’ve already extracted it; that’s exactly the same signature as it had before. I’ve changed it to class method, because it does not have any references to
self. By making it a class, we’re making it harder for NSDate to be interfering with this. We are saying this class, this method, the only thing it could be affecting is global state. It certainly cannot be affecting any object state. And then again, this is exactly what we had before. Obviously you saw it before, it was compiling before; it’s not compiling anymore because we’ve moved that around, but that’s easy to change. We’re going to change that to NSDate, which is the class we made the category on, and it compiles. At least, it looks sensible.
We have not written any tests yet, but we probably haven’t broken anything either. The nice thing is, this is completely isolated from the view controller. Our view controllers are smaller, which is always a good thing. We have nice isolated code that we can test, and we can see it’s returning strings for today, tomorrow, yesterday, and some date formatting. This seems a good thing that we might want to test.
I’m writing this test in Swift (even if I am not writing the code in Swift at this point). Test passes, but we will make sure that it fails when it’s not what we would expect.
Now we have the beginnings of some tests for something that’s part of the user interface, which is the title of every page in the app, where we are recording symptoms. That is nice: we can start testing this to see it behaves as we expect in every situation. Given it is a date, there are all sorts of possibilities. We could have a distant past or distant future. We have today, tomorrow, yesterday, and other dates. It has a lot of stuff that we can write tests around.
Once we’ve written all those tests, we could just delete all this code, rewrite it in Swift, and we will have a suite of tests that reassures us that we have not broken anything. Without these tests, we wouldn’t know that that was the case. It would require risky things to rewrite all of this.
These are simple, small changes. This is 10,000 lines of code, and you all probably have jobs where you have to produce code every day and ship product. Simply asking to completely cover the app and test is probably not something that many employers are going to entertain. But by doing small iterations like this, you can begin to extract pieces of the code and wrap them in tests.
Again, it is safe. If you’re interested in moving to Swift, you can then begin to rewrite those in Swift at the same time. These are the things you can do as you go without requiring or asking for a large amount of time to do a huge refactor–something that is often not well received, certainly at the places where I’ve worked in the past.
With every little change that you make, your code is getting more robust, and you’re getting more tests. Over time you’ll be able to get more confident about changing the code within the app and knowing that you have not broken anything. The more good tests you have, the more confident you can be that you are not breaking things as you go about refactoring.
I wanted to show you where I normally start when faced with what seems like an insurmountable job. Baby steps is my advice.
Q: You said that you created that test in Swift, and the code is in Objective-C. Have you found any new limitations, or do you not have to deal with that because after writing the test you change the code to Swift?
Michael: I’ve done both. I’ve written tests in Swift and left the Objective-C, and I’ve written tests in Swift and then rewritten the Objective-C.
You can write the test in Objective-C, of course. If your code base is still Objective-C, and you want to stay Objective-C (which is entirely your decision), write the test in Objective-C as well, and then keep the whole app in Objective-C.
If you are new to Swift, and you want to start adding Swift to your project, particularly if maybe your current team are not particularly inclined towards moving to Swift yet, writing tests in Swift is a good toe in the water. It’s production code and you should treat it as such, but it’s also not code that you are shipping. It’s a slightly easier sell to your team or to your management.
Q: Can you explain in a more general sense, rather than particular functions, how would you get started on coming to an app when you are well aware of what the app’s use is to begin with? How would you approach it if you were also coming from the point of view where you had to get up to speed on the domain of the app itself as well?
Michael: The first thing that you do is have a look around. I normally spend the first morning on a new case looking around. Are there any tests yet? Sometimes there are. Often there is a test target, and it has the boilerplate test in there, and that’s it.
Then I do often start with the model because, as I said before, it’s an easy thing to test as a general rule. It will begin to give you a sense of what the app’s core is about, and if you’re looking for mutability where you would not expect it, you can often start following trains through the app, i.e., where is this model object getting used or abused? And then start to poke around.
What you’re looking for is the core of the thing. What is holding it all up? That does take a bit of time, but poking around its code often is the way I normally do that. Hack at the stuff, then throw it all away when you’ve finished so you don’t make any untoward mistakes.
What we are trying do is, as much as possible, not changing the code at this point. If you are changing the code, that’s not testing it, that’s randomly changing code because you have not written any tests to be assured that you have not actually introduced new bugs that may not have been there before, or got rid of some business logic that was obscure that you couldn’t see, but is important. The analytics stuff we saw before as well. I’ve definitely done that before, and got rid of it, and then analytics went dark, and we were in trouble.
About the content
This talk was delivered live in July 2016 at CMD+U Conference. The video was transcribed by Realm and is published here with the permission of the conference organizers.