这是一篇xcode单元测试入门的文章。有空了给各位翻出来方便大家看。
This is a post by iOS Tutorial Team member
Unit testing is great because it makes your life easier. Easier to deliver high quality code, and easier to make changes without fear of breaking something!
But what might not be so easy is getting started if you’re new to unit testing – and that’s what this tutorial is all about!
We’ll cover how to set up Xcode to use three different unit testing frameworks:
We won’t cover how to actually write test cases in this tutorial, but don’t worry – I’ll be covering that in my upcoming tutorial series on Test Driven Development for iOS!
This tutorial assumes you already know the basics of iOS development. If you are a complete beginner, check out some of these Beginner iOS Tutorials first.
Getting Started with OCUnit
OCUnit is the unit testing framework that’s built straight into Xcode, so let’s try that out first.
In Xcode, go to File\New\New Project, select iOS\Application\View-based Application, and click Next. Name the project SampleProject, and make sure to check the Include Unit Tests option, as shown below.
Click Next, choose a folder for your project, and click Create.
If you look at the generated project, you’ll see that Xcode has created two targets for you: SampleProject (the app target), and SampleProjectTests (the unit test target).
The unit test target is created with a dependency on the app target, so that when you run the tests, the app target will automatically be built.
Xcode also creates a single test class as an example for you, which you can find in SampleProjectTests\SampleProjectTests.m. You’ll see a single test case set up in the file that looks like this:
- (void)testExample { STFail(@"Unit tests are not implemented yet in SampleProjectTests"); } |
Basically this is a sample test that should fail immediately when it’s run, because you haven’t written any unit tests yet!
Let’s try this out and see if it indeed fails like it should. Select the iPhone Simulator from the Scheme drop down, then choose Product\Test from the Xcode menu (shortcut key ⌘U).
Xcode will then build the app target, then build the unit test target. If both targets can be built it then runs the test cases on the simulator and highlights any failures in the Issue Navigator and in the source file itself, just like it does with build warnings and errors.
So as you can see, setting up unit testing with OCUnit in Xcode is really easy – it’s just a matter of selecting a checkbox!
We won’t get into how to use write unit tests with OCUnit in this tutorial series, since I prefer the alternative unit testing frameworks GHUnit and OCMock which we’ll cover next.
However, if you decide OCUnit is right for you, check out Apple’s Unit Testing Overview for more information on how you can write your own tests.
GHUnit vs OCUnit
GHUnit is a popular unit testing framework developed by Gabriel Handford as an alternative to OCUnit. With the release of Xcode 4, using OCUnit is better than it used to be, but I still prefer GHUnit because:
OCUnit does still have the advantage of being tightly integrated with Xcode, which makes the initial project setup easier, but to me the advantages of GHUnit outweigh this.
If you’re interested in reading more about the differences between GHUnit and OCUnit, check out this nice comparison by Mark Makdad.
Introducing OCMock
Unit testing without OCMock.
Image credit: ettina82
Before we cover how to integrate GHUnit into your Xcode project, let’s take a minute to discuss OCMock.
If you have ever written automated unit tests before, you probably have encountered the problem of trying to test more than one class at a time.
This is a recipe for brittle tests and spaghetti code. After a while, you get to the point that you’re ready to throw the tangled mess into the trash!
Using some form of dummy objects (also known as mock objects) to reduce dependencies is a good method to solve this.
Mock objects allow you to test interactions with the outside world while keeping external dependencies as low as possible. If your code has external dependencies or responsibilities (and most do), you’ll want to use these!
OCMock is a framework for OS X and iOS developed by Mulle Kybernetik that follows the pattern of mock frameworks developed for other platforms.
So you’ll learn how to set up Xcode to use this along with GHUnit!
Installing GHUnit and OCMock: Overview
These instructions will create a project structure with all files stored in the actual project directory, including the GHUnit and OCMock frameworks. These instructions also assume everything we create or download will be in the directory ~/myproj, but feel free to put them wherever you like.
After each step in the process, you should do a clean, build, and run for each target to validate the configuration and dependencies.
We will use the name MyProj for our project and derive several targets and file names from it. You can choose whatever name you wish, but it is best to be consistent in your naming of targets and components.
There are many more options for GHUnit and OCMock, but they are outside the scope of these instructions. For more information, see the Other Links at the end of this post.
Getting Started
Let’s get started by creating a fresh directory and project:
Integrating GHUnit
OK, time to integrate GHUnit! It just takes three quick steps.
1) Add a GHUnit Test Target
First, we need to add a test target to our project for GHUnit. Select the project file in the Project Navigator view, and click ‘Add Target’.
Create an iOS View-based Application named ‘MyProjTests’ for the GHUnit target. As with the project creation, leave the “Include Unit Tests” checkbox unchecked.
2) Add GHUnitIOS Framework
The next step is to add the GHUnitIOS framework to our project. You’ll need to download it from github first if you haven’t already.
Note: Be sure you select the latest iOS version (named like ‘GHUnitIOS-####.zip’), and not the Mac OS X version (named like ‘GHUnit-####.zip’)!
We want to include the full set of files in our project, so unzip it into ~/myproj/MyProj.
Then select the “Build Phases” tab for the MyProjTests target, expand the “Link Binary With Libraries” section, and click the “+” button. Select “Add Other…” and select the GHUnitIOS.framework from the ~/myproj directory, as shown in the screenshot below.
Now verify that the test target builds before continuing. Select the iPhone Simulator for the MyProjTests target from the Scheme drop down and click the Run button. You will only see a blank view at this point in the simulator.
3) Configure GHUnit Test Target
The GHUnitIOS framework has an embedded window and app delegate, so we need to remove the ones installed by default when we created the test target.
First, delete the files MyProjTestsAppDelegate.h, MyProjTestsAppDelegate.m, MainWindow.xib, MyProjTestsViewController.h, MyProjTestsViewController.m, MyProjTestsViewController.xib, and main.m from MyProjTests.
Second, remove the “Main nib file base name” property from MyProjTests-Info.plist.
Next, download the file GHUnitIOSTestMain.m to ~/myproj/MyProj/MyProjTests, as shown in the screenshot below.
Now, add the file to the MyProjTests target, making sure to select the MyProjTests target when adding the file.
Lastly, we need to update the build settings for the MyProjTests target. Select “Other Linker Flags” under MyProjTests and add the value “-ObjC -all_load”.
Congrats – you’ve successfully configured GHUnit! Run the MyProjTests target in the simulator and you’ll see the GHUnit test runner:
Creating a Simple Test Case
Although GHUnit is integrated in your project now, you don’t have any tests to run yet! So let’s make a simple test case to try things out.
Create a new Objective-C class SampleTestCase.m in the MyProjTests group, making sure it belongs to the MyProjTests target, but not to the MyProj target.
Our test case class does not need a header file, but Xcode 4 does not give us the option to only create a .m file, so delete the SampleTestCase.h file and replace the entire contents of the SampleTestCase.m with the following code:
#import <GHUnitIOS/GHUnit.h>
@interface SampleLibTest : GHTestCase { } @end
@implementation SampleLibTest
- (void)testSimplePass { // Another test }
- (void)testSimpleFail { GHAssertTrue(NO, nil); }
@end |
Then run the MyProjTests target in the simulator and you should see this:
You can tap the Run button in the upper right corner to run the tests – one should fail and one should succeed.
Creating a test case in GHUnit is straightforward, a test case is simply a method that follows these simple rules:
Obviously, to be a useful test case, it needs to do something (unlike the ‘testSimplePass’ method above.)
Each test case should have a single purpose, and often after all the setup is done it only checks a single value. It can be as simple as creating an instance of the class to be tested, calling a method on that class, and checking that the value returned matches your expectations.
Test cases specify what they expect by asserting certain conditions, and in GHUnit, there is a set of macros that cover a variety of cases in the file GHTestMacros.h. There are a few simple examples on the GHUnit website.
w00t! At this point you now have a working project using GHUnit on which you can build. Read on to learn how to add in OCMock support as well!
Addding OCMock
Now that we have GHUnit set up and running, we want to add support for OCMock. The framework available on the website does not work with iOS projects. We need the static library from the the example iPhone project and the header files from the framework.
Before we get started, go ahead and create a separate directory in our project to hold the library and header files.
From the Project Navigator in Xcode, right click on MyProj and select “Add Files to ‘MyProj’”:
Select the directory ~/myproj/MyProj, and click the “New Folder” button. Add a folder named “Libraries” and click the “Create” button. Once the folder is created, click ‘Add’ to add it to the target.
Now that we have a folder ready to add OCMock into, let’s get started! Again, adding OCMock into your project is just three steps.
1) Install latest OCMock iOS library
First download the file libOCMock.a to the ~/myproj/Libraries directory.
Then right click on ‘Libraries’ folder in the Project Navigator, and add the libOCMock.a file to the MyProjTests target.
2) Extract header files from OCMock framework
Download the latest ocmock dmg file from download directory to the ~/myproj directory and open it in finder. It should mount the image “OCMock #.##” in finder.
Select the directory ‘Release/Library/Headers/OCMock’ directory and add it to our project ‘Libraries’ folder (for the MyProjTests target).
The end result in the Project Navigator should look like this:
3) Update build settings for MyProjTests
The following linker settings depend on the fact that the above step created a physical directory ‘OCMock’ under our directory ‘Libraries’.
In the “Build Settings” for the MyProjTests target, make sure the “Library Search Paths” contain the “$(SRCROOT)/Libraries” directory like this:
Next, we need to add “$(SRCROOT)/Libraries” to the “Header Search Paths” item. Make sure you check the ‘Recursive’ checkbox as well:
Simple Test Case with OCMock
Now we need to add some test cases that use OCMock to our SampleTestCase. Update SampleTestCase.m to look like this:
#import <GHUnitIOS/GHUnit.h> #import <OCMock/OCMock.h>
@interface SampleLibTest : GHTestCase { } @end
@implementation SampleLibTest
- (void)testSimplePass { // Another test }
- (void)testSimpleFail { GHAssertTrue(NO, nil); }
// simple test to ensure building, linking, // and running test case works in the project - (void)testOCMockPass { id mock = [OCMockObject mockForClass:NSString.class]; [[[mock stub] andReturn:@"mocktest"] lowercaseString];
NSString *returnValue = [mock lowercaseString]; GHAssertEqualObjects(@"mocktest", returnValue, @"Should have returned the expected string."); }
- (void)testOCMockFail { id mock = [OCMockObject mockForClass:NSString.class]; [[[mock stub] andReturn:@"mocktest"] lowercaseString];
NSString *returnValue = [mock lowercaseString]; GHAssertEqualObjects(@"thisIsTheWrongValueToCheck", returnValue, @"Should have returned the expected string."); }
@end |
And finally, run the MyProjTests target in the simulator and it should look like this:
The tests we added that use OCMock merely demonstrate we have the project configured correctly. They do not show the usefulness of mock objects or the OCMock framework.
Mock objects are useful in testing that a class interacts with other objects properly, without the need to configure a large object hierarchy, and in some cases before a class is even implemented.
Where To Go From Here?
Here is the sample project with all of the code from the above tutorial.
Congratulations, now you know how to integrate OCUnit, GHUnit, and OCMock into your Xcode project!
However, this tutorial did not touch on how to make good use of unit tests in your iOS project, but I will soon be releasing a tutorial series on Test Driven Development for iOS, so stay tuned!
In the meantime, if you would like more information about GHUnit or OCMock, one of these links might help.