.st0{fill:#FFFFFF;}

My Dependency Injection Article in the objc.io Testing Issue 

 August 12, 2014

by Jon Reid

7 COMMENTS

objc.io is a monthly online magazine, with an editorial team. Each issue focuses on a particular subject, and Issue #15 is about Testing. I’m honored to be a contributor with my article “Dependency Injection”.

Books

My article refers to two books I highly recommend:

  • Dependency Injection in .NET by Mark Seemann. This book is marvelous, and I need to study it more. (If you’re wondering why an Objective-C programmer is recommending a book with “.NET” in the title, you need to get out more. Learn what’s going on in other languages, and the smart folks who work in them.)
  • Working Effectively with Legacy Code by Michael Feathers. This now-classic text was instrumental to me when I first got started writing unit tests, helping me break through many conceptual barriers. Feathers boldly defines legacy code as “code without tests.”

Question: Do you have any comments about my Dependency Injection article, or any of the other Testing issue articles? Leave a comment below.

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • Jon
    Thanks so much for your very clear and instructive article. In fact the whole objc.io issue this month is pure gold, but that’s normal.
    I’m a noob at TDD but not at development in general. Perhaps I’m starting too far out in the deep end, but I have a subsystem of an app which causes problems for maintenance because it’s pretty complex. It has a couple of moving parts, as it were:
    * Networking – talks to a web server on a hardware device using HTTP; for simplicity I rely on AFNetworking to do the NSURL stuff. The device has updatable firmware using a tool supplied by the manufacturer, that occasionally adds or corrects HTTP APIs.
    * Internal state – I model this using a state machine (YBStateChart) and a couple of model classes to represent the data returned by the web server; there are timers for periodic updates, HTTP queries to the web server, reactions to the web server’s responses, downloading files from the server, and more.
    The conundrum is how to take this “legacy” system, which is working fairly well, and encapsulate its behaviour in a series of tests that will enable me to maintain it. What should I be looking for as indicators that the system is working? Do I need to expose some of the internal state so it can be checked by the testing code after some input is applied? Or do I concern myself purely with results and ignore the internal state?
    It’s mostly asynchronous (has to be because it uses the network) and it has some complex internal state transitions that are determined by sequences of triggers in other parts of the app and responses from the hardware device. I haven’t yet come across any TDD article that deals with how to test something this complex.
    It looks to me like I’m going to have to mock up the hardware device’s API so I don’t have to physically connect to it each time I run tests, especially since some of the operations actually modify the device’s configuration and require a physical power-off-power-on restart. However how do you cope with the fact that since it’s a hardware device and its firmware changes from time to time you then have to maintain this dummy version of the real thing, in addition to all your code that talks to it? Suppose your dummy version isn’t correct? All your tests might pass, but the real code won’t work.
    I see that you’ll be at iOSDevUK next month. I’d love to be able to be there too and get some insights in person, but perhaps also you could enlarge in a future article or blog post on how you would approach this kind of system.

    • David,

      Maybe Jon will chime in later, but here are some tips I’ve learned about testing complex apps like yours…

      Test-Driven Development (TDD) has the word “test” in it, but I think the word “test” actually confuses the real benefits to practicing TDD. For me, the main benefit of TDD is that my team’s code starts following the Single Responsibility Principle much better. This, in turn, reduces the number of “side-effects” we have to deal with – situations where fixing one bug breaks something else.

      TDD clicked for me when I stopped thinking of it as testing and started thinking of it as something more like “executable comments.” The suite of unit tests that you wind-up with in the end is just a nice extra benefit, a safety net for future refactoring.

      So, TDD produces a suite of unit tests, but that’s not the be-all-and-end-all of automated testing. The scenarios you described (networking, asynchronous, etc.) are often better-served by integration or acceptance tests than unit tests. Unit tests typically exercise one-thing-and-one-thing-only while integration tests are concerned with interactions between multiple components (networking being a good example.) I use the Kiwi framework for integration testing. Frank and KIF are a couple alternatives.

      Here’s some sample Kiwi code that I used to test an asynchronous networking process…


      context(@"when attempting to start Reachability on a background thread", ^{
      it(@"should eventually receive a notification indicating success or failure", ^{
      sut = [[ReachabilityFacade alloc] init];

      [[expectFutureValue(sut) shouldEventuallyBeforeTimingOutAfter(60.0)] receive:@selector(reachabilityStarted:)];

      [sut startReachabilityOnBackgroundThread];
      });
      });

      A good rule-of-thumb I’ve learned…

      Anytime I add much more than 2 or 3 mocks trying to make a unit test do what I want, I’ve begun to veer away from TDD and unit testing and towards Kiwi and integration testing. At that point, it’s simpler and less fragile to just setup a Kiwi test with the real objects.

    • David,

      As Brandon says, there are different kinds of tests. Unit tests answer the question, “Did I build this piece correctly?” Acceptance tests answer the question, “Does it all work?” Both are important.

      For a legacy system in particular, I want some kind of tests that will enable me to refactor things into a cleaner architecture where the Single Responsibility Principle applies more & more. Acceptance tests are great, but slow down TDD. I want feedback faster, so I want to write unit tests.

      The problem is that in order to write unit tests on existing code, you often have to redesign things so that dependencies are injectable. How does one go about making such changes in a safe manner? That’s what the book Working Effectively with Legacy Code is all about; I highly recommend it.

  • Hey, Jon – I’m that “other” iOS guy who swears by the Mark Seemann book. (Maybe there are more of us out there somewhere.)

    Most of your examples use nibs. I frequently use storyboards and doing TDD with view controllers becomes a bit awkward. I wanted to get your opinion on my method for Constructor Injection with storyboards and view controllers.

    The awkward part is that it seems like you need three initializers to make it happen.

    1. An initializer for Constructor Injection of your dependencies, which in turn calls the designated initializer for UIViewController.
    2. The initWithCoder: initializer, which is the one that Storyboards use when loading a view.
    3. The initWithNibName:bundle: which, even though it’s the designated initializer for UIViewController, is never actually used when you’re doing Storyboards exclusively.


    #pragma mark - Initializers

    // Initializer for Constructor Injection
    - (id)initWithEquipmentList:(PSEquipmentList *)equipmentList
    {
    self = [super initWithNibName:nil bundle:nil];

    if (self)
    {
    _equipmentList = equipmentList;
    }

    return self;
    }

    // Initializer used by Storyboards
    - (id)initWithCoder:(NSCoder *)aDecoder
    {
    self = [super initWithCoder:aDecoder];

    if (self)
    {
    _equipmentList = [[PSEquipmentList alloc] init];
    }

    return self;
    }

    // Designated initializer for UIViewController
    - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
    {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];

    if (self)
    {
    _equipmentList = [[PSEquipmentList alloc] init];
    }

    return self;
    }

    The duplication is a bit troubling. What do you think of including a redundant Property Injection with Local Default? Just to protect against a future change that forgets to initialize the instance variable.


    #pragma mark - Properties

    - (PSEquipmentList *)equipmentList
    {
    if (!_equipmentList)
    {
    _equipmentList = [[PSEquipmentList alloc] init];
    }

    return _equipmentList;
    }

    Thanks for the great article in objc.io! All of the material there is top-notch.

    • Brandon,

      This is a good idea for another blog post. In the meantime, here’s the short version:

      Storyboard-created view controllers are the perfect time to prefer property injection over constructor injection. Ditch all your other initializers, except for initWithCoder:

      Then you need a default. This can be done as you show, with a lazy getter. But it can also be done by making the property an outlet, and providing the default in the storyboard itself.

      • Supplying the default through an outlet sounds almost like turning the storyboard into a “de facto” DI Container. That’s an interesting idea – I’ll have to try it out.

  • Jon & Brandon

    Many many thanks for your illuminating insights. I will need to re-read a few times to make sure I inwardly digest everything.

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >