TAKING A TEST DRIVE
     Graham Lee, Fuzzy Aliens Limited




                                    fuzzyaliens.com
TAKING A TEST DRIVE
     Graham Lee, Fuzzy Aliens Limited
Pants              Pants




✘             ✔                ✘
            Trousers
PREVIOUSLY ON VTM…
STAssertTrue(thisThing.state == NSOnState, @”Is this thing on?”);
PREVIOUSLY ON VTM…
STAssertTrue(thisThing.state == NSOnState, @”Is this thing on?”);
PREVIOUSLY ON VTM…
STAssertTrue(thisThing.state == NSOnState, @”Is this thing on?”);




                                                            @ddribin
Episode 1 Summary
Software should satisfy the users’ needs

Testing demonstrates that this is true

  Unit testing can be done by the developer

    Cheap

    Well-situated

  Test-Driven Development is a design practice
This week, on VTM…

- (void)testBlah {
  STAssertTrue(thisThing.state == NSOnState,
          @”Is this thing on?”);
}
This week, on VTM…

OC_TEST_CASE(“General/IsOn”, “Is this thing on?”) {
  REQUIRE(thisThing.state == NSOnState);
}




            https://github.com/philsquared/Catch

                    @phil_nash
Rule 0
Rule 0




Model-View-Controller
Recipe: IBOutlets

- (void)viewDidLoad {
   NSAssert(myOutlet != nil);
   //...
}
Recipe: IBOutlets

- (void)setUp {
   ctrl = [[MyViewController alloc] initWithNibName: nil bundle: nil];
   label = [[UILabel alloc] init];
   label.text = @"Label";
   ctrl.label = label;
}

OC_TEST_CASE(“MyViewController/SetText”, “Text should be updated”) {
  [ctrl setText];
  REQUIRE([label.text isEqualToString: @"Set"]);
}

- (void)tearDown {
   [ctrl release];
   [label release];
}
Recipe: IBActions

- (IBAction)doSomething;


- (IBAction)doSomething: (id)sender;


- (IBAction)doSomething: (id)sender forEvent: (UIEvent *)event;
Recipe: Core Data
Recipe: Core Data


Rule 1 of testing Core Data:
Recipe: Core Data


Rule 1 of testing Core Data:


don’t talk about Core Data
Recipe: Core Data
Recipe: Core Data
Recipe: Core Data

- (IBAction)addStaff: (id)sender {
   NSManagedObject *employee = [[NSManagedObject alloc]
                        initWithEntity: employeeEntity
             insertIntoManagedObjectContext: moc];
   employee.lastName = @"Smith";
}




- (IBAction)addStaff: (id)sender {
   id <Employee> employee = [self.employeeBuilder newEmployee];
   employee.lastName = @"Smith";
}
Aside: Builder
Recipe: Core Data
Recipe: Core Data


Rule 2 of testing Core Data:
Recipe: Core Data


Rule 2 of testing Core Data:


   don’t use a database
A test is not a unit test if:

It talks to the database                Michael Feathers, idealized unit
                                        test
It communicates across the network

It touches the file system

It can't run at the same time as any of your other unit tests

You have to do special things to your environment
Recipe: Core Data
@interface TheTest : NSObject <OcFixture> {
@private
  NSPersistentStoreCoordinator *coord;
  NSManagedObjectContext *ctx;
  NSManagedObjectModel *model;
  NSPersistentStore *store;
}
@end
Recipe: Core Data
@implementation TheTest

- (void)setUp {
   model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
   coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
   store = [coord addPersistentStoreWithType: NSInMemoryStoreType
                     configuration: nil
                           URL: nil
                         options: nil
                          error: NULL];
   ctx = [[NSManagedObjectContext alloc] init];
   [ctx setPersistentStoreCoordinator: coord];
}

- (void)tearDown {
   [ctx release];
   ctx = nil;
   [coord removePersistentStore: store error: NULL];
   store = nil;
   [coord release];
   coord = nil;
   [model release];
   model = nil;
}

OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") {
  REQUIRE(store != nil);
}

@end
Recipe: Core Data
@implementation TheTest

- (void)setUp {
   model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
   coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
   store = [coord addPersistentStoreWithType: NSInMemoryStoreType
                     configuration: nil
                           URL: nil
                         options: nil
                          error: NULL];
   ctx = [[NSManagedObjectContext alloc] init];
   [ctx setPersistentStoreCoordinator: coord];
}

- (void)tearDown {
   [ctx release];
   ctx = nil;
   [coord removePersistentStore: store error: NULL];
   store = nil;
   [coord release];
   coord = nil;
   [model release];
   model = nil;
}

OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") {
  REQUIRE(store != nil);
}

@end
Recipe: Core Data
@implementation TheTest

- (void)setUp {
   model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
   coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
   store = [coord addPersistentStoreWithType: NSInMemoryStoreType
                     configuration: nil
                           URL: nil
                         options: nil
                          error: NULL];
   ctx = [[NSManagedObjectContext alloc] init];
   [ctx setPersistentStoreCoordinator: coord];
}

- (void)tearDown {
   [ctx release];
   ctx = nil;
   [coord removePersistentStore: store error: NULL];
   store = nil;
   [coord release];
   coord = nil;
   [model release];
   model = nil;
}

OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") {
  REQUIRE(store != nil);
}

@end
Recipe: Core Data
@implementation TheTest

- (void)setUp {
   model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
   coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
   store = [coord addPersistentStoreWithType: NSInMemoryStoreType
                     configuration: nil
                           URL: nil
                         options: nil
                          error: NULL];
   ctx = [[NSManagedObjectContext alloc] init];
   [ctx setPersistentStoreCoordinator: coord];
}

- (void)tearDown {
   [ctx release];
   ctx = nil;
   [coord removePersistentStore: store error: NULL];
   store = nil;
   [coord release];
   coord = nil;
   [model release];
   model = nil;
}

OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") {
  REQUIRE(store != nil);
}

@end
Recipe: Core Data
@implementation TheTest

- (void)setUp {
   model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
   coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
   store = [coord addPersistentStoreWithType: NSInMemoryStoreType
                     configuration: nil
                           URL: nil
                         options: nil
                          error: NULL];
   ctx = [[NSManagedObjectContext alloc] init];
   [ctx setPersistentStoreCoordinator: coord];
}

- (void)tearDown {
   [ctx release];
   ctx = nil;
   [coord removePersistentStore: store error: NULL];
   store = nil;
   [coord release];
   coord = nil;
   [model release];
   model = nil;
}

OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") {
  REQUIRE(store != nil);
}

@end
Recipe: NSNotificationCenter


Actually talking about Singletons in general

Rule #1 of Singletons is don’t use Singletons

Cocoa Touch doesn’t give us much choice
Recipe: NSNotificationCenter


Actually talking about Singletons in general

Rule #1 of Singletons is don’t use Singletons

Cocoa Touch doesn’t give us much choice

                   Remember, a singleton is a global in sheep’s clothing.
Recipe: NSNotificationCenter
- (void)watchForThingsHappening {
   [[NSNotificationCenter defaultCenter]
     addObserver: self
       selector: @selector(workDidFinish:)
          name: WorkDidFinishNotification
        object: worker];
}




OC_TEST_CASE("ViewController/WorkerFinished", "Ensure we
know when the work's done") {
  [controller watchForThingsHappening];
  REQUIRE(/*???*/);
}
Recipe: NSNotificationCenter
- (void)watchForThingsHappening: (NSNotificationCenter *)center {
   [center addObserver: self
          selector: @selector(workDidFinish:)
             name: WorkDidFinishNotification
            object: worker];
}



OC_TEST_CASE("ViewController/WorkerFinished", "Ensure we know when
the work's done") {
  [controller watchForThingsHappening: mockNotificationCenter];
  REQUIRE([mockNotificationCenter hasObserver: controller
                 forNotificationNamed:
                     WorkDidFinishNotification]);
}
Conclusions
Conclusions


TDD implies a particular approach to writing APIs
Conclusions


TDD implies a particular approach to writing APIs

Certain patterns support this approach way
Conclusions


TDD implies a particular approach to writing APIs

Certain patterns support this approach way

The patterns you’ve seen today work well with TDD and with the
Cocoa Touch SDK
THANKS!

http://blog.securemacprogramming.com/


       @iamleeg

Taking a Test Drive

  • 1.
    TAKING A TESTDRIVE Graham Lee, Fuzzy Aliens Limited fuzzyaliens.com
  • 2.
    TAKING A TESTDRIVE Graham Lee, Fuzzy Aliens Limited
  • 4.
    Pants Pants ✘ ✔ ✘ Trousers
  • 5.
    PREVIOUSLY ON VTM… STAssertTrue(thisThing.state== NSOnState, @”Is this thing on?”);
  • 6.
    PREVIOUSLY ON VTM… STAssertTrue(thisThing.state== NSOnState, @”Is this thing on?”);
  • 7.
    PREVIOUSLY ON VTM… STAssertTrue(thisThing.state== NSOnState, @”Is this thing on?”); @ddribin
  • 8.
    Episode 1 Summary Softwareshould satisfy the users’ needs Testing demonstrates that this is true Unit testing can be done by the developer Cheap Well-situated Test-Driven Development is a design practice
  • 9.
    This week, onVTM… - (void)testBlah { STAssertTrue(thisThing.state == NSOnState, @”Is this thing on?”); }
  • 10.
    This week, onVTM… OC_TEST_CASE(“General/IsOn”, “Is this thing on?”) { REQUIRE(thisThing.state == NSOnState); } https://github.com/philsquared/Catch @phil_nash
  • 11.
  • 12.
  • 13.
    Recipe: IBOutlets - (void)viewDidLoad{ NSAssert(myOutlet != nil); //... }
  • 14.
    Recipe: IBOutlets - (void)setUp{ ctrl = [[MyViewController alloc] initWithNibName: nil bundle: nil]; label = [[UILabel alloc] init]; label.text = @"Label"; ctrl.label = label; } OC_TEST_CASE(“MyViewController/SetText”, “Text should be updated”) { [ctrl setText]; REQUIRE([label.text isEqualToString: @"Set"]); } - (void)tearDown { [ctrl release]; [label release]; }
  • 15.
    Recipe: IBActions - (IBAction)doSomething; -(IBAction)doSomething: (id)sender; - (IBAction)doSomething: (id)sender forEvent: (UIEvent *)event;
  • 16.
  • 17.
    Recipe: Core Data Rule1 of testing Core Data:
  • 18.
    Recipe: Core Data Rule1 of testing Core Data: don’t talk about Core Data
  • 19.
  • 20.
  • 21.
    Recipe: Core Data -(IBAction)addStaff: (id)sender { NSManagedObject *employee = [[NSManagedObject alloc] initWithEntity: employeeEntity insertIntoManagedObjectContext: moc]; employee.lastName = @"Smith"; } - (IBAction)addStaff: (id)sender { id <Employee> employee = [self.employeeBuilder newEmployee]; employee.lastName = @"Smith"; }
  • 22.
  • 23.
  • 24.
    Recipe: Core Data Rule2 of testing Core Data:
  • 25.
    Recipe: Core Data Rule2 of testing Core Data: don’t use a database
  • 26.
    A test isnot a unit test if: It talks to the database Michael Feathers, idealized unit test It communicates across the network It touches the file system It can't run at the same time as any of your other unit tests You have to do special things to your environment
  • 27.
    Recipe: Core Data @interfaceTheTest : NSObject <OcFixture> { @private NSPersistentStoreCoordinator *coord; NSManagedObjectContext *ctx; NSManagedObjectModel *model; NSPersistentStore *store; } @end
  • 28.
    Recipe: Core Data @implementationTheTest - (void)setUp { model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain]; coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model]; store = [coord addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; ctx = [[NSManagedObjectContext alloc] init]; [ctx setPersistentStoreCoordinator: coord]; } - (void)tearDown { [ctx release]; ctx = nil; [coord removePersistentStore: store error: NULL]; store = nil; [coord release]; coord = nil; [model release]; model = nil; } OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") { REQUIRE(store != nil); } @end
  • 29.
    Recipe: Core Data @implementationTheTest - (void)setUp { model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain]; coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model]; store = [coord addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; ctx = [[NSManagedObjectContext alloc] init]; [ctx setPersistentStoreCoordinator: coord]; } - (void)tearDown { [ctx release]; ctx = nil; [coord removePersistentStore: store error: NULL]; store = nil; [coord release]; coord = nil; [model release]; model = nil; } OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") { REQUIRE(store != nil); } @end
  • 30.
    Recipe: Core Data @implementationTheTest - (void)setUp { model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain]; coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model]; store = [coord addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; ctx = [[NSManagedObjectContext alloc] init]; [ctx setPersistentStoreCoordinator: coord]; } - (void)tearDown { [ctx release]; ctx = nil; [coord removePersistentStore: store error: NULL]; store = nil; [coord release]; coord = nil; [model release]; model = nil; } OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") { REQUIRE(store != nil); } @end
  • 31.
    Recipe: Core Data @implementationTheTest - (void)setUp { model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain]; coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model]; store = [coord addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; ctx = [[NSManagedObjectContext alloc] init]; [ctx setPersistentStoreCoordinator: coord]; } - (void)tearDown { [ctx release]; ctx = nil; [coord removePersistentStore: store error: NULL]; store = nil; [coord release]; coord = nil; [model release]; model = nil; } OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") { REQUIRE(store != nil); } @end
  • 32.
    Recipe: Core Data @implementationTheTest - (void)setUp { model = [[NSManagedObjectModel mergedModelFromBundles: nil] retain]; coord = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model]; store = [coord addPersistentStoreWithType: NSInMemoryStoreType configuration: nil URL: nil options: nil error: NULL]; ctx = [[NSManagedObjectContext alloc] init]; [ctx setPersistentStoreCoordinator: coord]; } - (void)tearDown { [ctx release]; ctx = nil; [coord removePersistentStore: store error: NULL]; store = nil; [coord release]; coord = nil; [model release]; model = nil; } OC_TEST_CASE("CoreData/Store", "Ensure the persistent store was set up.") { REQUIRE(store != nil); } @end
  • 33.
    Recipe: NSNotificationCenter Actually talkingabout Singletons in general Rule #1 of Singletons is don’t use Singletons Cocoa Touch doesn’t give us much choice
  • 34.
    Recipe: NSNotificationCenter Actually talkingabout Singletons in general Rule #1 of Singletons is don’t use Singletons Cocoa Touch doesn’t give us much choice Remember, a singleton is a global in sheep’s clothing.
  • 35.
    Recipe: NSNotificationCenter - (void)watchForThingsHappening{ [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(workDidFinish:) name: WorkDidFinishNotification object: worker]; } OC_TEST_CASE("ViewController/WorkerFinished", "Ensure we know when the work's done") { [controller watchForThingsHappening]; REQUIRE(/*???*/); }
  • 36.
    Recipe: NSNotificationCenter - (void)watchForThingsHappening:(NSNotificationCenter *)center { [center addObserver: self selector: @selector(workDidFinish:) name: WorkDidFinishNotification object: worker]; } OC_TEST_CASE("ViewController/WorkerFinished", "Ensure we know when the work's done") { [controller watchForThingsHappening: mockNotificationCenter]; REQUIRE([mockNotificationCenter hasObserver: controller forNotificationNamed: WorkDidFinishNotification]); }
  • 37.
  • 38.
    Conclusions TDD implies aparticular approach to writing APIs
  • 39.
    Conclusions TDD implies aparticular approach to writing APIs Certain patterns support this approach way
  • 40.
    Conclusions TDD implies aparticular approach to writing APIs Certain patterns support this approach way The patterns you’ve seen today work well with TDD and with the Cocoa Touch SDK
  • 41.

Editor's Notes

  • #2 \n
  • #3 \n
  • #4 \n
  • #5 \n
  • #6 \n
  • #7 \n
  • #8 \n
  • #9 \n
  • #10 \n
  • #11 \n
  • #12 \n
  • #13 \n
  • #14 \n
  • #15 \n
  • #16 The idea is to provide a set of recipes that you can use in testing iPhone apps. Note that I won&amp;#x2019;t talk about the automated UI testing Instrument - that&amp;#x2019;s not TDD or unit testing.\n
  • #17 The idea is to provide a set of recipes that you can use in testing iPhone apps. Note that I won&amp;#x2019;t talk about the automated UI testing Instrument - that&amp;#x2019;s not TDD or unit testing.\n
  • #18 The idea is to provide a set of recipes that you can use in testing iPhone apps. Note that I won&amp;#x2019;t talk about the automated UI testing Instrument - that&amp;#x2019;s not TDD or unit testing.\n
  • #19 The idea is to provide a set of recipes that you can use in testing iPhone apps. Note that I won&amp;#x2019;t talk about the automated UI testing Instrument - that&amp;#x2019;s not TDD or unit testing.\n
  • #20 The idea is to provide a set of recipes that you can use in testing iPhone apps. Note that I won&amp;#x2019;t talk about the automated UI testing Instrument - that&amp;#x2019;s not TDD or unit testing.\n
  • #21 I&amp;#x2019;ve previously said that NeXT should never have allowed ObjC code that didn&amp;#x2019;t follow MVC to compile. I still stand by that.\n
  • #22 The most common thing I get wrong with outlets is to not set them in the NIB. My strategy for dealing with this is to assert that the outlet is not nil so the app will die if I forget.\n\n
  • #23 There&amp;#x2019;s no problem with programmatically creating views in test code, you don&amp;#x2019;t even need to provide a frame if not necessary. That avoids relying on the XIB in test cases, which is environmental data. For complicated views, it may be easier to fake them.\n
  • #24 The arrow shows increasing &amp;#x201C;tell, don&amp;#x2019;t ask&amp;#x201D;-ness. If you use the first action type, the action method has to find out what it needs to know. That means introspection, which is harder to test than explicit demand (&amp;#x201C;no, use _this_ button&amp;#x201D;).\n
  • #25 \n
  • #26 \n
  • #27 \n
  • #28 \n
  • #29 \n
  • #30 The Builder pattern allows you to decouple the construction of objects from their use. This is automatic to some extent in ObjC because of the way Class objects work, but in this case it&amp;#x2019;s good to have an object that returns a class of a type you define.\n
  • #31 \n
  • #32 \n
  • #33 It&amp;#x2019;s also important to look at what a unit test is not. How to avoid I/O -&gt; fake/mock objects. In-memory CoreData. Textcast has 212 unit tests that run in 0.6 seconds.\n\n&amp;#x201C;Tests that do these things aren&apos;t bad. Often they are worth writing, and they can be written in a unit test harness. However, it is important to be able to separate them from true unit tests so that we can keep a set of tests that we can run fast whenever we make our changes&amp;#x201D;\n
  • #34 \n
  • #35 Note that this does violate the &amp;#x201C;don&amp;#x2019;t depend on data&amp;#x201D; principle, but Core Data is useless without a MOM so you need to do this. The real deal is the in-memory store. Note that an error is ignored in -tearDown, but if you have that error then you should write a test anyway. As I did to ensure that the store is correctly set up.\n
  • #36 Note that this does violate the &amp;#x201C;don&amp;#x2019;t depend on data&amp;#x201D; principle, but Core Data is useless without a MOM so you need to do this. The real deal is the in-memory store. Note that an error is ignored in -tearDown, but if you have that error then you should write a test anyway. As I did to ensure that the store is correctly set up.\n
  • #37 Note that this does violate the &amp;#x201C;don&amp;#x2019;t depend on data&amp;#x201D; principle, but Core Data is useless without a MOM so you need to do this. The real deal is the in-memory store. Note that an error is ignored in -tearDown, but if you have that error then you should write a test anyway. As I did to ensure that the store is correctly set up.\n
  • #38 Note that this does violate the &amp;#x201C;don&amp;#x2019;t depend on data&amp;#x201D; principle, but Core Data is useless without a MOM so you need to do this. The real deal is the in-memory store. Note that an error is ignored in -tearDown, but if you have that error then you should write a test anyway. As I did to ensure that the store is correctly set up.\n
  • #39 NSNotificationCenter is a good example of a singleton that app code often makes use of. Singletons encourage &amp;#x201C;ask, don&amp;#x2019;t tell&amp;#x201D; because it&amp;#x2019;s easy to grab the shared instance from anywhere. The problem is that this means relying on the code&amp;#x2019;s environment, which is bad. Let&amp;#x2019;s try and get out of that.\n
  • #40 NSNotificationCenter is a good example of a singleton that app code often makes use of. Singletons encourage &amp;#x201C;ask, don&amp;#x2019;t tell&amp;#x201D; because it&amp;#x2019;s easy to grab the shared instance from anywhere. The problem is that this means relying on the code&amp;#x2019;s environment, which is bad. Let&amp;#x2019;s try and get out of that.\n
  • #41 NSNotificationCenter is a good example of a singleton that app code often makes use of. Singletons encourage &amp;#x201C;ask, don&amp;#x2019;t tell&amp;#x201D; because it&amp;#x2019;s easy to grab the shared instance from anywhere. The problem is that this means relying on the code&amp;#x2019;s environment, which is bad. Let&amp;#x2019;s try and get out of that.\n
  • #42 Statement of the problem.\n
  • #43 This is the best solution. Another approach is to swizzle the -defaultCenter method to return your mock. That&amp;#x2019;s a bit dangerous: though test code isn&amp;#x2019;t production code so if it works for you, do it. Beware the pitfalls though.\n
  • #44 \n
  • #45 \n
  • #46 \n
  • #47 If I missed any recipe you would like to have seen, let me know and I&amp;#x2019;ll write it up.\n