Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 13 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
13
Dung lượng
3,86 MB
Nội dung
560 ❘ CHAPTER 20 UNIT TESTING CREATING A UNIT TEST Once you have your unit test target created and confi gured, adding unit tests is simple. Here are the basic steps: 1. Create a unit test class and add its source fi le to the unit test target. 2. Add test methods to the class. 3. Register the tests with the unit testing framework. Unit test class fi les can go anywhere in your project, but I suggest, at the very least, creating a group for them named “ Tests ” or “ Unit Tests. ” In a larger project you might organize your unit test fi les in a folder, or you might group them together with the code that they test. The choice is yours. Each class that you create defi nes a group of tests. Each test is defi ned by a test method added to the class. A class can contain as many different tests as you desire, but must contain at least one. How you organize your tests is entirely up to you, but good practices dictate that a test class should limit itself to testing some functional unit of your code. It could test a single class or a set of related functions in your application. Once defi ned, your tests must be registered with the unit testing framework so that it knows what tests to run. For Objective - C tests this happens automatically. Objective - C test methods must adhere to a simple naming scheme — basically they must all begin with the name “ test. ” Objective - C introspection is then used to locate and run all of the tests you defi ned. For C++ unit tests, you add a declaration for each test you ’ ve written. The exact requirements for each are described in the “ Objective - C Tests ” and “ C++ Test Registration ” sections, respectively. Each test method should perform its test and return. Macros are provided for checking the expectations of each test and reporting failures. A test is successful if it completes all of its tests and returns normally. An example test is shown in Listing 20 - 2. LISTING 20 - 2: Sample C++ unit test void SieveOfEratosthenesTests::testPrimes( ) { // Test a number of known primes static int knownPrimes[] = { 2, 3, 5, 11, 503, 977, 12347, 439357, 101631947 }; SieveOfEratosthenes testSieve(UNIT_TEST_MAX_PRIMES); for (size_t i=0; i < sizeof(knownPrimes)/sizeof(int); i++) CPTAssert(testSieve.isPrime(knownPrimes[i])); } In this example, the testPrime function defi nes one test in the SieveOfEratosthenesTests class. The test creates an instance of the SieveOfEratosthenes class, and then checks to see that it correctly identifi es a series of numbers known to be prime. If all of the calls to testSieve.isPrime() return true , the test is successful; the testPrimes object is destroyed and the function returns. If any call to testSieve.isPrime() returns false , the CPTAssert macro signals to the testing framework that the test failed. The testing macros are described in the “ Objective - C Test Macros ” and “ C++ Test Macros ” sections. c20.indd 560c20.indd 560 1/22/10 4:17:08 PM1/22/10 4:17:08 PM Download at getcoolebook.com Common Test Initialization If a group of tests — defi ned as all of the tests in a TestCase class — deal with a similar set of data or environment, the construction and destruction of that data can be placed in two special methods: setUp and tearDown . The setUp method is called before each test is started, and the tearDown method is called after each test is fi nished. Override these methods if you want to initialize values or create common data structures that all, or at least two, of the tests will use. The typical use for setUp and tearDown is to create a working instance of an object that the tests will exercise, as illustrated in Listing 20 - 3. The test class defi nes a single instance variable that is initialized by setUp and destroyed by tearDown . Each test is free to use the object in the instance variable as the subject of its tests. For Objective - C tests, the methods your test class should override are - (void)setUp and - (void)teardown . For C/C++ tests, the functions to override are void TestCase::setup() and void TestCase::tearDown() . LISTING 20 - 3: Objective - C unit test using setUp and tearDown SieveOfEratosthenesTests.h #define UNIT_TEST_MAX_PRIMES 100000 @interface SieveOfEratosthenesTests : SenTestCase { SieveOfEratosthenes* testSieve; } SieveOfEratosthenesTests.m @implementation SieveOfEratosthenesTests - (void)setUp { testSieve = [[SieveOfEratosthenes alloc] init:UNIT_TEST_MAX_PRIMES]; } - (void)tearDown { [testSieve release]; testSieve = nil; } - (void)testInvalidNumbers { // These should all return NO STAssertFalse([testSieve isPrimeInMap:-1], @"-1 is not a prime number"); STAssertFalse([testSieve isPrimeInMap:0], @"0 is not a prime number"); STAssertFalse([testSieve isPrimeInMap:1], @"1 is not a prime number"); } Creating a Unit Test ❘ 561 c20.indd 561c20.indd 561 1/22/10 4:17:09 PM1/22/10 4:17:09 PM Download at getcoolebook.com 562 ❘ CHAPTER 20 UNIT TESTING The setUp and tearDown methods are called before and after every test. This allows tests to perform destructive tests on the object — that is, tests that alter the object ’ s state — because the object will be destroyed at the end of the test and re - created anew before the next test is run. Unit test classes have standard constructor and destructor methods. Do not use the constructor to create test data . If your test structures are expensive to create and destroy, you may be tempted to create them in the constructor and let them persist for the duration of the test class. Don ’ t do this. Your next approach might be to turn the setUp method into a factory that creates a singleton object when called the fi rst time. Sorry, but that probably won ’ t work either. Some testing frameworks create a separate instance of the test class for each test. Instead, make a single test that creates the expensive object and then calls a series of subtests itself. Remember not to name your Objective - C subtests “ test … ” or the testing framework will run them again. Because so many of the minor details of creating tests for Objective - C and C/C++ differ, the steps for creating your own tests have been separated into the following two sections, “ Objective - C Tests ” and “ C++ Tests. ” Objective - C Tests To create an Objective - C test class and add it to a unit test target, start by selecting the File ➪ New File command. The new fi le assistant presents a list of new fi le templates. Choose the Objective - C Test Case Class, as shown in Figure 20 - 3, from either the Cocoa Class or Cocoa Touch Class group, as appropriate. FIGURE 20 3 c20.indd 562c20.indd 562 1/22/10 4:17:10 PM1/22/10 4:17:10 PM Download at getcoolebook.com Click the Next button and give the test case class and fi le a name. The name should be descriptive of its purposes, such as StudentTests for a set of tests that validate the Student class. Make sure you create a matching .h fi le. Select the working project and add the test to the desired unit test target, as shown in Figure 20 - 4, making sure you don ’ t include the test class fi le in any other target. Click the Finish button. FIGURE 20 - 4 Xcode creates a skeletal test class defi nition and implementation, similar to the one shown in Listing 20 - 4. All of your test classes for Objective - C must be direct subclasses of the SenTestCase class. LISTING 20 - 4: Example Objective - C test case #import < SenTestingKit/SenTestingKit.h > @interface StudentTests : SenTestCase { } @end Xcode has created the framework for your class and has already added it to your unit test target. The only thing you need to do now is to write one or more test methods. Each test method: Creating a Unit Test ❘ 563 c20.indd 563c20.indd 563 1/22/10 4:17:10 PM1/22/10 4:17:10 PM Download at getcoolebook.com 564 ❘ CHAPTER 20 UNIT TESTING Must begin with “ test ” in lowercase, as in - testNegativeCoordinates Must return void Must not take any parameters An example of three such tests is shown in Listing 20 - 5. LISTING 20 - 5: Example Objective - C tests #import "StudentTests.h" @implementation StudentTests - (void)setUp { student = [[Student alloc] initWithName:@"Jane Doe"]; STAssertNotNil(student,@"Unable to create Student"); } - (void)tearDown { [student release]; student = nil; } - (void)testNameConstructor; { STAssertTrue([[student name] isEqualToString:@"Jane Doe"], @"Student.name property incorrect"); } - (void)testNamePartsParsing; { STAssertTrue([[student firstName] isEqualToString:@"Jane"], @"Student.firstName parsing incorrect"); STAssertTrue([[student lastName] isEqualToString:@"Doe"], @"Student.lastName parsing incorrect"); } - (void)testNamePartsReplacement; { STAssertTrue([[student name] isEqualToString:@"Jane Doe"], @"Student.name property incorrect"); [student setFirstName:@"John"]; STAssertTrue([[student name] isEqualToString:@"John Doe"], @"Student.name first name replacement incorrect"); [student setLastName:@"Smith"]; STAssertTrue([[student name] isEqualToString:@"John Smith"], @"Student.name last name replacement incorrect"); } @end ➤ ➤ ➤ c20.indd 564c20.indd 564 1/22/10 4:17:11 PM1/22/10 4:17:11 PM Download at getcoolebook.com Amazingly, you are all done. The introspective nature of Objective - C allows the unit test framework to discover automatically all classes that are subclasses of SenTestCase in the bundle, and then fi nd all void methods that begin with the name “ test. ” The unit test framework creates your test object and then executes each test method one at a time. Objective - C Test Macros When you write your test, you will employ a set of macros to evaluate the success of each test. If the assertion in the macro fails to meet the expectation, the test fails and a signal with a description of the failure is passed back to the unit test framework. Test failures appear as an error in the build log. Each macro accepts a description of the failure. The description argument is a Core Foundation format string that may be followed by a variable number of arguments, à la NSLog(description, ) or - [NSString stringWithFormat:format, ] . The unit test macros available are listed in the following table. The STFail macro unconditionally records a failed test. Use it in a block of code where the program fl ow has already determined that a failure has occurred. All of the other macros are assertion macros. The test is successful if the parameters meet the expectations of the assertion. If they do not, a failure is recorded using the description constructed using the format string ( @ ” ” in the table) and the remaining arguments. UNIT TEST ASSERTION MACRO DESCRIPTION STFail(@ ” ” , ) This is the basic macro for unconditionally recording a test failure. This macro always causes the described failure to be recorded. STAssertTrue(expression,@ ” ” , ) The test is successful if the statement expression evaluates to YES . Otherwise, a description of the failed test is logged. STAssertFalse(expression,@ ” ” , ) The test is successful if the statement expression evaluates to NO . STAssertNil(reference,@ ” ” , ) The test is successful if the statement reference evaluates to nil . STAssertNotNil(reference,@ ” ” , ) The test is successful if the statement reference evaluates to something other than nil . STAssertEquals(left,right,@ ” ” , ) The test is successful if the numeric value of the statement left equals the numeric value of the statement right . Both statements must evaluate to the same primitive type. That is, they must both be long int, fl oat, and so on. You may cast them if necessary. If the values are not the same type or value, the test fails. continues Creating a Unit Test ❘ 565 c20.indd 565c20.indd 565 1/22/10 4:17:12 PM1/22/10 4:17:12 PM Download at getcoolebook.com 566 ❘ CHAPTER 20 UNIT TESTING UNIT TEST ASSERTION MACRO DESCRIPTION STAssertEqualsWithAccuracy(left,right, accuracy,@ ” ” , ) The test is successful if the absolute di erence between the numeric value of the left statement and the numeric value of the right statement is equal to or less than the value of accuracy . Both left and right must evaluate to the same primitive type. If the values di er by more than accuracy or are not the same type, the test fails. STAssertEqualObjects(left,right,@ ” ” , ) The test is successful if the object reference in the left statement is equal to the object reference in the right statement, according to the [left isEqual:right] method. Both object references must be of the same type and the isEqual method must return normally with a Boolean result. If the object references are not the same type, the isEqual method returns NO , or the isEqual method throws an exception, the test fails. STAssertThrows(statement,@ ” ” , ) The test is successful if statement causes an exception to be thrown. STAssertThrowsSpecific(statement,class, @ ” ” , ) The test is successful if statement causes an exception of the class class to be thrown. STAssertThrowsSpecificNamed(statement, class,name,@ ” ” , ) The test is successful if the statement causes an exception of class with the name exception name to be thrown. STAssertNoThrow(statement,@ ” ” , ) The test is successful if statement does not cause an exception to be thrown. STAssertNoThrowSpecific(statement, class,@ ” ” , ) The test is successful if statement does not cause an exception of class to be thrown. Note that the test is still successful if the statement causes some other class of exception to be thrown. STAssertThrowsSpecificNamed(statement, class,name,@ ” ” , ) The test is successful if statement does not cause an exception of class with the name exception name to be thrown. After you have added your tests, you are ready to build the unit test target. (continued) c20.indd 566 c20.indd 566 1/22/10 4:17:12 PM1/22/10 4:17:12 PM Download at getcoolebook.com C++ Tests To create a C++ test class, follow the instructions for adding an Objective - C Test Case Class to your project, with the one exception that you ’ ll start by choosing the C++ Test Case Class template from the Carbon group. Once you ’ ve selected the correct targets and added the class fi les to the project, return here. Even if your application is written in pure C, the C/C++ testing framework still requires C++ objects to defi ne and drive the test process. Write your tests by creating the appropriate C++ class. The test member functions you add can then call your application’s C functions. Xcode creates a skeletal test class defi nition and implementation, as shown in Listing 20 - 6. All of your test classes for C++ must be direct subclasses of the TestCase class. LISTING 20 - 6: Example C++ test case #include < CPlusTest/CPlusTest.h > class StudentTests : public TestCase { public: StudentTests(TestInvocation* invocation); virtual ~StudentTests(); }; Xcode has created the framework for your class and has already added it to your unit test target. The only thing you need to do now is to write one or more test methods. Each test method: Must return void Must not take any parameters Unlike Objective - C, C++ test method names do not have to conform to any naming convention, but it is more readable if you retain the habit of starting each method name with “ test. ” An example of two such tests is shown in Listing 20 - 7. LISTING 20 - 7: Example C++ tests #include "StudentTests.h" StudentTests::StudentTests(TestInvocation *invocation) : TestCase(invocation) { } continues ➤ ➤ Creating a Unit Test ❘ 567 c20.indd 567c20.indd 567 1/22/10 4:17:13 PM1/22/10 4:17:13 PM Download at getcoolebook.com 568 ❘ CHAPTER 20 UNIT TESTING LISTING 20-7 (continued) StudentTests::~StudentTests() { } void StudentTests::testNameConstructor( ) { Student student("Jane Doe"); CPTAssert(strcmp(student.getName(),"Jane Doe")==0); } void StudentTests::testNameProperty( ) { Student student(); CPTAssert(student.getName()==NULL) student.setName("Jane Doe"); CPTAssert(strcmp(student.getName(),"Jane Doe")==0); } C++ Test Registration C++ does not include the kind of introspection that Objective - C uses to discover the test classes and methods that you ’ ve defi ned. Consequently, you must tell the C++ unit test framework exactly what tests you ’ ve defi ned. You accomplish this by registering the tests using static constructors, as shown in Listing 20 - 8. LISTING 20 - 8: C++ test registration StudentTests studentTestsNameConstructor(TEST_INVOCATION(StudentTests, testNameConstructor)); StudentTests studentTestsNameProperty(TEST_INVOCATION(StudentTests, testNameProperty)); For every test you want run, you must create an instance of your test class, passing a TestInvocation object to its constructor. To make this easier to code, the unit test framework provides a TEST_INVOCATION macro, which creates a confi gured instance of the TestInvocation class for you. The macro parameters are the name of your TestCase subclass and the test function. You can give the static variable any name you want, but it is more readable if you give it a name that describes the test. Remember that these object names are public, so generic names like test1 are likely to collide with similar names from other TestCase classes. Each invocation object is constructed when the application starts up. The constructor for the TestCase class registers the test with the unit test framework. Thus, as soon as the application is ready to run, the testing framework has a complete list of the tests to be executed. C++ Test Macros When you write your test, you will employ the CPTAssert macro to evaluate the success of each test. If the argument to the macro evaluates to a non - zero value, the test was successful. If not, the test and a signal with a description of the failure is passed back to the unit test framework. Test failures appear as an error in the build log. Examples of using CPTAssert were shown in Listing 20 - 7. c20.indd 568c20.indd 568 1/22/10 4:17:18 PM1/22/10 4:17:18 PM Download at getcoolebook.com C++ Test Execution In addition to registering the tests, a C/C++ application being tested by a dependent unit test needs to invoke the unit tests at the appropriate time. Unlike an Objective - C application, C applications can ’ t be automatically intercepted to prevent their normal execution. You must add code to your application to run the tests and exit — but only when your application is being run for the purposes of unit testing. This section describes the code you need to add to non - Carbon applications — that is, any kind of process that doesn ’ t use a Carbon run loop — and a less invasive method you can use with Carbon (run loop) applications. For command - line applications, this is simply a matter of inserting some code into your main() function. You insert this code after the point in your application where your unit tests can be run — typically after any required initialization — but before the application actually starts running. Assuming your application has no special initialization, the example in Listing 20 - 9 shows what you need. LISTING 20 - 9: Unit test hook for main() // Conditional support for C/C++ unit tests #ifndef UNIT_TEST_SUPPORT #define UNIT_TEST_SUPPORT 1 #endif #if UNIT_TEST_SUPPORT #include < CPlusTest/CPlusTest.h > #endif int main (int argc, char * const argv[]) { //Perform any required initialization here #if UNIT_TEST_SUPPORT TestRun run; // Create a log for writing out test results TestLog log(std::cerr); run.addObserver( & log); // Get all registered tests and run them. TestSuite & allTests = TestSuite::allTests(); allTests.run(run); // If the TestSuite ran any tests, log the results and exit. if (run.runCount()) { // Log a final message. std::cerr < < " Ran " < < run.runCount() < < " tests, " < < run.failureCount() < < " failed." < < std::endl; return (0); } // Otherwise, run the application normally. #endif Creating a Unit Test ❘ 569 c20.indd 569c20.indd 569 1/22/10 4:17:19 PM1/22/10 4:17:19 PM Download at getcoolebook.com [...]... run, the code reports the success or failure of those tests and exits immediately The defi nitions of the TestRun, TestLog, TestSuite, and related classes are included in the special unit testing framework that was added to your system when you installed the Xcode Developer Tools These classes do not normally exist in a standard installation of Mac OS X, and your application will fail to start without... Else, fall through and continue running the application } } Download at getcoolebook.com c20.indd 571 1/22/10 4:17:26 PM 572 ❘ CHAPTER 20 UNIT TESTING The static constructor for installTimer causes an instance of this object to be created during the initialization of your application Because the constructor is part of the code in the unit test, the UnitTestRunner object is only created when your application... test code, rather than your application The problem is that unit tests run during the build phase, not the debug phase, of the Xcode environment The tests themselves are never the target of a Debug or Run command, and you have the added catch-22 of trying to build an application whose unit tests fail Debugging iPhone Application Tests However awkward an iPhone application (dependent) unit test is to set... application tests, simply run your test application target under the control of the debugger (Run ➪ Debug) Figure out what’s wrong and then return to running your application normally Debugging Dependent Mac OS X Unit Tests Debugging dependent unit tests for an application requires that you reverse the normal order of targets and trick the application target into running your unit tests instead of executing... application is devoid of any references to the unit testing classes and framework Using the previous example, the Release build configuration of this project could defi ne the UNIT_TEST_SUPPORT=0 preprocessor macro to ensure that no unit test code is compiled in the fi nal version For Carbon applications, you can intercept the start of program execution far more elegantly Any technique (like the one just demonstrated)... bundle, there is no constructor, no timer object is created, and your application starts running normally The beauty of this scheme is that it requires no modification to your application There is no possibility of accidentally producing a version of your application that contains any unit test support, and you can test the fi nal application binary you intend to release DEBUGGING UNIT TESTS Who watches... protected: static void testTimerFired(EventLoopTimerRef timer, void* userData); void runTests(EventLoopTimerRef timer); }; Download at getcoolebook.com c20.indd 570 1/22/10 4:17:22 PM Creating a Unit Test ❘ 571 UnitTestRunner.cpp #include #include "UnitTestRunner.h" UnitTestRunner installTimer; // static constructor to create timer UnitTestRunner::UnitTestRunner() : timerUPP(NULL),... (void)InstallEventLoopTimer(GetMainEventLoop(),0,0,timerUPP,this,&timerRef); } UnitTestRunner::~UnitTestRunner() { // Destroy the timer if (timerRef != NULL) { RemoveEventLoopTimer(timerRef); timerRef = NULL; } if (timerUPP != NULL) { DisposeEventLoopTimerUPP(timerUPP); timerUPP = NULL; } } // Static method to bridge the call to the local instance void UnitTestRunner::testTimerFired(EventLoopTimerRef timer, void* userData) { ((UnitTestRunner*)userData)->runTests(timer);... the debugger Here’s how: 1 Open the Info window for the project (Project ➪ Edit Project Settings) In the General tab, change the Place Intermediate Build Files In setting to Build Products Location 2 3 4 Remove the application target dependency from the unit test target Set the active target to the application target and build it (Build ➪ Build) Add the unit test target as a dependency for the application . void SieveOfEratosthenesTests::testPrimes( ) { // Test a number of known primes static int knownPrimes[] = { 2, 3, 5, 11, 5 03, 977, 1 234 7, 439 357, 101 631 947 }; SieveOfEratosthenes testSieve(UNIT_TEST_MAX_PRIMES); . false , the CPTAssert macro signals to the testing framework that the test failed. The testing macros are described in the “ Objective - C Test Macros ” and “ C++ Test Macros ” sections. c20.indd. at a time. Objective - C Test Macros When you write your test, you will employ a set of macros to evaluate the success of each test. If the assertion in the macro fails to meet the expectation,