Angelo Stavrow [dot] Blog

tghr

The Great HoneyJar Refactoring is a series of posts in which I take the first iOS app I ever wrote, HoneyJar, and refactor it out of its original burning-dumpster-fire state and into a modern app. And I'm doing it in public.

Earlier this week, I tweeted about my adventures in trying to add a test suite to HoneyJar.

The idea is this: I want to be sure that I'm not breaking anything in the app, as it exists right now, when I start refactoring. Without an existing test suite, I have no way of knowing if I'm creating any regressions.

The TDD approach is to

  1. Write a test that checks a particular public method against a particular condition;
  2. Write the code that makes the test pass; and
  3. Repeat steps 1 and 2 until you're done writing the method / class / app.

The tests come first, and the code “fills in the blanks”.

When you've got a legacy codebase1, however, does it still make sense to write failing tests first? Probably not.

Furthermore, what if it's just not possible to test certain methods or classes, because of the way the code is structured? If you haven't written the code with testing in mind, then it might be too coupled or complex to test after the fact.

Stop writing legacy code. Do not write new features without unit testing. You are just making the code base worse and getting further away from introducing tests into your system. Get your testing infrastructure set up and a few tests running successfully and it will be much easier to think about introducing tests for the rest of the code.

— Dan Lee, TDD when up to your neck in legacy code

Right now, what's making me a little bit crazy is that I'll open a class to start adding some unit tests, see all kinds of ways to improve the code before adding the tests, and have to remind myself that I've made a promise not to touch the code until the test suite is in place.

SO. FRUSTRATING.

It's especially frustrating when you realize that you'd written something that you can't really test, but you're not allowing yourself to fix it.

But we have a great way of getting around that. Comments.

So, here's what I'm doing:

  1. Open the class to which I want to add tests;
  2. Add a new test case class for said tests;
  3. Scan through the class under test and grind my teeth at obvious problems;
  4. Take three deep breaths;
  5. Start adding whatever tests that I can add without changing the code; and
  6. Add thorough //TODO comments on “next steps” for refactoring this class—obvious problems, how to improve testability, &cet.

I'm going to go through doing this for the entire codebase first. I could add some tests for a class, then refactor it, then move on to the next one, but then I have no way of knowing if I'm making a change that will somehow propagate through the app and trigger a failure elsewhere. So, while I'm not especially happy with the coverage I'm getting right now, it's better this way.

Once the issue is closed, I'll start tackling the actual refactoring—and I'll feel more like the project is ready for others to work on, too. For me, at least, I t's really hard to make changes to a codebase I'm unfamiliar with if I don't have the safety net of a test suite.

As always, more tk.

  1. By "legacy codebase", here, we mean untested code, i.e., a codebase with no pre-existing test suite.

#tghr

Discuss...

The Great HoneyJar Refactoring is a series of posts in which I take the first iOS app I ever wrote, HoneyJar, and refactor it out of its original burning-dumpster-fire state and into a modern app. And I'm doing it in public.

Last week I introduced an idea I had: to open-source my first-ever iOS app, embarrassing as it might be, and refactor it out in the open.

Over the last week, I handled the open-sourcing of the app. This took a bit more time than I expected, and normally this should be done before releasing the project to the wild, but I figured it might be interesting to see just what the process was, what I learned, and some resources I found along the way.

The starting point

HoneyJar, in its closed-source state, was very clearly an “I'm learning as I go and am super excited to ship” kind of project. It had a README, kinda, which really just re-iterated some tagline-style description of the project, and that's it. Unit tests were few and far between, comments were… not awesome, and there was no real structure in place to make it useful for other contributors.

So the first step was to do exactly that. I opened the first issue and set up a task list:

  • Add a CONTRIBUTING document
  • Add a LICENSE document
  • Add a CHANGELOG document
  • Update the README to provide more information on the project
  • Set up GitLab continuous integration

Okay. I know. That sounds like a pretty straightforward list. Quick and easy to add. Turns out, you should put a little effort into these things.

Readez-moi

The first thing most people will see in a project is the README file. This should provide some information on how to install/use the project, where to find further info on licensing and contributing, &cet. But how much info? What exactly do I need to add?

I found an excellent template by Billie Thompson that fits the bill nicely. While I've commented out a bunch of stuff that isn't necessarily relevant (or maybe not relevant yet), I think it provides a pretty good overview of the what, who, why, and hows of the project. I'm relatively happy with the way the README turned out.

With every change, log, log, log

Next up: everyone's favourite, the CHANGELOG.

No, seriously. This either gets totally overlooked, or totally becomes a series of punchlines about episodes of Friends or whatever. In trying to figure out what needs to go into a change log, I discovered Keep A CHANGLELOG. Great resource.

As you go through merging in changes, part of the merge should including updating the change log with whatever you've, well, changed. As the project maintainers indicate, typical sections are:

  • Added for new features.
  • Changed for changes in existing functionality.
  • Deprecated for once-stable features removed in upcoming releases.
  • Removed for deprecated features removed in this release.
  • Fixed for any bug fixes.
  • Security to invite users to upgrade in case of vulnerabilities.

Additionally—I love this— every subtitle of the document should be a link to a git compare of the previous and current tags (or HEAD, if you're working on an unreleased version).

Excellent idea.

License to thrive

Jeff Atwood said it in 5 words. Pick a license. This is important, or you're pretty much putting your work out there without letting anyone know if they can use it. Picking a license helps you both cut the overhead of having to reply to emails (“can I use this code?”) while putting down some ground rules on how your work can be used.

I went with a simple 3-clause BSD license. Essentially, this is a use-as-is-without-warranty license, requiring attribution by others who redistribute your work, and written permission if they want to use my name (or my organization's name) in any endorsement or promotion of their derived products.

Just add contributors

The hardest to set up was the CONTRIBUTING document. Here, there are two main things I wanted to address:

  1. Expectations of all involved (i.e., a code of conduct), and
  2. Just how to contribute.

The how-to-contribute part is pretty straightforward. Everything should start with an issue, and once labels and owners are assigned, work can begin. There's some basic info on how to report a bug vs. how to add a feature request, and I added some requirements for merge requests too. Such requirements can get pretty complex and involved, but it's important to step back and examine the scope of the project too. We're not building CocoaPods here; a tiny not-super-important project like HoneyJar probably won't attract much in the way of contributions, so there's no need for overly complex requirements for making changes.

That said, I believe that any project, regardless of size, has the potential for community. And a community is only as good as the expectations for interaction. In my work, as in my life, I really try to come from a position of trust, mindfulness, and open-ness, so putting together a code of conduct that embraces these values is important to me.

Luckily, I'm not the only one that feels this way. The Contributor Covenant is a good place to start, and other open-source communities (1, 2, 3, as examples) have done a great job of adapting it. I've followed in their footsteps, and I think the code of conduct section nicely explains the let's-be-wonderful-to-each-other feels that I want in my community.

All the great fails

The last thing I absolutely wanted up and running for this first issue was to get continuous integration up and running.

“But Angelo, you don't have a test suite set up for your project.”

Yeah, I know. But the next big step before I start working on the actual refactoring is exactly that: add a test suite for existing code. It would be downright silly to try refactoring something without making sure that you're not breaking anything as you go through.

So, I followed the steps in the article I wrote and created a new runner and a .gitlab-ci.yml file, along with some sample test, to make sure everything's working.

And builds kept getting stuck with the error:

Failed to authorize rights (0x1) with status: -60007.

Turns out, I needed to update the runner on my build machine to address some changes in Xcode 7.3—once that was done, things started building as expected. One of the great things about GitLab being an open-source project is that if you're running into trouble, there's likely an issue available to help you figure out what's happening and how to fix it (and if there isn't, open one!).

I've left the build history as-is, along with the commit history, as breadcrumbs to how I managed to get things working, along with the stupid mistakes like overlooked typos in your runner tags. I don't think there's any value in trying to hide these goofs, because they could be invaluable the next time you're yelling at your system for not just working come on you stupid thing everything was fine a minute ago what do you mean this tag doesn't exist.

Onwards and upwards

So, the project is now, IMO, officially open-sourced. The next big step is to add the test suite, and then I can move on to the fun stuff: refactoring.

More tk.

#tghr

Discuss...

Over the next few weeks, I've decided to write a series of posts about taking my first iOS app, HoneyJar, and refactoring it from its current (terrible) state into something, well, less-terrible.

And I plan on doing it in public, too.

I'm calling this project The Great HoneyJar Refactoring.

HoneyJar is a future-value calculator. As explained on the product page, you set a payment amount, a rate of return, and a period of time, and HoneyJar will give you a scenario showing how your money will grow if the amount entered amount was

  • a one-time lump-sum payment
  • a series of equal weekly payments
  • a series of equal monthly payments

I haven't really touched it after I released 1.0 save for a few minor updates. It could certainly use some love, as there's a whole lot of technical debt going on in that codebase. Just off the top of my head, without having poked around the repository for months, I can list the following required fixes:

  1. Refactor the main UIViewController into several lighter classes. The thing is a mess.
  2. Along a similar line, convert the project to use Storyboards and Autolayout.
  3. Add a test suite.
  4. Add localization (I'll start with English and French, since I speak those languages pretty fluently).
  5. Ensure accessibility.
  6. Port the thing to Swift, or at least write new code in Swift.
  7. General UI/UX improvements (e.g., make the result presentation clearer).
  8. Drop support for devices running iOS 8 and before.

I'm going to call this version 2.0 and I've opened a milestone in GitLab to track my progress. The associated issues will be added over the next day or two, so you can see how what I'm working on at any given time. I'll also be setting up CI and other (non-code) project settings.

So, I'm open-sourcing the (embarrassing) version as it exists right now—please be nice—and I plan on tackling one issue every couple of weeks or so, until the milestone is closed and 2.0 is released. Each time I close an issue, I'll write up a post on the whys and wherefores of the changes.

I haven't yet decided if I'll accept any merge requests during this project, but I certainly look forward to feedback on my work. To make it easier for people, to follow along, I will be mirroring the repository on GitHub, too—GitLab makes it easy to push changes to a remote repo. GitLab is my origin of choice, though, thanks to the built-in continuous integration.

Update (2016-05-25): I've moved the GitHub mirror from my personal account to the organization account, since it's Dropped Bits that's handling the App Store stuff.

More tk!

#tghr

Discuss...