Wednesday, August 13, 2014

Playing With Swift: Extensions

I am finally getting around to playing with Swift, Apple's new language for Mac and iOS development. I have to say, I am excited to have a new language to learn. Swift looks very interesting as it pulls in many great features of other modern languages.

Like Objective-C, and Ruby, and probably others that I don't know about, Swift allows us to add functionality to any class, including the built-in classes like String and Array.

Here are a couple of examples of extending the Array and the String built-in classes to add a join() method.

First, let's extend the Array class to add a join(sep:String) method that returns a string containing all of the elements of the Array, delimited by the separate value passed in.

extension Array {
    func join(sep:String) -> String {
        var s = ""
        if let firstItem = self.first {
            s = "\(firstItem)"
            for i in 1..<self.count {
                s = s + sep + "\(self[i])"
            }
        }
        return s
    }
}

Pretty cool, right? But what if our religion thinks it's better to extend String and pass in the list to be joined (Python style)?

extension String {
    func join<T>(list:Array<T>) -> String {
        var s = ""
        if let firstItem = list.first {
            s = "\(firstItem)"
            for str in list[1..<list.count] {
                s = s + self + "\(str)"
            }
        }
        return s
    }
}

Just as easy. Did you notice the slight variation to use a slice of the array, rather than a for-loop?

While I love a good pedantic discussion about which class is more "proper" to extend, you probably don't have time for such things, so let's just extend them both. And to keep it DRY, let's alter our Array extension to call the String extension via the separator argument. (Yes, I've been influenced by Python.)

extension Array {
    func join(sep:String) -> String {
        return sep.join(self)
    }
}

One enhancement I would like to make to these methods is to use a default parameter value for the separator. Unfortunately, as of this writing, defining a default parameter causes a segfault in Xcode. It is beta software, after all!

Saturday, August 9, 2014

The Real Value of Writing Automated Tests

The product manager asks in the standup, "Why did this start to fail? Don't we have tests that cover this?"

"Yes, we have tests. They all passed," we respond.

"Then your tests aren't very good."

Well, maybe. It's true, we could write a lot more tests. In general, if your software team is committed to automated testing, you'll write at least as much test code as application code. Project estimators should plan on writing two applications - the one with all the features, and a second one to test the first one.

In our case, the unit tests were all passing, but some of the integration tests started failing. These are the tests that verify the interactions among the various parts of the application. Of course, each time we find a new situation where a test fails, we try to figure out why, and cover that scenario with yet another test. In this way, what evolves is a system that informs us when our software parts interact in ways we didn't expect.

Or maybe it devolves? It takes a lot of discipline in a team (even a team of one) to keep your testing system clean and orderly. Often it becomes a hodgepodge of bolted on test cases that can start disagreeing even with themselves! We've all been victims of someone else's failure to tearDown() completely.

Even in mature projects, the tests don't seem to catch everything, no matter how thorough we think we've been.

So why go to all the extra effort? If the tests require as much maintenance as the application itself, what value do they really bring to the project?

First, tests make sure existing interfaces between parts of the application remain consistent.

Second, tests make you as a developer consider the more subtle interfaces than you might not have been aware of. If you consider that your module's interface might be more than just the method signature and return value, you begin to understand some of these more subtle interfaces "Whoa, why did that fail? I didn't even touch that?!" It could be because that code you just refactored had a side effect that other code has come to depend upon. Oops. Good thing the test caught that. Of course, sometimes the tests don't catch every subtle interface impact. But they work often enough that their value to the project cannot be disclaimed.

Finally, and in my opinion, most importantly, when tests are automated (and hopefully executed by a continuous integration system), they keep you from cutting corners when it becomes crunch time. I've developed software under various methodologies. Regardless of the individual tenets of each methodology, they always seem to slip into a "just get it done" mode as milestones approach. Often, when a trouble ticket arrives, the developer jumps right in, makes a quick fix, commits the code, gets a buddy to review it, merges it into the head (tip, trunk, whatever), then receives the email from the CI server that they broke the build. The automated tests catch the fact that the developer didn't think about everything they should have thought about when they made their change.

I once worked in a team that rewarded the build-breaker with a rubber chicken that had to hang over their cubicle until the build turned green again. Yes, I flew the rubber chicken a lot. My role in that team was mostly in the infrastructure of the application, so when I goofed up, it broke a lot of things. Fortunately, we had a decent test system in place, and the CI server let me know about my shortcomings very quickly. So in crunch time, when everyone's focus is naturally narrowed to a dangerous tunnel vision, our automated testing system forced us to widen our vision and take a breath, and think more broadly about everything our simple little change might affect.

Summary

  • Testing is hard work. Plan on writing two applications.
  • Testing helps maintain a consistent interaction between software modules.
  • Automated testing helps you consider more of the overall effect of your change when under pressure.
And finally,
  • Fly a rubber chicken in the team. It's a lot of fun. The look on other teams' managers' faces when they walk down the hall is priceless.