diff options
Diffstat (limited to 'UnitTesting/GTMNSObject+UnitTesting.h')
-rw-r--r-- | UnitTesting/GTMNSObject+UnitTesting.h | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/UnitTesting/GTMNSObject+UnitTesting.h b/UnitTesting/GTMNSObject+UnitTesting.h new file mode 100644 index 0000000..73a2c9a --- /dev/null +++ b/UnitTesting/GTMNSObject+UnitTesting.h @@ -0,0 +1,534 @@ +// +// GTMNSObject+UnitTesting.h +// +// Utilities for doing advanced unittesting with objects. +// +// Copyright 2006-2008 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy +// of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. +// + +#include <Cocoa/Cocoa.h> + +/// Fails when image of |a1| does not equal image in TIFF file named |a2| +// +// Generates a failure when the unittest image of |a1| is not equal to the +// image stored in the TIFF file named |a2|, or |a2| does not exist in the +// executable code's bundle. +// If |a2| does not exist in the executable code's bundle, we save a TIFF +// representation of |a1| on the desktop with name |a2|. This can then be +// included in the bundle as the master to test against. +// If |a2| != |a1|, we save a TIFF representation of |a1| on the desktop +// with name |a2|_Failed so that we can compare the two files to see what +// has changed. +// See pathForTIFFNamed to see how name is searched for. +// Implemented as a macro to match the rest of the SenTest macros. +// +// Args: +// a1: The object to be checked. Must implement the -unitTestImage method. +// a2: The name of the TIFF file to check against. +// Do not include the extension +// description: A format string as in the printf() function. +// Can be nil or an empty string but must be present. +// ...: A variable number of arguments to the format string. Can be absent. +// +#define GTMAssertObjectImageEqualToTIFFNamed(a1, a2, description, ...) \ +do { \ + NSObject* a1Object = (a1); \ + NSString* a2String = (a2); \ + NSString *failString = nil; \ + BOOL isGood = [a1Object respondsToSelector:@selector(unitTestImage)]; \ + if (isGood) { \ + if (![a1Object areSystemSettingsValidForDoingImage]) { \ + break; \ + } \ + NSString *aPath = [a1Object pathForTIFFNamed:a2String]; \ + isGood = aPath != nil; \ + if (isGood) { \ + isGood = [a1Object compareWithTIFFAt:aPath]; \ + } \ + if (!isGood) { \ + if (aPath != nil) { \ + a2String = [a2String stringByAppendingString:@"_Failed"]; \ + } \ + BOOL aSaved = [a1Object saveToTIFFNamed:a2String]; \ + if (NO == aSaved) {\ + if (aPath == nil) { \ + failString = [NSString stringWithFormat:@"File %@ did not exist in bundle. Tried to save %@ to desktop and failed.", a2String, a2String]; \ + } else { \ + failString = [NSString stringWithFormat:@"Object image different than file %@. Tried to save to desktop as %@ and failed.", aPath, a2String]; \ + } \ + } else { \ + if (aPath == nil) { \ + failString = [NSString stringWithFormat:@"File %@ did not exist in bundle. Saved to ~/Desktop/%@", a2String, a2String]; \ + } else { \ + failString = [NSString stringWithFormat:@"Object image different than file %@. Saved image to desktop as %@.", aPath, a2String]; \ + } \ + } \ + } \ + } else { \ + failString = @"Object does not respond to -unitTestImage"; \ + } \ + if (!isGood) { \ + if (nil != description) { \ + STFail(@"%@: %@", STComposeString(description, ##__VA_ARGS__), failString); \ + } else { \ + STFail(@"%@", failString); \ + } \ + } \ +} while(0) + +/// Fails when state of |a1| does not equal state in file |a2| +// +// Generates a failure when the unittest state of |a1| is not equal to the +// state stored in the state file named |a2|, or |a2| does not exist in the +// executable code's bundle. +// If |a2| does not exist in the executable code's bundle, we save a state +// representation of |a1| on the desktop with name |a2|. This can then be +// included in the bundle as the master to test against. +// If |a2| != |a1|, we save a state representation of |a1| on the desktop +// with name |a2|_Failed so that we can compare the two files to see what +// has changed. +// Implemented as a macro to match the rest of the SenTest macros. +// +// Args: +// a1: The object to be checked. Must implement the -unitTestImage method. +// a2: The name of the state file to check against. +// Do not include the extension +// description: A format string as in the printf() function. +// Can be nil or an empty string but must be present. +// ...: A variable number of arguments to the format string. Can be absent. +// +#define GTMAssertObjectStateEqualToStateNamed(a1, a2, description, ...) \ +do { \ + NSObject* a1Object = (a1); \ + NSString* a2String = (a2); \ + NSString *failString = nil; \ + BOOL isGood = [a1Object respondsToSelector:@selector(unitTestEncodeState:)]; \ + if (isGood) { \ + NSString *aPath = [a1Object pathForStateNamed:a2String]; \ + isGood = aPath != nil; \ + if (isGood) { \ + isGood = [a1Object compareWithStateAt:aPath]; \ + } \ + if (!isGood) { \ + if (aPath != nil) { \ + a2String = [a2String stringByAppendingString:@"_Failed"]; \ + } \ + BOOL aSaved = [a1Object saveToStateNamed:a2String]; \ + if (NO == aSaved) {\ + if (aPath == nil) { \ + failString = [NSString stringWithFormat:@"File %@ did not exist in bundle. Tried to save %@ to desktop and failed.", a2String, a2String]; \ + } else { \ + failString = [NSString stringWithFormat:@"Object state different than file %@. Tried to save to desktop as %@ and failed.", aPath, a2String]; \ + } \ + } else { \ + if (aPath == nil) { \ + failString = [NSString stringWithFormat:@"File %@ did not exist in bundle. Saved to ~/Desktop/%@", a2String, a2String]; \ + } else { \ + failString = [NSString stringWithFormat:@"Object state different than file %@. Saved image to desktop as %@.", aPath, a2String]; \ + } \ + } \ + } \ + } else { \ + failString = @"Object does not respond to -unitTestEncodeState:"; \ + } \ + if (!isGood) { \ + if (nil != description) { \ + STFail(@"%@: %@", STComposeString(description, ##__VA_ARGS__), failString); \ + } else { \ + STFail(@"%@", failString); \ + } \ + } \ +} while(0) + +/// test both GTMAssertObjectImageEqualToTIFFNamed and GTMAssertObjectStateEqualToStateNamed +// +// Combines the above two macros into a single ubermacro for comparing +// both state and image. When only the best will do... +#define GTMAssertObjectEqualToStateAndImageNamed(a1, a2, description, ...) \ +do { \ + GTMAssertObjectImageEqualToTIFFNamed(a1, a2, description, ##__VA_ARGS__); \ + GTMAssertObjectStateEqualToStateNamed(a1, a2, description, ##__VA_ARGS__); \ +} while (0) + +/// Tests the setters and getters for exposed bindings +// For objects that expose bindings, this tests them for you, saving you from +// having to write a whole pile of set/get test code if you add binding support. +// You will need to implement valueClassForBinding: for your bindings, +// and you may possibly want to implement unitTestExposedBindingsToIgnore +// and unitTestExposedBindingsTestValues. See descriptions of those +// methods below for details. +// Implemented as a macro to match the rest of the SenTest macros. +// +// Args: +// a1: The object to be checked. +// description: A format string as in the printf() function. +// Can be nil or an empty string but must be present. +// ...: A variable number of arguments to the format string. Can be absent. +// +#define GTMTestExposedBindings(a1, description, ...) \ +do { \ + NSArray *bindings = [a1 exposedBindings]; \ + if (bindings) { \ + NSArray *bindingsToIgnore = [a1 unitTestExposedBindingsToIgnore]; \ + NSEnumerator *bindingsEnum = [bindings objectEnumerator]; \ + NSString *bindingKey; \ + while ((bindingKey = [bindingsEnum nextObject])) { \ + if (![bindingsToIgnore containsObject:bindingKey]) { \ + Class theClass = [a1 valueClassForBinding:bindingKey]; \ + STAssertNotNil(theClass, @"Should have valueClassForBinding %@", bindingKey); \ + NSDictionary *testValues = [a1 unitTestExposedBindingsTestValues:bindingKey]; \ + NSEnumerator *testEnum = [testValues keyEnumerator]; \ + id testValue; \ + while ((testValue = [testEnum nextObject])) { \ + [a1 setValue:testValue forKey:bindingKey]; \ + id value = [a1 valueForKey:bindingKey]; \ + STAssertEqualObjects([testValues objectForKey:testValue], value, description, ##__VA_ARGS__); \ + } \ + } \ + } \ + } \ +} while(0) + +/// \cond Protocols + +// GTMUnitTestingEncoding protocol is for objects which need to save their +// "state" for using with the unit testing categories +@protocol GTMUnitTestingEncoding +// Encodes the state of an object in a manner suitable for comparing +// against a master state file so we can determine whether the +// object is in a suitable state. Encode data in the coder in the same +// manner that you would encode data in any other Keyed NSCoder subclass. +// +// Arguments: +// inCoder - the coder to encode our state into +- (void)unitTestEncodeState:(NSCoder*)inCoder; +@end + +/// Category for saving and comparing object state and image for unit tests +// +// The GTMUnitTestAdditions category gives object the ability to store their +// state for use in unittesting in two different manners. +// 1) Objects can elect to save their "image" as a TIFF that we can compare at +// runtime to a TIFF on file to make sure that the representation hasn't +// changed. All views and Windows can save their image. In the case of Windows, +// they are "bluescreened" so that any transparent areas can be compared between +// machines. For this to work, the appearance must be set to "Aqua blue" In the +// case of NSWindows and NSScreens, we do a screen capture operation to capture +// their image. In these cases, font smoothing settings must be set consistently +// across machines. The current standard is +// Font Smoothing Style: Standard - Best For CRT +// Turn Off Text Smoothing For Font Sizes: 8 And Smaller +// If you do not have these settings, any unit tests depending on them will not +// be executed, and a warning will be logged. +// Also, we need to be careful about avoiding ColorSync. In most cases the +// unittesting system handles this for you. If you are running into troubles +// make sure that you are using device colors, and not calibrated colors +// wherever you are doing drawing. +// 2) Objects can elect to save their "state". State is the attributes that we +// want to verify when running unit tests. Applications, Windows, Views, +// Controls and Cells currently return a variety of state information. If you +// want to customize the state information that a particular object returns, you +// can do it via the GTMUnitTestingEncodedObjectNotification. Items that have +// delegates (Applications/Windows) can also have their delegates return state +// information if appropriate via the unitTestEncoderDidEncode:inCoder: delegate +// method. +// To compare state/image in your unit tests, you can use the three macros above +// GTMAssertObjectStateEqualToStateNamed, GTMAssertObjectImageEqualToTIFFNamed and +// GTMAssertObjectEqualToStateAndImageNamed. +@interface NSObject (GTMUnitTestingAdditions) <GTMUnitTestingEncoding> +/// Returns an image containing a representation suitable for use in comparing against a master image. +// +// NB this means that all colors should be +// device based, as colorsynced colors will be different on different devices. +// +// Returns: +// an image of the object +- (NSImage*)unitTestImage; + +/// Checks to see that system settings are valid for doing an image comparison. +// The main issue is that we make sure that we are set to using Blue Aqua as +// our appearance. +// Instead of directly overriding this, a unit test can just use: +// needsAquaBlueAppearanceForDoingImage +// needsScrollBarArrowsLowerRightForDoingImage +// to enable those tests w/in this base implementation. +// The other issues are for NSScreen and NSWindow images as they are affected by +// the font smoothing settings in the system preferences. For things to work +// these settings must be set to: +// Font Smoothing Style: Standard - Best For CRT +// Turn Off Text Smoothing For Font Sizes: 8 And Smaller +// +// Returns: +// YES if we can do image comparisons for this object type. +- (BOOL)areSystemSettingsValidForDoingImage; + +/// Checks if this test needs the AquaBlue Appearance for doing the image comparison. +// If the test uses the appearance colors, this should be overriden to return +// YES (ie-default is no). This provides a hook so the unittest can be skipped +// if the running user's settings aren't the "standard" for the UI unitttests. +// +// Returns: +// YES if this test needs the AquaBlue Appearance. +- (BOOL)needsAquaBlueAppearanceForDoingImage; + +/// Checks if this test needs the ScrollBarArrows LowerRight for doing the image comparison. +// If the test uses the scrollbar drawing, this should be overriden to return +// YES (ie-default is no). This provides a hook so the unittest can be skipped +// if the running user's settings aren't the "standard" for the UI unitttests. +// +// Returns: +// YES if this test needs the ScrollBarArrows LowerRight. +- (BOOL)needsScrollBarArrowsLowerRightForDoingImage; + +/// Save the unitTestImage to a TIFF file with name |name| at ~/Desktop/|name|.tif. +// The TIFF will be compressed with LZW. +// +// Args: +// name: The name for the TIFF file you would like saved. +// +// Returns: +// YES if the file was successfully saved. +// +- (BOOL)saveToTIFFNamed:(NSString*)name; + +/// Save unitTestImage of |self| to a TIFF file at path |path|. +// The TIFF will be compressed with LZW. All non-drawn areas will be transparent. +// +// Args: +// name: The name for the TIFF file you would like saved. +// +// Returns: +// YES if the file was successfully saved. +// +- (BOOL)saveToTIFFAt:(NSString*)path; + +/// Compares unitTestImage of |self| to the TIFF located at |path| +// +// Args: +// path: the path to the TIFF file you want to compare against. +// +// Returns: +// YES if they are equal, NO is they are not +// +- (BOOL)compareWithTIFFNamed:(NSString*)name; + +/// Compares unitTestImage of |self| to the TIFF located at |path| +// +// Args: +// path: the path to the TIFF file you want to compare against. +// +// Returns: +// YES if they are equal, NO is they are not +// +- (BOOL)compareWithTIFFAt:(NSString*)path; + +/// Find the path for a TIFF by name in your bundle. +// Searches for the following: +// "name.tif", +// "name.arch.tif", +// "name.arch.OSVersionMajor.tif" +// "name.arch.OSVersionMajor.OSVersionMinor.tif" +// "name.arch.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.tif" +// "name.arch.OSVersionMajor.tif" +// "name.OSVersionMajor.arch.tif" +// "name.OSVersionMajor.OSVersionMinor.arch.tif" +// "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.arch.tif" +// "name.OSVersionMajor.tif" +// "name.OSVersionMajor.OSVersionMinor.tif" +// "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.tif" +// Do not include the ".tif" extension on your name. +// +// Args: +// name: The name for the TIFF file you would like to find. +// +// Returns: +// the path if the TIFF exists in your bundle +// or nil if no TIFF to be found +// +- (NSString *)pathForTIFFNamed:(NSString*)name; + + +/// Gives us a LZW compressed representation of unitTestImage of |self|. +// +// Returns: +// a LZW compressed TIFF if successful +// nil if failed +// +- (NSData *)TIFFRepresentation; + + +/// Save the encoded unit test state to a .gtmUTState file with name |name| at ~/Desktop/|name|.gtmUTState. +// +// Args: +// name: The name for the state file you would like saved. +// +// Returns: +// YES if the file was successfully saved. +// +- (BOOL)saveToStateNamed:(NSString*)name; + +/// Save encoded unit test state of |self| to a .gtmUTState file at path |path|. +// +// Args: +// name: The name for the state file you would like saved. +// +// Returns: +// YES if the file was successfully saved. +// +- (BOOL)saveToStateAt:(NSString*)path; + +/// Compares encoded unit test state of |self| to the .gtmUTState named |name| +// +// Args: +// name: the name of the state file you want to compare against. +// +// Returns: +// YES if they are equal, NO is they are not +// +- (BOOL)compareWithStateNamed:(NSString*)name; + +/// Compares encoded unit test state of |self| to the .gtmUTState located at |path|. +// +// Args: +// path: the path to the state file you want to compare against. +// +// Returns: +// YES if they are equal, NO is they are not +// +- (BOOL)compareWithStateAt:(NSString*)path; + +/// Find the path for a state by name in your bundle. +// Searches for: +// "name.gtmUTState", +// "name.arch.gtmUTState", +// "name.arch.OSVersionMajor.gtmUTState" +// "name.arch.OSVersionMajor.OSVersionMinor.gtmUTState" +// "name.arch.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.gtmUTState" +// "name.arch.OSVersionMajor.gtmUTState" +// "name.OSVersionMajor.arch.gtmUTState" +// "name.OSVersionMajor.OSVersionMinor.arch.gtmUTState" +// "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.arch.gtmUTState" +// "name.OSVersionMajor.gtmUTState" +// "name.OSVersionMajor.OSVersionMinor.gtmUTState" +// "name.OSVersionMajor.OSVersionMinor.OSVersion.bugfix.gtmUTState" +// Do not include the ".gtmUTState" extension on your name. +// +// Args: +// name: The name for the state file you would like to find. +// +// Returns: +// the path if the state exists in your bundle +// or nil if no state to be found +// +- (NSString *)pathForStateNamed:(NSString*)name; + + +/// Gives us the encoded unit test state for |self| +// +// Returns: +// the encoded state if successful +// nil if failed +// +- (NSDictionary *)stateRepresentation; + + +/// Encodes the state of an object +// Encodes the state of an object in a manner suitable for comparing +// against a master state file so we can determine whether the +// object is in a suitable state. Encode data in the coder in the same +// manner that you would encode data in any other Keyed NSCoder subclass. +// +// Arguments: +// inCoder - the coder to encode our state into +- (void)unitTestEncodeState:(NSCoder*)inCoder; + +/// Allows you to ignore certain bindings when running GTMTestExposedBindings +// If you have bindings you want to ignore, add them to the array returned +// by this method. The standard way to implement this would be: +// - (NSMutableArray*)unitTestExposedBindingsToIgnore { +// NSMutableArray *array = [super unitTestExposedBindingsToIgnore]; +// [array addObject:@"bindingToIgnore1"]; +// ... +// return array; +// } +// The NSObject implementation by default will ignore NSFontBoldBinding, +// NSFontFamilyNameBinding, NSFontItalicBinding, NSFontNameBinding and +// NSFontSizeBinding if your exposed bindings contains NSFontBinding because +// the NSFont*Bindings are NOT KVC/KVO compliant, and they just happen to work +// through what can only be described as magic :) +- (NSMutableArray*)unitTestExposedBindingsToIgnore; + +/// Allows you to set up test values for your different bindings. +// if you have certain values you want to test against your bindings, add +// them to the dictionary returned by this method. The dictionary is a "value" key +// and an "expected return" object. +// The standard way to implement this would be: +// - (NSMutableDictionary*)unitTestExposedBindingsTestValues:(NSString*)binding { +// NSMutableDictionary *dict = [super unitTestExposedBindingsTestValues:binding]; +// if ([binding isEqualToString:@"myBinding"]) { +// [dict setObject:[[[MySpecialBindingValueSet alloc] init] autorelease] +// forKey:[[[MySpecialBindingValueGet alloc] init] autorelease]]; +// [dict setObjectAndKey:[[[MySpecialBindingValue alloc] init] autorelease]]; +// ... +// else if ([binding isEqualToString:@"myBinding2"]) { +// ... +// } +// return array; +// } +// The NSObject implementation handles many of the default bindings, and +// gives you a reasonable set of test values to start. +// See the implementation for the current list of bindings, and values that we +// set for those bindings. +- (NSMutableDictionary*)unitTestExposedBindingsTestValues:(NSString*)binding; +@end + +// Utility for simplifying unitTestExposedBindingsTestValues implementations +@interface NSMutableDictionary (GTMUnitTestingAdditions) +// Sets an object and a key to the same value in a dictionary. +- (void)setObjectAndKey:(id)objectAndKey; +@end + +// Informal protocol for delegates that wanst to be able to add state info +// when state info is collected for their "owned" objects +@interface NSObject (GTMUnitTestingEncodingAdditions) +// Delegate function for unit test objects that have delegates. Delegates have +// the option of encoding more data into the coder to store their state for +// unittest usage. +- (void)unitTestEncoderDidEncode:(id)sender inCoder:(NSCoder*)inCoder; +@end + +/// \endcond + +// Whenever an object is encoded by the unit test encoder, it send out a +// notification so that objects who want to add data to the encoded objects unit +// test state can do so. The Coder will be in the userInfo dictionary for the +// notification under the GTMUnitTestingEncoderKey key. +extern NSString *const GTMUnitTestingEncodedObjectNotification; + +// Key for finding the encoder in the userInfo dictionary for +// GTMUnitTestingEncodedObjectNotification notifications. +extern NSString *const GTMUnitTestingEncoderKey; + + +/// Support for Pulse automated builds +@interface NSObject (GTMUnitTestingPulseAdditions) + +// Determine if the current unittest is running under Pulse +- (BOOL)isRunningUnderPulse; + +// Get the current base directory for Pulse +- (NSString *)pulseBaseDirectory; + +@end |