Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kiwi

Jeff Burt
September 11, 2014

 Kiwi

A talk I did at an Ann Arbor CocoaHeads meetup on the Kiwi testing framework.

Jeff Burt

September 11, 2014
Tweet

More Decks by Jeff Burt

Other Decks in Programming

Transcript

  1. Setting up CocoaPods • Install CocoaPods on your computer •

    gem install cocoapods • Create a new project in Xcode • Create a Podfile
  2. Setting up CocoaPods • Install the dependencies • pod install

    • Open YourProject.xcworkspace (not the YourProject.xcodeproject) -CocoaPods automatically sets up the workspace for you
  3. Kiwi Spec Template • You can create a normal file,

    delete the header, and Kiwi-ise the implementation file • Or.. • You can create an Xcode template that does all of this for you • https://github.com/jeffaburt/XcodeTemplates
  4. -Here is what gets generated for you -Note: I added

    the Kiwi import. You might already have this imported in a .pch file that way all spec files automatically have the Kiwi import
  5. Let’s check for failure • Add a failing test to

    make sure everything is set up and working properly
  6. -The test failed so we are good. If it were

    to pass, we would be in trouble (something might not have been set up properly) -Your first test: notice that since NO is not an object, we wrap it in a theValue() block. This is a Kiwi-specific macro -Also notice how readable the test is. The value NO should be YES (this is pretty dang understandable). Kiwi is based on rspec.
  7. -Let’s test a model class -Let’s just make sure that

    when the user is created, it is signed out by default
  8. -Let’s add a method called signIn, that sets _isSignedIn to

    YES -Our test checks that after calling signIn, _isSignedIn is equal to YES
  9. -But what if we want to explicitly check that isSignedIn

    gets set to YES during the method execution? It’s possible that something else set it to YES, which would invalidate our test -First, we need to give private readwrite access to isSignedIn -Second, we need to inform the spec file of the private header (only re-declare items you are testing. Sometimes you might refactor a public method so they call a few private methods, but it’s better to test the public method). Plus, if you do it right, you won’t have to create any new tests. -Instead of checking for the value of isSignedIn after the method is called, let’s check that setIsSignedIn gets called with YES as the parameter -Important: if the method expects 3 arguments, you must pass in 3 separate arguments into the withArguments: parameter. If not, you will get weird errors. Luckily, if you only care about one specific parameter, you can use Any() for the rest. -Finally, we need to call the signIn method
  10. -Context is a synonym for describe (it behaves the same

    way) -It’s used as a better way to organize tests -Our signIn method has been altered to only sign in the user if it is not already already signed in -We add two contexts that represent the two “if” statement outcomes: the user is not signed in yet and the user is already signed in -You will notice we used something called a stub. Basically, a stub returns a canned response that we can choose. There is no safe checking though - if you return an object that is not the valid type, the test will fail and may or may not be hard to determine why. -Stubs are handy when we don’t want to set everything up ahead of time by knowing which methods to call. In this case, we don’t have to call the signIn method to sign in the user for the test on line 52. Line 36 is redundant (the value should default to NO), but it’s okay to be completely explicit in a test. -You’ll notice on line 53, we do not include withArguments: since we want to make sure setIsSignedIn does not get called. We could test that it does not get called with theValue(NO) and theValue(YES), but this does the same thing and is less code to write -Note: stubs are also a good way to prevent methods from being called. You are not required to return a value. For example, you might have a sorting method but you aren’t testing that the objects get sorted. So you decide to stub it since it’s not really needed. But you don’t have to stub everything. Sometimes you might get weird issues though. Say you have validation when a user signs in that checks that they are 13 years old or older. Since we didn’t set anything up with a birthday here, you might get an exception which causes the test to fail. You aren’t testing validation here, so you decide to just ignore it. Test == passed (success)
  11. -Sometimes we want to test something in a completion block

    -Kiwi is thread safe, and we don’t have to explicitly call things on the main thread -I know, I know, integration testing is a whole different concept and we won’t hit the network with unit tests, but this is for demo purposes. More async examples that might make more sense: sorting, notification observers, pretty much anything else that runs on a background thread -shouldEventually should be used first, which has a default 1 second timeout, but it’s possible that the test suite will finish before the completion block gets called. In this case, setIsSignedIn to NO gets called after 3 seconds. We only have a couple tests that don’t even take 1 second to run. As a result, we have to stall the test suite a bit. 10 is a random number I threw in.
  12. -Say the signOut method does hit the network, but since

    we are running a unit test, we want to fake a response. -signOut was updated so that we only set isSignedOut to No if the network responses with YES (noted by the succeeded parameter) -Since we privately talk to the server (i.e. we don’t want other classes to talk to the server), we need to tell the spec file about the private method -Now, we want to set up a spy, which will capture the completion block passed in to the tellServerToSignOutWithCompletionBlock method -In the tests below, we are able to call the completion block directly, which completely bypasses the server -Note that we are calling the completionBlock with the succeeded parameter every time. This is a good way of ensuring you are setting up your contexts in a way that makes most sense. -Keeping your tests small is a good way to keep tests legible and not deter the next person who wants to add some more tests
  13. Values And Numerics • [[subject shouldNot] beNil] • [[subject should]

    beNil] • [[subject should] beIdenticalTo:(id)anObject] - compares id's • [[subject should] equal:(id)anObject] • [[subject should] equal:(double)aValue withDelta:(double)aDelta] • [[subject should] beWithin:(id)aDistance of:(id)aValue] • [[subject should] beLessThan:(id)aValue] • [[subject should] beLessThanOrEqualTo:(id)aValue] • [[subject should] beGreaterThan:(id)aValue] • [[subject should] beGreaterThanOrEqualTo:(id)aValue] • [[subject should] beBetween:(id)aLowerEndpoint and:(id)anUpperEndpoint] • [[subject should] beInTheIntervalFrom:(id)aLowerEndpoint to:(id)anUpperEndpoint] • [[subject should] beTrue] • [[subject should] beFalse] • [[subject should] beYes] • [[subject should] beNo] • [[subject should] beZero]
  14. Substring Matching • [[subject should] containString:(NSString*)substring] • [[subject should] containString:(NSString*)substring

    options: (NSStringCompareOptions)options] • [[subject should] startWithString:(NSString*)prefix] • [[subject should] endWithString:(NSString*)suffix] • [[@"Hello, world!" should] containString:@"world"]; • [[@"Hello, world!" should] containString:@"WORLD" options:NSCaseInsensitiveSearch]; • [[@"Hello, world!" should] startWithString:@"Hello,"]; • [[@"Hello, world!" should] endWithString:@"world!"];
  15. Regular-Expression • [[subject should] matchPattern: (NSString*)pattern] • [[subject should] matchPattern:

    (NSString*)pattern options: (NSRegularExpressionOptions)options] • [[@"ababab" should] matchPattern:@"(ab)+"]; • [[@" foo " shouldNot] matchPattern:@"^foo$"]; • [[@"abABab" should] matchPattern:@"(ab)+" options:NSRegularExpressionCaseInsensitive];
  16. Object Testing • [[subject should] beKindOfClass:(Class)aClass] • [[subject should] beMemberOfClass:

    (Class)aClass] • [[subject should] conformToProtocol:(Protocol *)aProtocol] • [[subject should] respondToSelector: (SEL)aSelector]
  17. Collections • [[subject should] beEmpty] • [[subject should] contain:(id)anObject] •

    [[subject should] containObjectsInArray:(NSArray *)anArray] • [[subject should] containObjects:(id)firstObject, ...] • [[subject should] haveCountOf:(NSUInteger)aCount] • [[subject should] haveCountOfAtLeast: (NSUInteger)aCount] • [[subject should] haveCountOfAtMost: (NSUInteger)aCount]
  18. Interactions and Messages • [[subject should] receive:(SEL)aSelector] • [[subject should]

    receive:(SEL)aSelector withCount:(NSUInteger)aCount] • [[subject should] receive:(SEL)aSelector withCountAtLeast: (NSUInteger)aCount] • [[subject should] receive:(SEL)aSelector withCountAtMost: (NSUInteger)aCount] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount: (NSUInteger)aCount] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast:(NSUInteger)aCount] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost:(NSUInteger)aCount]
  19. Interactions and Messages • [[subject should] receive:(SEL)aSelector withArguments:(id)firstArgument, ...] •

    [[subject should] receive:(SEL)aSelector withCount:(NSUInteger)aCount arguments: (id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector withCountAtLeast:(NSUInteger)aCount arguments:(id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector withCountAtMost:(NSUInteger)aCount arguments:(id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withArguments: (id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCount: (NSUInteger)aCount arguments:(id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtLeast: (NSUInteger)aCount arguments:(id)firstArgument, ...] • [[subject should] receive:(SEL)aSelector andReturn:(id)aValue withCountAtMost: (NSUInteger)aCount arguments:(id)firstArgument, ...]